feat(conversations): unify native session continuation - #939
Open
Neonforge98 wants to merge 5 commits into
Open
Conversation
Neonforge98
force-pushed
the
codex/audience-routing
branch
from
August 26, 2026 01:01
98d0821 to
f1420a5
Compare
This was referenced Aug 26, 2026
Neonforge98
force-pushed
the
codex/audience-routing
branch
2 times, most recently
from
August 26, 2026 03:58
60fb115 to
2ad6f12
Compare
Neonforge98
marked this pull request as draft
August 26, 2026 08:37
Neonforge98
marked this pull request as ready for review
August 26, 2026 10:08
Neonforge98
force-pushed
the
feat/pm-expansion-batch
branch
from
August 26, 2026 23:50
157e735 to
8e9b3b4
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
from
August 26, 2026 23:53
5e9bded to
a91320a
Compare
Neonforge98
force-pushed
the
feat/pm-expansion-batch
branch
from
August 27, 2026 16:48
8e9b3b4 to
61ef78b
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
from
August 27, 2026 16:48
ec54e67 to
fdc296b
Compare
Neonforge98
force-pushed
the
feat/pm-expansion-batch
branch
from
August 27, 2026 23:25
61ef78b to
223ebfa
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
2 times, most recently
from
August 27, 2026 23:42
e801165 to
1c4a772
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
from
August 30, 2026 20:34
7361aff to
d601dcf
Compare
Neonforge98
force-pushed
the
feat/pm-expansion-batch
branch
from
August 31, 2026 10:58
05f80a6 to
bf692a2
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
from
August 31, 2026 10:58
d601dcf to
320dbf1
Compare
Neonforge98
force-pushed
the
feat/pm-expansion-batch
branch
from
August 31, 2026 11:50
bf692a2 to
1112c74
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
from
August 31, 2026 11:50
320dbf1 to
87e4b35
Compare
Neonforge98
force-pushed
the
feat/pm-expansion-batch
branch
from
August 31, 2026 14:12
1112c74 to
2d86124
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
from
August 31, 2026 14:13
87e4b35 to
758b2a9
Compare
Neonforge98
force-pushed
the
feat/pm-expansion-batch
branch
from
August 31, 2026 15:12
2d86124 to
aa7c9df
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
from
August 31, 2026 15:21
758b2a9 to
36e7649
Compare
Neonforge98
force-pushed
the
feat/pm-expansion-batch
branch
from
September 1, 2026 16:52
aa7c9df to
c0b0f5a
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
from
September 1, 2026 16:52
36e7649 to
5c2d481
Compare
Neonforge98
force-pushed
the
feat/pm-expansion-batch
branch
from
September 2, 2026 00:11
c0b0f5a to
2b8c2f2
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
2 times, most recently
from
September 2, 2026 00:20
2d06a5a to
c941f95
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
from
September 2, 2026 00:45
c941f95 to
ce232a9
Compare
Neonforge98
force-pushed
the
codex/audience-routing
branch
2 times, most recently
from
September 2, 2026 01:09
6538c8e to
4e8928a
Compare
Define canonical conversation roots, typed events, turn intents, local continuation bindings, Cloud-plane synchronization, incremental timelines, durable publication outbox, and provider-independent sender metadata.
Convert canonical role/tool events into Codex and Claude Code native histories, bind provider session identities, preserve interrupted tool activity, resume through the selected CLI transport, and recover from provider context exhaustion.
Scope native identities by source session, reconcile durable queue projections, preserve interrupted and failed turns, keep runtime selection aligned with provider capabilities, and harden Cloud/Team conversation replay plus dual-instance coverage.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Imported provider histories (Codex app, Claude Code) could be read in ORG2 but not continued: there was no canonical owner of message content and delivery state, the window-scoped queue and a separate executions store disagreed after restarts, and nothing could materialize a conversation into a provider's own catalog so the native CLI could resume it. Interrupted tool results, Codex
is_error, and compaction were handled inconsistently across the two existing paths.Solution
Provider-native session continuation stacked on #844 as three reviewable commits:
feat(conversations): add canonical event plane and durable continuationEventStoreas the single owner of message content andpending/sent/failed; single persisteddeliveriesdocument replacing the window-scoped queue + separate executions storefeat(native): materialize and resume provider-native conversationsrefactor(conversations): unify native continuation and delivery ownersPotential risks
EventStoreand the single persisteddeliveriesdocument replace two older owners; any code still reading the window-scoped queue or the separate executions store is now dead by construction and was removed in the third commit.~/.claude/projects/<repo>/), so continued conversations appear in the native app's own history.Defects fixed during verification
ToolCall/ToolResultpair for an unresolved provider call (the provider's own representation of an interrupt), so every latersynchronize_native_conversationfailed its semantic-prefix check. The projection now mirrors the canonical owner;interrupted(not model-visible, no provider can carry it) is no longer compared,is_errorstill is.is_error: a failed or killed tool was replayed to Codex as a successfulfunction_call_output. Failures now use the exec envelope Codex's own reader restores byte-exact (exit_code1; interrupted → 130).Failed. Absence-of-evidence cases warn and returnOk(None); proven divergence still fails closed and blocks the next turn.lastSeqkeeps advancing. Inbound writes stay strictly validated.context_compactedtruncated the native projection to summary + suffix; the documented skip policy is restored.onEditSubmit, submitting the message to the agent as a prompt instead of retrying the comment. Discussion rows now only retry.editCommentno longer RPCs a local-only optimistic id.File::lock/unlock; twoquestion_marklints.@-mention row activates onmousedown, soclickRenderedwas a silent no-op reported as success; the C3 scenario now uses the driver'svisibleClick. Itspending → failedDOM assertion is scoped to what an instantly-refused endpoint can render; the state machine stays covered at the store layer.Acceptance status — stated plainly
The native-continuation acceptance (
E2E_NATIVE_CONTINUATION_LIVE=1) had never been executed before this pass: its anchors readcallId/displayStatus/displayVariant, none of which exist on therawEventscontract. Anchors were re-expressed against the declared contract,assertHistoryfixed (JSON-escaped haystack could never match multi-line anchors), and the ≥40-event bar kept as a hard floor. With real accounts and a real 649-turn Codex session the run opens the session, passes the size bar, and stopped at the runtime-switch step. Root cause: harness, not product. The Claude source option did render (the driver's ownwaitUntilfor it passed), butisSessionPatchedToasserts that the imported session row was mutated — which this PR deliberately never does forexternal_historysessions: the pick is written toconversationTargetOverridesAtomkeyed by the conversation root, and the execution episode created on send is the durable authority. The reportedcount:0was the last retry strategy after the palette had closed;model: gpt-5.5is the imported rollout's own metadata (the configuredgpt-5.3-codexis the return-leg target). The driver's predicate now reads the committed target from the runtime pill's projection for conversation-bound sessions and keeps the row check for ordinary sessions. Two further harness defects surfaced once that passed: the acceptance input must be a session whose last turn contains a completed tool call (imported sessions load only the final turn's body on open, so the tool-result anchor cannot exist otherwise — the spec now preflights this before spending a live turn), and the shared driver clicked the last visible variant of a model group (claude-opus-5-high) while its expectation was clamped to the account'senabled_models(claude-opus-5), rejecting the model it had itself picked; it now prefers the exact requested row and always accepts the clicked model id. In every case the pill projection showed the pick had applied — the product behaved correctly. Local imported roots now show their continuation. The acceptance first proved the execution path with a valid Claude account (episode created, full history materialized into Claude's own catalog,claude --resume, exact reply in 22 s, durable turn intent completed, tail recovered) and then exposed that the root never showed it:ConversationStreamProvidercomposed the canonical timeline only from a Cloud family and gated the live runner overlay on anorg2-cloudregistry key, so for animported-history/local-sessionroot the reply lived only in the hidden execution child and the root kept the queue-synthesized user row aspending. Fixed in this PR: for a root without a Cloud target the provider resolves the root locator from the imported-history source (or the local session), loads the root's execution children through the same backend query the dispatcher uses (es_get_child_sessions, hidden children are not in the roster atom), subscribes to each child's chat events, and merges each child's new turn(s) into the root timeline (runlanded-*ids bound to the root). The boundary is the same semantic-prefix rule the dispatcher uses: the root's canonical events project to native items that must be a prefix of the child's projection, and the items beyond that prefix are the child's own turns; the child'screated_atis only a fallback when the prefix cannot be established. Children that were never opened in this app instance are hydrated from their authoritative CLI history into provider state (not through the event store, whoseloadFromCacheon tap mount would race and wipe them); the live tap still wins whenever the store has events, so the active runner streams as before. The queue-synthesized pending row is suppressed once the same user turn has landed, and the footer/Stop runner binding follows deliveries matched on the local root key. The E2E inspector now exposes the provider-published stream asstreamEvents, and the acceptance asserts on it, so the check reflects what the user sees. Acceptance result: green in both directions. With a valid Claude account and Codex quota,E2E_NATIVE_CONTINUATION_LIVE=1passes end to end (round-trips large Codex/Claude histories in both directions, 1m59s): Codex→Claude→Codex on the real 47-turn Codex session and Claude→Codex→Claude on the real Claude session. Four live turns completed as durable turn intents with their provider-native tails recovered, three fresh execution episodes were created and one compatible episode reused, and every reply appeared on the imported root's timeline where the harness asserts it. Earlier failing runs of the same acceptance were all environment, not code: an expired auto-detected Claude token (refreshed through the CLI, then re-imported with the app's own detect→save hooks),gpt-5.3-codexbeing rejected for ChatGPT-login Codex accounts, and that account's usage limit (also hit by the pre-sampling compact thatgpt-5.3-codex-sparkneeds for a 512-line thread). Picking spark at all exposed a picker bug (distinct Codex models folded into the effort-variant family and therefore unselectable); that fix is general model-picker code and ships as #1266.Robustness gap found while testing (not fixed here — needs a design call): if the execution episode behind an
accepteddelivery row disappears (the child session is deleted),recoverLocalConversationTurnreturnsnullandconversationTurnRunnertreats that as recovery-pending forever, so the conversation root stays wedged with a retry every minute and every later message queues behind it. A confirmed-missing runner is a permanent state and should turn the row into a failed, retryable delivery. Related pre-existing note: the durable queue store (chat-message-queue.json) lives in the app-support directory, not underORGII_HOME, so E2E runs share it with the developer's own instance.The dual-instance suite's C3 (Team Chat pending/failed/retry) reaches the retry step; retry correctness is proven in-process end to end (
SessionCommentsRetryProjection.test.ts, new coverage for a seam that had none).Verification
All five gates run on the final tree of this PR (stacked on #844, both on
developc1d0e2976):pnpm typecheckpnpm lint(oxlint + eslint--max-warnings 0)vitest --changed <#844 head>cargo clippy --workspace --all-targets -- -D warningscargo test --workspaceWorktreeLockGuardrelease flake fixed in #844)Also run against a fresh build: the rendered dual-instance suite (A/B pass; remaining failures triaged to harness staleness on
develop, one pre-existing product gap, and shared-session cascades — none attributable to this stack) and the stack's own new specs (work-item-features,routine-wizard,session-account-switch), whose failures were root-caused to a spec readingE2E_ORGII_HOMEinstead of the publishedORGII_HOME, a 1680px viewport collapsing the detail pane below its 300px placeholder threshold, and the two Routine regressions fixed in #844.