Skip to content

Commit 8161dd4

Browse files
fix: make terminal run publication atomic
1 parent f3fb4d8 commit 8161dd4

13 files changed

Lines changed: 1127 additions & 43 deletions

docs/logs/engineering-log.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,47 @@
11
# Engineering Log
22

3+
## 2026-07-31 (Terminal Status/Event Atomicity — Issue #1067)
4+
5+
- Symptom: aggregate race load exposed `RunStatusFailed` from `GetRun` while
6+
the same run's immediate replay ended at `llm.turn.requested` without
7+
`run.failed`; code inspection found the same status-first window on completed
8+
and cancelled paths.
9+
- Cause: every terminal helper called `setStatus` before `emit`, splitting the
10+
public run record from the event journal's ledger, bounded store append,
11+
subscriber fanout, and recorder drain.
12+
- Deterministic red: a no-sleep transition barrier reproduced all three states.
13+
Completed replay lacked `run.completed`, failed replay contained the required
14+
`error.context` but lacked `run.failed`, and cancelled replay lacked
15+
`run.cancelled` while `GetRun` already returned each terminal status.
16+
- Fix: one `transitionTerminal` seam now lets the winning terminal emit seal and
17+
append the matching event, completes the bounded store append and ordered
18+
recorder dispatch/drain, commits and persists the matching status, then fans
19+
out to subscribers. Per-run transition serialization prevents competing
20+
terminal helpers from performing mismatched side effects or overwriting the
21+
winning status.
22+
- Preserved reliability: terminal store I/O remains outside `Runner.mu`, and
23+
status-store I/O remains outside the per-conversation journal lock; unrelated
24+
`GetRun` and unrelated event journals stay responsive; durable-before-fanout, terminal
25+
redaction sealing, event IDs, causal/error snapshot order, recorder order,
26+
status persistence, backup, and pruning contracts remain intact.
27+
- Explicit exception: existing terminal `StorageModeNone` configurations still
28+
suppress the matching replay event while sealing and publishing status, as
29+
pinned by terminal-redaction tests. The stronger replay implication applies
30+
to terminal events retained by policy.
31+
- Regression coverage: completed/failed/cancelled barrier checks; required
32+
causal/error ordering; blocked terminal-store target status plus unrelated
33+
query availability; 100-iteration competing terminal races; same-conversation
34+
terminal-before-later-event subscriber ordering; and HTTP terminal poll
35+
followed immediately by Last-Event-ID SSE replay.
36+
- Verification: focused normal/race stress passed at `-count=100`; complete
37+
`internal/harness` + `internal/server` normal/race and affected `go vet`
38+
passed; unchanged foreground non-TTY `./scripts/test-regression.sh` passed
39+
normal, race, and coverage at 85.6% with zero uncovered functions.
40+
- Environmental retry evidence: the first coverage attempt hit two real
41+
Keychain 15-second kills plus an OpenRouter connection reset. A direct
42+
affected coverage run passed, and an unchanged full-gate retry passed without
43+
code, test, or command changes.
44+
345
## 2026-07-31 (Provider-Key Matrix Health Wait — Issue #1062)
446

547
- Symptom: hosted race run `30583930460` failed

docs/logs/long-term-thinking-log.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
# Long-Term Thinking Log
22

3+
## 2026-07-31 (Terminal Status/Event Atomicity — Issue #1067)
4+
5+
- Command intent: repair the aggregate-load Runner race where `GetRun` exposes
6+
completed, failed, or cancelled before the matching terminal event and
7+
required causal evidence are replayable.
8+
- User intent: external monitoring must never report a terminal result from an
9+
incomplete transcript, and this engine repair must remain isolated from PR
10+
#1060/#1055.
11+
- Success definition: deterministic no-sleep red evidence covers all terminal
12+
statuses; one shared lifecycle seam makes terminal ledger/store publication
13+
precede status visibility while preserving recorder order; immediate
14+
Subscribe and HTTP SSE replay agree; focused stress, affected race/vet, full
15+
regression, and hosted checks pass on one unmerged closing PR.
16+
- Non-goals: conversation cursor redesign, cron/callback behavior, client UI,
17+
provider routing, schemas, or workflow timing changes.
18+
- Guardrails: preserve out-of-lock bounded store writes, durable-before-fanout,
19+
terminal sealing, recorder drain order, cleanup order, causal/error snapshots,
20+
status persistence, SSE IDs, and unrelated query responsiveness.
21+
- Outcome: one winner-only transition now commits terminal ledger/store history
22+
before matching status and subscriber fanout. Deterministic all-status red,
23+
focused normal/race stress, affected normal/race/vet, HTTP reconnect, and the
24+
unchanged full regression are green locally; PR and hosted review remain.
25+
326
## 2026-07-31 (Provider-Key Matrix Health Wait — Issue #1062)
427

