|
| 1 | +# Plan: Make Terminal Status and Event Publication Atomic |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +- Governing GitHub issue: #1067. |
| 6 | +- Problem: terminal helpers publish `Run.Status` before the event journal has |
| 7 | + prepared, persisted, and exposed the matching terminal event. A concurrent |
| 8 | + `GetRun` can therefore return completed, failed, or cancelled while an |
| 9 | + immediate `Subscribe` replay still ends at a non-terminal event. |
| 10 | +- User impact: API polling, SSE reconnect, CLI/TUI, and macOS clients can |
| 11 | + briefly render terminal state without the authoritative terminal transcript |
| 12 | + or its preceding causal/error evidence. |
| 13 | +- Constraints: preserve durable-before-fanout, terminal sealing, recorder |
| 14 | + drain ordering, bounded store writes, run-independent query availability, |
| 15 | + current event/status schemas, cleanup ordering, and the explicit |
| 16 | + `StorageModeNone` terminal-redaction policy. |
| 17 | + |
| 18 | +## Scope |
| 19 | + |
| 20 | +- In scope: one shared Runner terminal-transition seam for completed, failed, |
| 21 | + cancelled, max-step failed, and max-turn failed paths; deterministic |
| 22 | + concurrency and replay regressions; real HTTP poll-then-replay proof. |
| 23 | +- Out of scope: PR #1060/#1055 changes, cron/callback behavior, conversation |
| 24 | + cursor redesign, GUI visual changes, provider routing, schemas, and workflow |
| 25 | + timing issue #1049. |
| 26 | + |
| 27 | +## Documentation Contract |
| 28 | + |
| 29 | +- Feature status: implemented and fully verified locally; hosted checks pending. |
| 30 | +- Public docs affected: none; existing terminal event/status wire formats stay |
| 31 | + unchanged. |
| 32 | +- Spec docs before code: this plan and its linked impact map. |
| 33 | +- Implementation notes after code: engineering, observational, system, and |
| 34 | + long-term logs plus the plans index and active plan. |
| 35 | + |
| 36 | +## Test Plan (TDD) |
| 37 | + |
| 38 | +- First red: a deterministic phase barrier pauses each completed, failed, and |
| 39 | + cancelled helper after the old status write but before terminal event |
| 40 | + publication. Concurrent `GetRun` plus `Subscribe` must never observe that |
| 41 | + forbidden state. |
| 42 | +- Causal control: on an error-chain-enabled failure, the required |
| 43 | + `error.context` snapshot must precede `run.failed` before failed status is |
| 44 | + observable. |
| 45 | +- Store/recorder controls: block terminal store append and preserve the existing |
| 46 | + recorder drain regressions; assert target status remains non-terminal while |
| 47 | + unrelated run queries remain responsive, then becomes terminal only after |
| 48 | + replay, durability, and recorder delivery are ready. Block status-store |
| 49 | + persistence separately and prove unrelated event journals remain responsive. |
| 50 | +- Concurrency control: race competing terminal transitions and require the |
| 51 | + winning status to match the single sealed terminal event; hold a terminal at |
| 52 | + the pre-fanout boundary and prove a later same-conversation event cannot |
| 53 | + overtake it for an existing conversation subscriber. |
| 54 | +- Real path: HTTP `GET /v1/runs/{id}` followed immediately by run-event SSE |
| 55 | + replay contains the matching terminal event for all three statuses. |
| 56 | +- Focused stress: normal and race at `-count=100`. |
| 57 | +- Affected gates: `internal/harness` and `internal/server` normal/race and vet. |
| 58 | +- Repository gate: unchanged foreground non-TTY |
| 59 | + `./scripts/test-regression.sh`. |
| 60 | +- Hosted gates: required PR checks, including `test-fast` and `test-race`. |
| 61 | + |
| 62 | +## Cross-Surface Impact Map |
| 63 | + |
| 64 | +- See `2026-07-31-issue-1067-terminal-status-event-atomicity-impact-map.md`. |
| 65 | + |
| 66 | +## Implementation Checklist |
| 67 | + |
| 68 | +- [x] Link contract-complete bug #1067 before implementation. |
| 69 | +- [x] Record current ownership, callers, sources of truth, and search evidence. |
| 70 | +- [x] Write this plan and impact map before code. |
| 71 | +- [x] Add and confirm the deterministic failing regressions. |
| 72 | +- [x] Implement the smallest shared terminal-transition repair. |
| 73 | +- [x] Confirm focused stress, affected normal/race/vet, and repository gates. |
| 74 | +- [x] Prove the HTTP poll-then-replay path. |
| 75 | +- [x] Update all required logs and documentation status. |
| 76 | +- [ ] Open one closing PR, push its exact head, and request `@codex` review. |
| 77 | +- [ ] Confirm hosted checks are green; do not merge. |
| 78 | + |
| 79 | +## Risks and Mitigations |
| 80 | + |
| 81 | +- Risk: holding `Runner.mu` across persistence would block unrelated queries. |
| 82 | +- Mitigation: retain the existing out-of-lock bounded terminal append/fanout |
| 83 | + path and test unrelated `GetRun` responsiveness while it is blocked. |
| 84 | +- Risk: concurrent terminal helpers could publish one event and a different |
| 85 | + status. |
| 86 | +- Mitigation: serialize each run's complete terminal helper lifecycle, make the |
| 87 | + shared transition return whether it won terminal sealing, and update status |
| 88 | + only for that winner. |
| 89 | +- Risk: moving status later could reorder cleanup, causal snapshots, audit, |
| 90 | + profile persistence, backup, or pruning. |
| 91 | +- Mitigation: pin causal/event order and keep cleanup before terminal |
| 92 | + transition, operational side effects after the matching status/event pair. |
| 93 | +- Risk: recorder or store failure could weaken the existing lifecycle. |
| 94 | +- Mitigation: preserve bounded non-fatal persistence semantics, recorder drain, |
| 95 | + terminal retention, and terminal-event-persisted pruning guards. |
| 96 | +- Risk: an explicit terminal `StorageModeNone` policy intentionally removes the |
| 97 | + matching event from replay. |
| 98 | +- Mitigation: preserve and test the existing redaction exception and scope the |
| 99 | + stronger replay implication to terminal events retained by policy. |
| 100 | + |
| 101 | +## Verification Evidence |
| 102 | + |
| 103 | +- Semantic red: |
| 104 | + `go test ./internal/harness -run |
| 105 | + '^TestTerminalStatusNeverPrecedesTerminalReplayEvent$' -count=1` failed all |
| 106 | + three cases: completed before `run.completed`, failed after `error.context` |
| 107 | + but before `run.failed`, and cancelled before `run.cancelled`. |
| 108 | +- Focused final green: terminal atomicity, competing transitions, blocked store, |
| 109 | + and HTTP replay passed normal and race at `-count=100` across |
| 110 | + `internal/harness` and `internal/server`. |
| 111 | +- Affected packages: complete harness/server normal and race passed; `go vet` |
| 112 | + passed. |
| 113 | +- Real path: HTTP terminal polling followed immediately by Last-Event-ID run |
| 114 | + SSE replay passed for completed, failed, and cancelled. |
| 115 | +- Repository: unchanged foreground non-TTY `./scripts/test-regression.sh` |
| 116 | + passed normal, race, and coverage |
| 117 | + (`total=85.6%`, `zero-functions=0`). An earlier coverage attempt hit transient |
| 118 | + real-Keychain timeouts plus an OpenRouter connection reset; the unchanged |
| 119 | + retry passed completely without code or command changes. |
0 commit comments