feat(session): start sessions from locked stored publication state - #180
feat(session): start sessions from locked stored publication state#180cursor[bot] wants to merge 41 commits into
Conversation
Store participant and published-release identity for SessionState::Created with exact replay and fail-closed rebinding. Command-replay persistence stays outside this first slice.
Assert the Database error message and source, and fail the replay SELECT after ON CONFLICT by redirecting search_path so classify runs instead of the insert.
Linux llvm-cov leaves the isolated query_one ? tail uncovered unless the Err arm is an explicit match. Keep the search_path redirect test.
Linux branch coverage missed the later AND operands of exact-replay classification. Rebind each stored field independently, and prove a domain-legal u64::MAX creation time fails closed as ValueOutOfRange.
The replay SELECT failure constructed Database evidence without checking its safe display text or source, leaving those two production lines uncovered on Linux.
SHOW transaction_isolation can fail after the caller transaction is already aborted. Persist must surface that as a typed database error instead of leaving the probe Result uncovered.
Satisfy clippy::manual_let_else in the library test that instantiates AssessmentSessionPersistenceError::Database.
Name instrument_version_ref in the public persist contract and assert the committed version column. Keep TRACEABILITY, changelog, and as-built schema at Active PR #61 rather than promoting the slice to protected-main truth. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Restore persisted created-session identity from PostgreSQL without asking whether the original release still accepts new sessions, so later suspend or retire cannot rewrite provenance. Missing rows return none; later stored states and malformed lookup references fail closed. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Name the opened successor so TRACEABILITY, as-built schema, and ERD point at the persist-and-load head instead of the persist-only #106 slice. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Rejecting a shorter command history is not enough under READ COMMITTED. Lock the created-session row with SELECT … FOR UPDATE before inserting or counting commands so a concurrent Activate-only persist cannot count a prefix and then rewind a later Pause/Resume projection. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Point TRACEABILITY, as-built schema, and ERD at the successor that locks assessment_session before command insert or count. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
New sessions must call AssessmentSession::new through created_session_for_start / start_created_assessment_session so a draft, suspended, or retired release cannot insert a row. Keep the #146 header-row lock and command-history persist. Reconstitution remains load, not start. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Name the start-boundary landing vehicle so TRACEABILITY, ERD, UML, and as-built schema point at this head instead of the #146 lock predecessor. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
In-memory AssessmentSession::new is not enough: a stale Published object could insert after another transaction persisted Suspend or Retire. Lock instrument_release with SELECT FOR UPDATE in the same start transaction, add start_created_assessment_session_from_stored_release, and fail closed on missing, unpublished, locale-mismatched, or digest-mismatched stored evidence. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Point TRACEABILITY, as-built schema, ERD, and UML at the successor that locks instrument_release before a new session insert. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Queue
This remains the preferred session-start landing while it locks stored
instrument_releasewithSELECT … FOR UPDATEbefore insert. Prefer this head over #188 (in-memory start on the #154 lock lineage), #164, #153, and #154 for the start boundary. Do not merge those in parallel.Residual (successor, not a lock defect)
start_created_assessment_sessioncallscreated_session_for_startfirst. After a later suspend, an exact HTTP retry of an already stored start fails closed as unpublished instead of returning the original session.persist_assessment_sessionexact replay still works if the caller already has the Created aggregate, but the start boundary itself does not load-and-return that row. HTTPPOST /v1/sessionsidempotency after publication withdrawal needs that replay, or a documented fail-closed retry contract.This is not merge approval. Keep Draft until exact-head checks and independent last-push review land. Do not self-approve.
Sent by Cursor Automation: Fix Issues
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Review (
cca5192)No blocking start-path defect on the stored-publication lock.
load_published_instrument_releaseusesSELECT … FOR UPDATE, and durable start fails closed on missing/unpublished/locale-mismatched evidence before insert. Command-history header lock is a separate slice on this lineage.Residual: persist classification of an existing
instrument_releaserow remains unlocked. A Duplicate published result can still lose the row to a concurrent Suspend/Retire. That persist-side lock is #196; do not fold HTTP or a second start-lock rewrite into this head.This review is not independent last-push approval. Do not merge while draft/checks are incomplete. Prefer this head over #164/#153/#138 for locked stored-release start. Prefer #196 for persist-classification lock. HTTP
POST /v1/sessionsremains #149.Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Review (exact head cca5192)
Stored-publication start locks and inserts on the same caller transaction. start_created_assessment_session and start_created_assessment_session_from_stored_release both take SELECT … FOR UPDATE on instrument_release before INSERT. A stale in-memory Published object fails closed when the stored row is missing, unpublished, or digest-mismatched. Locale has no silent fallback. Exact start replay while still published returns Duplicate.
This head is the lock slice. It is not the merge vehicle now that #198 exists.
Prefer
Prefer #198 (6e9e244) for start-from-store plus exact start replay after later persist Suspend or Retire. Prefer this head over #153, #164, #188, #154, #146, and #138 only if #198 is abandoned.
Residuals (do not block the lock itself)
persist_assessment_sessionstill first-inserts a reconstituted Created aggregate with no stored publication lock. Exact replay of an already-stored Created row after suspend must stay legal; first insert after suspend must not. That seal is still open.- Exact start retry after persist Suspend/Retire fails closed as unpublished instead of returning the original session. #198 claims that retry contract.
- Stored-path tests prove Suspend and digest mismatch. Retire, missing stored row, and version mismatch share the same fail-closed branch and are not asserted on the PostgreSQL path.
- No
tenant_refonassessment_session. Isolation is the globalsession_refprimary key. HTTP must not treat this slice as tenant-isolated. - HTTP
POST /v1/sessionsremains #149 / #161 and must callstart_created_assessment_session_from_stored_release(or load+start on the same transaction), notpersist_assessment_session.
Checks / merge
Keep Draft. Do not self-approve. Do not merge until exact-head checks and independent last-push approval land. PostgreSQL jobs need TEST_DATABASE_URL. CodeRabbit CLI agent auth timed out in this environment; this review is from the source at cca5192.
Next slice after #198: seal persist_assessment_session first-insert against unpublished reconstitution.
Sent by Cursor Automation: Fix Issues
| /// Returns [`AssessmentSessionPersistenceError`] for unsupported isolation, | ||
| /// a non-created session, conflicting replay, an out-of-range timestamp, | ||
| /// or a database failure. | ||
| pub fn persist_assessment_session( |
There was a problem hiding this comment.
persist_assessment_session is still a public first-insert API. A reconstituted from_persisted_created aggregate can INSERT after persist Suspend/Retire because this function never locks instrument_release.publication_state.
Keep exact replay of an already-stored Created row after later suspend. Seal only the first-insert path: if no assessment_session row exists, require the same locked published snapshot that start uses, then INSERT. That residual is out of scope here and is the next slice after #198.
| .unwrap(); | ||
|
|
||
| client | ||
| .execute( |
There was a problem hiding this comment.
This proves stored suspended plus a stale in-memory Published object cannot insert. Add the same assertion for retired, a missing instrument_release row, and a stored instrument_version_ref mismatch. Those arms already return InstrumentReleaseUnavailable / InvalidStoredRelease; they are not exercised on this PostgreSQL path.


Superseded by exact-retry and HTTP successor line
Close this predecessor without merge. Its stored-publication
FOR UPDATEstart lock was carried into #198 and then into the current durable-session HTTP line #232.Fresh evidence immediately before closure:
cca5192bc5f10e3ae6af89ef0ea8be92c81b29776e9e2440e14b0ec057d50dd26d3add765f1e5e48(closed predecessor)c7b45145c9b693951fae498e0779d58cbe170f86main:a7637351be8f0f90c12651d3bcafd959bc52ac81Current #180 review says the lock itself is sound but explicitly selects #198 because #180 cannot replay an already-stored exact start after a later persisted Suspend/Retire. Git ancestry now proves #232 is directly ahead of #198 by 8 commits (and directly ahead of #180 by 10), so the stored-publication lock and the corrected exact-retry contract are carried forward together with the public start/reload HTTP adapter.
The same review also notes first-insert sealing as a later concern. That was subsequently handled on the persist lineage before #232; this closure does not claim the predecessor itself fixed it.
Do not treat stale #180 reviews as approval. #232 remains Draft and must independently satisfy unchanged exact-head Runtime CI/security/coverage/review gates before merge.