528
- Command intent: isolate and clear the hosted race blocker first observed on

docs/logs/observational-log.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
Use this file for observations about system behavior without immediately prescribing code changes.
44

5+
## 2026-07-31 (Terminal Run Publication Window)
6+
7+
- Concurrency observation: a terminal event can be prepared under the Runner
8+
lock and persisted outside it without blocking unrelated run queries, but the
9+
matching status needs one explicit commit point between persistence and
10+
subscriber fanout. Status before preparation yields incomplete replay;
11+
status after fanout lets terminal-event consumers briefly read `running`.
12+
- Testing observation: a phase channel at the terminal transition boundary
13+
deterministically exposes the forbidden state for completed, failed, and
14+
cancelled paths without relying on aggregate load or fixed sleeps.
15+
- Replay observation: after an HTTP status poll returns terminal, reconnecting
16+
run SSE from the first event ID must replay exactly one matching terminal
17+
event and no terminal event of another status.
18+
- Conversation-stream observation: terminal persistence and terminal fanout
19+
must share the same per-conversation critical section; releasing it between
20+
those phases lets a later run's event overtake the terminal event for an
21+
already-connected conversation subscriber.
22+
523
## 2026-07-31 (Source-Workflow Dual-Error Arbitration)
624

725
- Process observation: a child can exit non-zero while closing its stdin also

docs/logs/system-log.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
# System Log
22

3+
## 2026-07-31 (Terminal Run Transition Publication — Issue #1067)
4+
5+
- System/component: `Runner.transitionTerminal`, `Runner.emit`, and
6+
`eventJournal` terminal persistence/fanout in `internal/harness`.
7+
- Responsibilities: the event journal remains the only event-ledger writer and
8+
terminal-seal owner; the transition seam binds the winning terminal event to
9+
its completed, failed, or cancelled `Run` record.
10+
- Order: prior causal/error events -> terminal ledger append/seal -> bounded
11+
store append -> ordered terminal recorder dispatch/drain -> matching
12+
in-memory and persisted status -> subscriber fanout -> backup/pruning
13+
lifecycle.
14+
- Concurrency boundary: store and recorder I/O remain outside `Runner.mu`.
15+
`conversationEventMu` preserves replay-to-live ordering through event-store
16+
append, recorder drain, in-memory status commit, and terminal fanout; it is
17+
released before status-store I/O. The status commit briefly reacquires only
18+
the Runner state lock.
19+
- Consumers: `GetRun`, run summary, run SSE replay/live delivery,
20+
conversation replay, CLI/TUI exit handling, and macOS transcript state keep
21+
existing schemas and event IDs.
22+
- Failure boundary: bounded store errors remain non-fatal and live in-memory
23+
replay remains available; terminal redaction drops still seal and publish
24+
status by the existing explicit policy; a competing terminal helper is
25+
serialized before terminal side effects, loses the seal, and cannot write a
26+
mismatched audit/profile outcome or overwrite status.
27+
328
## 2026-07-31 (Source-Workflow Terminal Error Arbitration)
429

