Skip to content

Fix terminal run publication atomicity - #1070

Merged
dennisonbertram merged 9 commits into
mainfrom
codex/issue-1067-terminal-status-event-atomicity
Jul 31, 2026
Merged

Fix terminal run publication atomicity#1070
dennisonbertram merged 9 commits into
mainfrom
codex/issue-1067-terminal-status-event-atomicity

Conversation

@dennisonbertram

@dennisonbertram dennisonbertram commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Closes #1067

Summary

Makes terminal run publication one-way consistent: whenever GetRun exposes
completed, failed, or cancelled, the matching terminal event is already
replayable. Terminal event persistence, conversation replay, recorder drain,
final status persistence, in-memory commit, and fanout now use one serialized
transition path.

The reviewed failure policy is explicit:

  • terminal event append failure prevents durable terminal status but still
    completes bounded live publication;
  • final terminal status update failure preserves the truthful in-memory state
    and protects it from pruning;
  • unresolved store-backed terminal states are bounded by fail-closed
    Start/Continue admission with HTTP 503 terminal_durability_unavailable;
  • status-only gaps are retried under one deadline of at most 250 ms without
    store I/O under runner, status, journal, or conversation locks;
  • StorageModeNone remains the intentional event-suppression exception and
    no-store runs do not activate durable backpressure.

Current-main semantic integration

This head semantically integrates the complete #1067 transition series with
current main's #1054 waiting/pending publication lifecycle. Both now share one
context-aware per-run status lock.

Frontier review caught and repaired an initial merge regression: nonterminal
status persistence had been moved ahead of the live commit, so a failed
best-effort UpdateRun could leave a real AskUser question registered while
API, TUI, and GUI still saw running. Nonterminal state now commits live first,
retains the bounded store attempt, and returns failure so strict
waiting_for_user status/event publication can retry. Terminal publication
retains event-before-status ordering.

Tests and evidence

Final integrated head: b45b4334aea593704581ed5c0b38a95b942e3193,
based on origin/main e6e255d8ba60d1ae3035031f1342ae22b0c5d74d.

  • New deterministic red/green regression:
    TestNonTerminalStatusPersistenceFailureKeepsLiveStateMoving.
  • Focused waiting/terminal/store-failure suite, normal and race, -count=100
    — PASS.
  • Complete internal/checkpoints, internal/harness, and internal/server
    normal and race suites — PASS.
  • go vet on affected packages — PASS.
  • Direct foreground non-TTY
    TMPDIR=/private/tmp GOCACHE=/private/tmp/gocode-go-cache ./scripts/test-regression.sh
    — PASS: repository normal, full race, coverage 85.7%, minimum 80.0%, zero
    uncovered functions.
  • Frontier semantic review — PASS after the failing-store correction.
  • Independent cheap-tier exact-head rereview — PASS with no P1/P2 findings;
    focused harness normal -count=20 and race -count=10 also passed.

Full retained #1067 regressions cover terminal status/replay ordering,
append/update failure and timeouts, recorder drain, redaction suppression,
same-conversation ordering, delayed nonterminal transitions, bounded degraded
admission, retry recovery, HTTP 503 behavior, retention/pruning, and concurrent
continuation reservations.

Hosted gate

Both hosted gates passed on exact head
b45b4334aea593704581ed5c0b38a95b942e3193:

  • test-fast run 30672325842 — PASS.
  • test-race run 30672325850 — PASS.

Scope and rollout

No route schema, event type, persistence schema, cron/callback behavior, TUI,
or macOS code changes. Roll out as an ordinary daemon update. Monitor store
append/update failures and terminal_durability_unavailable. Roll back if
terminal fanout deadlocks, healthy stores spuriously reject admission,
status/replay ordering regresses, or retention grows without bound. During a
real store outage, stop producers and preserve the process before rollback so
truthful in-memory terminal state is not lost.

Checklist

  • Structured issue and current acceptance addenda
  • Full impact map and durable plan/log updates
  • Deterministic red-green regressions
  • Full normal, race, vet, and coverage verification
  • Semantically integrated onto current main
  • Independent exact-head rereview complete
  • Hosted checks green on this exact pushed head

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@dennisonbertram

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8161dd4435

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/harness/runner.go Outdated
Comment on lines +5414 to +5416
journal.dispatch(delivery)
if terminalCommit != nil {
terminalCommit()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject cancellation after terminal sealing

When terminal persistence or recorder draining is slow, prepareLocked has already set state.terminated, but the status remains active until this delayed commit. During that window CancelRun still accepts the run based solely on its non-terminal status and calls killBackgroundJobsForRun, so cancelling a run whose completed event has already won can destroy intentionally detached jobs even though the run ultimately reports completed. Status-gated control paths should also reject a sealed/transitioning run.

Useful? React with 👍 / 👎.

@dennisonbertram
dennisonbertram force-pushed the codex/issue-1067-terminal-status-event-atomicity branch from 8161dd4 to 78d07ad Compare July 31, 2026 16:06
@dennisonbertram

Copy link
Copy Markdown
Owner Author

Rebased cleanly onto current main d8e7c261fc5680c10d599be080844d0837587b98; new head is 78d07ada774b9dd775e899a52986c4007af6271d. On the rebased SHA, complete internal/harness + internal/server normal/race tests and vet passed, and unchanged ./scripts/test-regression.sh passed normal, race, and coverage (85.6%, zero uncovered functions). @codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78d07ada77

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/harness/runner.go Outdated
if r.terminalBeforeDispatchHook != nil {
r.terminalBeforeDispatchHook(runID, eventType)
}
journal.dispatch(delivery)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release the global journal lock before recorder drain

When RolloutDir is enabled and the terminal recorder is slow or stalled, this dispatch call holds the runner-wide conversationEventMu while waiting up to 30 seconds to send and another 30 seconds to drain. Every unrelated emit and SubscribeConversationFrom also needs that mutex, so one run's terminal filesystem delay can freeze event publication and conversation SSE registration across the entire runner; previously terminal recorder dispatch happened after releasing this lock. Preserve the same-conversation persistence/fanout ordering without keeping the global journal mutex held through the bounded recorder waits.

Useful? React with 👍 / 👎.

@dennisonbertram
dennisonbertram force-pushed the codex/issue-1067-terminal-status-event-atomicity branch from 78d07ad to 5f106be Compare July 31, 2026 18:45
# Conflicts:
#	docs/plans/active-plan.md
#	internal/harness/runner.go
#	internal/harness/runner_event_journal.go
@dennisonbertram
dennisonbertram merged commit b45b433 into main Jul 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Publish terminal run event before terminal status becomes observable

1 participant