|
| 1 | +# Context: why this work exists |
| 2 | + |
| 3 | +## The incident in three sentences |
| 4 | + |
| 5 | +During live QA of two parallel permission-gated shell commands (session `db58551b`, |
| 6 | +2026-07-19), both commands executed exactly once and only after their approvals, but the |
| 7 | +system misreported both, flipped an already-approved card back to "waiting", and then went |
| 8 | +silent because the user's final approval was never sent to the runner. The root causes are |
| 9 | +that the durable session record stores every approval request but never any approval answer, |
| 10 | +that the post-pause cleanup invents tool results (a false "not executed" error for an |
| 11 | +approved running call, and a false success for a call that never started), and that the |
| 12 | +frontend only dispatches approvals once every visible card looks settled, a condition that |
| 13 | +can never hold after a state rebuild. A seventh, unrelated defect found in the same logs |
| 14 | +makes the new `session_turns` ingestion fail with HTTP 500 on every warm turn, because the |
| 15 | +turn index is computed once per environment acquire instead of once per turn. |
| 16 | + |
| 17 | +## Goals |
| 18 | + |
| 19 | +1. Fix the session-turns counter so `turn_index` is a true conversation-turn counter, and |
| 20 | + make the API answer a duplicate append with 409 Conflict instead of 500. |
| 21 | +2. Stop the pause cleanup from inventing tool results: an approved, executing call keeps its |
| 22 | + real result, and a never-started call is recorded as deferred, never as success. |
| 23 | +3. Persist the answer half of every gate (a new `interaction_response` record event plus the |
| 24 | + allow/deny verdict in the interaction row's existing `resolution` field), and make |
| 25 | + frontend hydration overlay answers onto requests, so a rebuilt conversation shows |
| 26 | + answered cards as answered. |
| 27 | +4. Dispatch each approval per card the moment the user answers it, and make the runner |
| 28 | + accept a resume that answers only part of the parked gates while remaining parked on the |
| 29 | + rest. |
| 30 | +5. Add a regression test that reproduces the exact incident shape, then verify with live QA |
| 31 | + on the dev stack. |
| 32 | + |
| 33 | +## Non-goals |
| 34 | + |
| 35 | +Two related fixes are deliberately out of scope here. |
| 36 | + |
| 37 | +- **Real turn cancellation** (the Zed-style cancel-then-restart when new user text arrives |
| 38 | + while gates are parked, which fixes defect 6 of the incident report) goes to Arda after |
| 39 | + JP's sessions PRs merge, because it depends on the session-streams control plane those PRs |
| 40 | + introduce and is partly a product decision. |
| 41 | +- **Audit-hardening of record ids** (scoping stable record ids by turn so contradictory |
| 42 | + results append instead of overwriting, defect 5) is queued separately, because the answer |
| 43 | + persistence in this project already fixes the user-facing rebuild problem and the id |
| 44 | + rescope touches the record identity contract end to end. |
| 45 | + |
| 46 | +## Constraints already decided |
| 47 | + |
| 48 | +- Work lands in this order, on these lanes: the counter fix on the existing rebase lanes for |
| 49 | + PR #5376 (runner, branch `sessions-rebase/runner`) and PR #5375 (backend, branch |
| 50 | + `sessions-rebase/backend`), with a wipe of the wrongly numbered dev rows and an |
| 51 | + explanatory note for JP; everything else on the PR #5382 lane (branch |
| 52 | + `plan/concurrent-approvals`). |
| 53 | +- `turn_index` is confirmed to be a true conversation-turn counter, not an acquire counter. |
| 54 | + The implementation must carry a code comment stating this invariant, and the PR body must |
| 55 | + explain what was done and what was assumed. |
| 56 | +- The implementation will be done by an agent that has not read the incident conversation, |
| 57 | + so every step in `plan.md` is specified with exact files, behavioral contracts, and |
| 58 | + acceptance criteria. |
0 commit comments