530
- System/component: `internal/workflow.SourceManager.runSourceWorkflow` and its
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Cross-Surface Impact Map: Issue #1067 Terminal Publication Atomicity
2+
3+
## Task
4+
5+
- Task / issue: #1067, terminal status visible before matching replay event.
6+
- Plan: `2026-07-31-issue-1067-terminal-status-event-atomicity-plan.md`.
7+
- Owner: Codex.
8+
- Status: implemented and fully verified locally; hosted checks pending.
9+
10+
## Current Ownership, Callers, and Data Flow
11+
12+
- Entry points: `completeRun`, `failRun`, `failRunMaxSteps`,
13+
`failRunMaxTurns`, and `cancelledRun`.
14+
- Source of truth: `Runner` owns `runState.run.Status`, `runState.events`,
15+
`runState.terminated`, recorder channels, and run subscribers;
16+
`eventJournal` owns append/store/fanout ordering.
17+
- Callers/consumers: step-engine completion/provider/tool/budget/cancellation
18+
paths; `GetRun`; `Subscribe`; run HTTP/SSE routes; CLI, TUI, and macOS
19+
transcript/lifecycle consumers.
20+
- Similar abstractions searched: `rg -n
21+
"setStatus|completeRun|failRun|cancelledRun|publishTerminal|GetRun|Subscribe"
22+
internal/harness internal/server`. No second terminal lifecycle owner exists.
23+
- Duplication conclusion: repair the shared Runner transition; do not add
24+
provider-, server-, or client-specific compensation.
25+
26+
## Config, API, CLI, and Tools
27+
28+
- Config/env/defaults: none.
29+
- Endpoints/request/response/wire formats: unchanged `Run`, `Event`, event IDs,
30+
SSE names, payload schema, and HTTP routes.
31+
- CLI/tools/integrations: no command changes; terminal polling and stream
32+
consumers gain a stronger ordering guarantee.
33+
- Error states: unchanged completed/failed/cancelled values and payloads.
34+
35+
## Persistence and Compatibility
36+
37+
- Schemas/migrations/caches/generated data: none.
38+
- Store order: matching terminal `AppendEvent` remains bounded and precedes
39+
recorder dispatch, terminal status `UpdateRun`, and subscriber fanout;
40+
status persistence occurs after releasing the per-conversation journal lock.
41+
- Recorder: terminal JSONL remains queued after all prior events, closed once,
42+
and drained before terminal transition returns.
43+
- Compatibility: additive ordering guarantee only; event/status values and
44+
replay IDs remain stable.
45+
- Mixed-version behavior: process-local; older daemons retain the race until
46+
upgraded, with no data migration.
47+
48+
## Lifecycle, Security, and Reliability
49+
50+
- Concurrency: the winning terminal event seals the ledger before status is
51+
updated; competing terminal helpers cannot overwrite it with a mismatched
52+
status.
53+
- Cancellation/retries/cleanup: cooperative cancellation and idempotency stay
54+
unchanged; workspace/tool/MCP cleanup remains before terminal publication.
55+
- Locks/resources: terminal store/recorder waits remain outside `Runner.mu`, so
56+
unrelated queries are not blocked; status-store I/O also remains outside the
57+
per-conversation journal lock, so unrelated event journals are not blocked.
58+
- Auth/permissions/privacy/secrets: no boundary change after searches through
59+
run routes and redaction/audit paths; terminal payload redaction remains
60+
owned by the event journal. Explicit terminal `StorageModeNone` remains the
61+
documented exception: it seals and publishes status without replaying the
62+
intentionally suppressed event.
63+
- Failure/recovery: bounded store failures remain non-fatal and in-memory replay
64+
remains authoritative for the live Runner; persisted-before-prune guards stay
65+
intact.
66+
67+
## Product and Integration Surfaces
68+
69+
- Server/runtime: `GetRun` terminal now implies immediate `Subscribe` replay
70+
contains the matching event.
71+
- TUI/web/macOS/other clients: terminal badges, failure text, exit codes, and
72+
transcript state no longer disagree during the publication window; no client
73+
code changes.
74+
- Provider/model/tool catalogs/routing: none; provider failure is only a caller.
75+
- External systems/automation: cron/callback/workflow semantics unchanged.
76+
- UX/accessibility/focus/motion: no visual or interaction change.
77+
78+
## Deployment and Operations
79+
80+
- Deployment/migrations/flags: ordinary daemon rollout; no migration or flag.
81+
- Observability: deterministic regression records transition phases without
82+
logging prompts, event payload secrets, or credentials.
83+
- Rollback: revert if terminal fanout deadlocks, unrelated `GetRun` blocks,
84+
cleanup order changes, recorder output truncates, or cancellation regresses.
85+
- Runbooks/operator docs: no public/operator command changes.
86+
87+
## Regression Tests
88+
89+
- First red: phase barrier proves old completed/failed/cancelled status can win
90+
before terminal history.
91+
- Acceptance: terminal status implies one matching replay event; failed causal
92+
snapshot precedes `run.failed`; competing terminal transitions match the
93+
winning sealed event; later same-conversation events cannot overtake terminal
94+
fanout to an existing conversation subscriber.
95+
- Store/recorder: durability and existing drain barriers preserve non-terminal
96+
status without blocking unrelated queries; a blocked status-store write does
97+
not block unrelated event journals.
98+
- Integration: HTTP poll immediately followed by run SSE replay for all three
99+
statuses.
100+
- Exact gates: focused normal/race stress `-count=100`; harness/server
101+
normal/race/vet; unchanged foreground non-TTY regression; hosted checks.
102+
103+
## Documentation and Handoff
104+
105+
- Plans/specs: issue-specific plan and this map.
106+
- Logs/indexes: engineering, observational, system, long-term, plans index, and
107+
active plan.
108+
- Public/training/release docs: none because no new route, schema, or command is
109+
introduced.
110+
111+
## Warning Check
112+
113+
- Every cross-surface heading is resolved. Unaffected surfaces are explicitly
114+
named with search and data-flow rationale above.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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

Comments
 (0)