Repository navigation
fix(app-server): reconcile loaded thread history on resume - #30866
steipete-oai wants to merge 6 commits into
Conversation
|
Tracking this PR from a downstream app-server client / Codex Mobile integration. The direction here looks relevant to a recent sanitized failure we saw around loaded/resumed thread state versus durable rollout visibility. Local signal, stripped of user/thread/path details:
This PR's resume/history reconciliation work seems like the closest upstream fix direction for that class of mismatch. We added a downstream post-submit durability check so Mobile will fail explicitly instead of projecting a non-materialized turn as active, but the root fix still appears to belong in app-server history/resume reconciliation. |
|
Hey @steipete-oai — thanks for putting this together. The reconciliation-on-resume approach is exactly what we were hoping for, and the test coverage looks thorough. We're building a workflow that injects external content into a running Codex Desktop thread so the local agent can pick it up in context. This fix is the missing piece for us — right now the thread doesn't reflect the injected turns until the user manually kills Codex app and reopen, which breaks the experience. Any rough sense of when this might land and ship in a release? Happy to test it against our setup in the meantime if a pre-release build is available. Thanks again for the detailed write-up in the PR description — it made the approach very clear. |
|
@steipete-oai Please ship this. 🙏 It's really annoying to be forced to restart my Codex (ChatGPT) app every day just because changes made to other sessions by my Codex automations are not properly picked up. E.g. I have a cleanup automation, that automatically deletes sessions that are not needed any more and become stale. Another automation creates sessions. Cleaned up sessions are not being removed. Created sessions are not added to the UI. I don't get the blue changed notification dot, etc. |
Codex Desktop can classify a task as interrupted before a fresh thread/resume returns its completed durable tail, then issue an implicit empty turn/start. Fence that stale continuation per live connection while preserving explicit and non-Desktop empty turns. Constraint: Empty turn/start is an intentional App Server contract. Constraint: The Desktop client cannot be patched from this open-source repository. Rejected: Reject all empty turn/start requests | breaks intentional continuation clients Rejected: Rewrite rollout history during resume | broader risk and overlaps draft upstream reconciliation work Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep the fence scoped to codex_desktop and clear it only for the acting connection or connection lifecycle. Tested: Focused fence matrix, ascending-page running resume, existing empty-input contract, and two-client WebSocket isolation. Not-tested: Full codex-app-server regression is not green because 17 environment-dependent code-mode, MCP, and plugin tests lack their required binaries or isolated user configuration. Related: openai#30866 Related: openai#39885
Summary
thread/resumeis called.Motivation
Fixes #21743. Related focus-refresh report: #21177.
When two app-server clients share a thread, one can append work after another client has loaded it. The loaded app-server previously answered
thread/resumefrom stale in-memory history, so Desktop could not discover the external turns until its app-server restarted. This also preserves the ordering requirement identified in #11756 (comment): history hydration and listener attachment remain serialized.Approach
The app-server remains the source of truth rather than introducing a local file or SQLite watcher, which would not generalize to remote app-servers.
For a loaded idle thread, resume now:
The storage and fresh metadata reads run off the listener behind an explicit event-delivery cut. The Finish phase projects the final overlay for the response, drains pre-cut events to prior subscribers only, then resumes normal track-one/dispatch-one delivery. High-volume legacy exec deltas continue draining to existing subscribers while storage is read, and Finish consumes only a finite queue snapshot. This prevents joiner duplicates, listener stalls, and unbounded output buffering.
Busy, incomplete, and conflicting histories fail closed or retry without overwriting live state. The cursor ignores metadata-only appends. After an ambiguous append failure, recovery accepts only identical history or a safe strict extension of the complete in-memory snapshot; older and divergent reads conflict without mutation.
Rejected resumes leave client compatibility settings untouched. Append-only reconciliation also preserves the current server-measured auto-compaction prefill; canonical rewrites and window changes reset it. Imported trailing compaction queues its next-turn lifecycle source exactly once, including cursor-mismatch reconstruction. Point operations such as rollback lease the listener without implicitly subscribing, and lease acquisition is serialized with idle unload.
The one-shot token-budget reminder now carries a distinct contextual marker. Cold resume and live reconciliation derive its delivered latch from the surviving current-window history, so an external reminder is not emitted twice and rollback or compaction rearms it correctly. Older unmarked reminder text remains intentionally indistinguishable from arbitrary configured developer text.
The diff is test-heavy: most added lines cover core reconstruction invariants and cross-process app-server ordering.
The commits retain a review seam between core/storage reconciliation and app-server delivery ordering; the draft keeps them together because the externally observable no-loss/no-duplicate guarantee spans both layers.
Verification
mainportability cleanup removed 806 net lines while preserving all 34 core reconciliation, 22 lifecycle, and 42 V2 resume tests; independent scenario-parity and structured code reviews were cleanorigin/mainunder this managed host's forced policy and skill rootsmainfollow-up: 34/34 reconciliation cases, 22/22 lifecycle cases, and the host-independent interrupted-resume regression passed; projected-item fixtures now use target-native absolute pathsjust fix -p codex-core,just fix -p codex-app-server, andjust fmtFollow-up
Experimental raw-event opt-in remains thread-scoped. Per-connection raw-event capabilities are a separate architectural change; this PR applies response coverage and redaction only to buffered handoff events.