feat(native-chat): Codex sessions write their subagents into the host status store - #22553
brennanb2025 wants to merge 14 commits into
Conversation
There was a problem hiding this comment.
Important
Two correctness gaps in the new producer's run fencing and lifecycle, both latent until the next PR starts reading the records.
Reviewed changes
This run reviewed the full 18-file diff at 1df7e358c0 (base brennanb2025/c3-claude-producer).
- New Codex child-work producer (
codex-child-work-evidence.ts,codex-child-work-translation.ts) — re-derives one child's whole observation per frame: open calls, last message, tokens, waiting, and the child's ownturn/completedas the terminal edge. Decodes, shared reducer decides. - Tracker/command wiring (
codex-background-task-tracker.ts,codex-background-command-tracker.ts) —observenow returns astarted/endedcommand decision, addsthreadTasks, and drains/publishes evidence via a session-bound sink. - Delivery ordering (adapter
emit,handleCodexSessionExit, runtime) — evidence is delivered after the journal row and the legacy republish on every notification, andsession-endedon every close path. - Contract change — new
turn_idrun-alias kind; a task runs undertool_use_id, a thread underturn_id; owner lookup still accepts only spawn calls. I traced this against the existing task path and the store's alias-key scoping and found it behavior-preserving there. - Tests — ran the three new files locally: 22/22 pass.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| ...(lastMessage ? { lastMessage } : {}), | ||
| ...(totalTokens !== undefined ? { totalTokens } : {}) | ||
| })) | ||
| return |
There was a problem hiding this comment.
When the child's turn ends before its announcement, this branch emits only an ended edge and returns before requeueOwnedBy. The reducer can settle but never create from ended, so a child that started and finished before its subAgentActivity announcement gets no record at all, and any persistent command or nested child it launched stays ownerless.
Technical details
# A child that ends before its announcement is dropped
## Affected sites
- `src/main/codex/codex-child-work-evidence.ts:194-209` — ended branch emits `ended` and returns
- `:233-235` — `requeueOwnedBy` is reached only from the working branch
- `src/shared/agent-status-child-work-reconciliation.ts:49-51` — `applyEnded` returns when no record exists
## Required outcome
A child the producer has seen run a turn gets a record even if the turn ended before the announcement, and work it launched is re-announced with its owner.
## Suggested approach
On the first observation (`facts.published === undefined`) in the ended branch, emit a live edge (to admit the record) before the ended edge, and/or call `requeueOwnedBy` before returning.
## Open questions for the human
Is dropping such a child acceptable as matching today's strip, which never showed it? The PR body's "no record until the announcement lands" claim implies it should still land.There was a problem hiding this comment.
ℹ️ No new issues in the delta — one lifecycle observation inline; the earlier run-fencing concern is fixed.
Reviewed changes
This run reviewed the two commits since the prior pullfrog review (1df7e358c0 → f098d8db9e) against the full 18-file diff (base brennanb2025/c3-claude-producer).
- Run fence fixed for turn-less child item frames —
recordnow falls back to the child's tracked turn andofTurnis strict, so an open call or last message recorded under one run can no longer leak into the next. - A child turn now ends without
turn/completed— a newturn-endedframe kind ends the child's turn on a non-retryerror(→failed) or athread/closed(→unverifiable) through a newCodexSubagentExecutions.endTurn, so the strip and the record settle on the same frame; asystemErrorstatus deliberately does not end a turn. - Tests — new evidence tests for each ending (error naming a turn / naming none / thread close), first-ending-wins, retried-error and
systemErrorkeeping the turn running, and a turn-less message never leaking into the next run; plus aLINTERchild in the parity script that ends on a fatal error.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| return event.method === 'thread/closed' | ||
| ? { kind: 'turn-ended', threadId: event.threadId, turnId: null, state: 'unverifiable' } | ||
| : null |
There was a problem hiding this comment.
thread/closed may not be reachable while Orca holds the session. Codex emits it only from its "unload a thread with no subscribers" path, and Orca's connection is auto-subscribed to every thread it creates, including children — so a child's close may never notify us, leaving this arm defensive-only. Worth confirming with a live capture; if it cannot fire, a child whose only ending is a close still reads working until session end.
Technical details
# Is a child's `thread/closed` reachable through Orca's connection?
## Affected sites
- `src/main/codex/codex-background-task-frames.ts:58-60` — the `thread/closed` → `turn-ended` (`unverifiable`) arm
- `src/main/codex/codex-subagent-executions.ts:93-103` — `endTurn` with a null `turnId` is reached only from this arm
## Evidence
- Codex emits `ThreadClosedNotification` from `unload_thread_without_subscribers`, which fires only when the thread's subscriber set is empty (codex-rs `app-server/src/request_processors/thread_lifecycle.rs`).
- Orca's connection is attached as a listener to every created thread (codex-rs `app-server/src/lib.rs` → `try_attach_thread_listener`), so a child's subscriber set stays non-empty while the session is held.
- Attribution itself is sound: `ThreadClosedNotification` carries a required `threadId` and is broadcast, and `readCodexThreadId(params)` resolves it.
## Open questions for the human
Can a child's `thread/closed` reach Orca's connection in any supported scenario (a second client unloading the thread, a session-end unsubscribe race)? If not, the arm is dead and the `unverifiable` ending it produces never fires.fc7fd5d to
6818421
Compare
f098d8d to
c99160f
Compare
c99160f to
8e22ed0
Compare
d5ffa7f to
eada18c
Compare
8e22ed0 to
08ca92b
Compare
There was a problem hiding this comment.
ℹ️ No new issues in the delta — the only net-new commit is a test-only parity assertion that passes; nothing to change.
Reviewed changes
Compared 08ca92b26c against the prior run's head (8e22ed0ad2). The branch was rebased onto a newer base, so 11 of the 12 PR commits are SHA churn; the only commit with no counterpart at the prior head is 08ca92b26c. All production files are unchanged.
- Journal↔record run numbering pinned —
codex-structured-child-work-producer.test.tsnow stamps each childagentMessagejournal row and asserts the journal's runattemptequals the record'sinvocation.generationacross the child's first three runs, including the case where Codex reports the child's first turn before the spawn that announces it. The assertion is exact, so it fails if either numbering diverges (e.g. an unstamped run-2 row readsattempt: 1againstgeneration: 2); the file's 4 tests pass at this head.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
eada18c to
503eb83
Compare
08ca92b to
358d1a9
Compare
503eb83 to
fa71175
Compare
1db34d1 to
36242df
Compare
…ods as a set Behavior-neutral: the same two methods stamp receipt time. Keeps the file under the size limit once the child-work sink lands.
… status store A Codex child thread and each persistent command become host child records, fed through the same delivery, ingest and reducer the Claude lane uses. The child's own turn decides it: turn start is live, turn completion settles it with the outcome Codex reports, and a follow-up turn reopens the same record as a new run. Its open tool call, last message, usage and waiting-on-user flag come from its own thread's frames. A parent turn ending settles nothing.
A completion frame need not restate the tool it ran, so reading the tool name before closing left the call open and the record naming a finished tool.
… the host's records Child turn start/end/follow-up, open tool call, last message, usage, waiting, the persistent command a child owns and its monitoring display, a primary turn end settling nothing, and session end. End to end through the real adapter: evidence after the journal and the legacy republish, and the parent state the records imply equals today's at every frame of a scripted session. Through the production runtime: a Codex session's child work reaches the status sink under its own address, and a provider exit ends it there.
…ex does, before counting edges
…e the runtime test's fake opener
…, or on its thread closing Codex can end a child's turn with no turn/completed: an error it will not retry is that turn's own end (the verdict the transcript already settles the same turn on), and a closed thread ran its last turn. The executions, the one owner of child turn state, now end the turn on both, so the strip drops the child and its record settles (failed, or unknown for a close) together, instead of reading working for the life of the session. A systemError status is not an ending: Codex raises it for errors that leave the turn running. A child fact whose frame names no turn now belongs to the turn the child is running, instead of counting for every run.
…ad close settles strip and record together
…ugh the shared fold's waiting arm
…s generation The journal numbers a Codex child's runs by the turns it observed on the child's thread; the host record numbers them by the runs its evidence opened. Both are keyed by the child's own turn id, so they must agree run for run, including when Codex reports the child's first turn before the spawn that announces it.
…d keeps the ended ones The host no longer erases a session's children when its provider goes away: a child still running settles with an outcome nobody reported, and a child that had already ended keeps what it said. The producer tests now expect exactly that, from the close path and from an unexpected exit.
36242df to
bc928a2
Compare
There was a problem hiding this comment.
ℹ️ No code issues in the delta — one stale note in the PR description.
Reviewed changes
This run reviewed the net-new commit bc928a2949 since the prior pullfrog review; the branch was also rebased onto a newer c3-claude-producer (94e5159f52), whose session-ended handling now settles live children instead of erasing them. No production logic changed.
- Session-end assertions updated to settlement —
codex-child-work-evidence.test.tsandcodex-structured-child-work-producer.test.tsnow assert a live child and its persistent command settleunknownwhen the session ends, and that already-ended children keep their recorded outcome.clear()'s doc comment was updated to match. All 28 tests in the three Codex child-work files pass at this head. - Runtime test comment —
structured-codex-child-work-runtime.test.tsnow describes a provider exit as reporting the session's end rather than taking its children with it.
ℹ️ The PR description still says a session's end removes the records
The body's frame table ends with "provider exit / close → session-ended → the session's records are removed", and the Decisions and deviations list repeats "Session end removes records instead of settling them". The base branch inverted that rule (35f97191b5, settleLive in agent-status-child-work-reconciliation.ts:93-99 now settles every live owned record unknown), and this delta's tests assert the new behavior. The description should be updated so the next reviewer and the #22614 cutover are not working from the old contract.
Technical details
# Stale session-end decision in the PR description
## Affected sites
- PR description, frame table last row: "provider exit / close | tracker cleared | `session-ended` | the session's records are removed"
- PR description, "Decisions and deviations": "**Session end removes records instead of settling them** (the same rule #22536 applies)."
- Actual behavior: `src/shared/agent-status-child-work-reconciliation.ts:93-99` — `settleLive` settles every live owned record `unknown` on `session-ended`; settled records are trimmed at `STRUCTURED_CHILD_WORK_MAX_SETTLED = 32`.
## Required outcome
The description matches the merged base behavior: session end settles a still-live child (and its owned commands) as `unknown`, and keeps an already-ended child's outcome, rather than erasing the session's records.
## Suggested approach
Update the two description spots to describe settlement. No code change is needed; this delta already pins the behavior.DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

ELI5
When a Codex chat starts a subagent (in Codex's multi-agent v2 mode; see "Mode coverage" below), or a subagent leaves a dev server running, Orca now keeps one record for it on the machine that runs the chat. The record says whether the subagent is still working, which tool it is running right now (for example
Bash: npm test), what it said last, how many tokens it has used, whether it is waiting for your approval, how it ended (finished, failed, stopped, or ended with no reason given), and when. If you send a finished subagent a follow-up, the same record opens again as a second run. Nothing on screen reads these records yet. #22565 adds one row component for CLI and structured children, and #22614 switches the sidebar and the chat strip over to these records, so this PR changes nothing you can see.Stacked on #22536 (the Claude producer, head
fc7fd5d8ab), which is stacked on #22521 (the child-record contract). The base branch isbrennanb2025/c3-claude-producer.Merge order
codex-subagent-executions.tsconflict resolution, keeping both APIs, and (2) the ordinal↔generation parity test (see "Overlap with fix(native-chat): record which Codex agent produced each journal row #22532").What Changed
This PR adds a second producer for the records #22536 introduced. It reuses #22536's delivery path, ingest and reducer, including the reducer's run verdict for follow-up runs. It adds no new hop, no new parent-held check and no run counter of its own. The one shared change is which alias kind names a run (see "Contract change").
The path from a Codex frame to a record, hop by hop
emit(codex-structured-session-adapter.ts). The journal translator writes the frame's rows. Then the background-task tracker observes the frame (CodexBackgroundTaskTracker.observe), and the legacy strip state is republished. Only after both,backgroundTasks.publishChildWork()drains the evidence that frame produced.handleCodexSessionExit,codex-structured-session-close.ts). Every way a Codex app-server stops (unexpected exit, requested close, forced close, a superseded acquisition) ends here. The tracker is cleared, which queuessession-ended. The legacynullis published, and then the evidence is drained. The session object carries its own sink, so none of those close paths needs extra plumbing.codexChildWorkSink, bound at acquire):deps.onChildWorkEvidence(sessionId, evidence).structured-agent-session-runtime.ts). The Codex adapter'sonChildWorkEvidencecallshost.publishChildWorkEvidence. This is the same host method the Claude adapter calls.codex) → status ownership (only under the address the parent row landed under) → status sink (orcad-entry.ts/main-process-runtime-service.ts) →ingestStructuredChildWork(refuses a parent the store does not hold) →reconcileAgentChildWorkEvidence.What the producer writes, and from which Codex frame
Mode coverage. Codex has two multi-agent modes, and the model picks one: GPT-5.5 runs the default mode, where a helper is announced only by the
collabAgentToolCallthat spawned it; GPT-5.6-Terra runs multi-agent v2, which emitssubAgentActivityitems. This PR registers children fromsubAgentActivityonly, so in the default mode it records nothing — a live default-mode session is unchanged by this PR alone. #22619, stacked on this PR, registers default-mode helpers from the spawn call into the same executions, after which both modes produce the same records. Everything below the frame table's first row is mode-independent: once a child is registered, its turns, items, status and endings flow the same way.subAgentActivitystarted(on the spawning agent's thread)live, once the child also has a turn runningthread_idalias = the child thread; fence{child turn id, 1}with aturn_idalias;description= label; residencybackground; not stoppableturn/startedlivewithrunId= the turn idchildWorkId, generation + 1, and the previous run's outcome kept inpreviousInvocationsturn/completedturn.statusendedwithrunId= that turn:completed→succeeded,interrupted→cancelled,failed→failed, anything else→unknown; plus the last message and usageerrorthat Codex will not retry (with noturn/completed)failedended, outcomefailedfailed. The strip drops the child on the same frame. The transcript already settles the same turn on this verdictthread/closedended, outcomeunknownunknown(Codex closes a thread only after shutting it down idle, and states no verdict)item/started/item/completedfor a tool calllivewithoperation {toolName, input, basis: 'open'}, oroperation: nullBash: npm test(commandExecution),apply_patch: <path>,mcp__<server>__<tool>,web_search: <query>. If a newer call closes first, the older one shows againitem/completedagentMessagelivewithlastMessagethread/tokenUsage/updatedlivewithtotalTokens(a running total, replaced)thread/status/changedactivewithwaitingOnApproval/waitingOnUserInputlivewith statewaiting; back toworkingwhen the flags clear; reset when the turn endscommandExecutionwith sourceunifiedExecStartup(a persistent command)item/started, on any threadlivekindcommand.task_id= today's strip id (codex-command:…). The owner is the child thread that launched it, or nobody for the session's own agentparentChildWorkId= that childitem/completed(its process exited)ended:completedwith exit 0 or no exit code→succeeded, non-zero exit orfailed→failed,declined→cancelled, otherwiseunknownturn/completedsession-endedA child's evidence is re-derived from state after each frame about it. The executions (which already own a child's identity and turn state) supply identity and turn state. The producer supplies the open call, last message, usage and waiting. An unchanged re-derivation sends nothing, so the announcement Codex delivers twice produces one edge. A child whose turn began before its announcement gets no record until the announcement lands, matching today's strip. When a child is first recorded, work it launched earlier (a persistent command, a nested child) is sent again with its owner named, so a launch that raced the announcement is not left ownerless.
Contract change (host-internal)
A run alias is now named in the provider's own terms. A task runs under its spawn call (
tool_use_id, as before) and a thread runs under its turn (new alias kindturn_id). The reducer's run verdict, previous-run fence and resume are unchanged. They now find the current run's alias under either kind. Owner lookup still accepts only a spawn call, so a turn id can never pass for one. Alias records are host memory only (never persisted or transmitted), so this needs no wire negotiation.Supporting changes
CodexBackgroundCommandTracker.observenow returns the decision it made (started, or ended with the item), andthreadTasks(threadId)lists a thread's live commands. The producer follows the tracker's own admission, so the record and today's strip row cannot disagree about which commands exist.CodexSubagentExecutionsgains:endTurn(), which ends a child's turn on a frame other thanturn/completed. The executions are the one owner of child turn state, so today's strip and the records settle on the same frame. The first ending a turn gets stands.find();spawnerThreadId. The field and its rule are identical to fix(native-chat): record which Codex agent produced each journal row #22532's.readCodexBackgroundTaskFrameclassifies a child's non-retryerrorusing the same verdict reader the transcript uses (readCodexProviderVerdict), and a child'sthread/closed.codexThreadWaitsOnUsersits beside the existing thread-status reader.Why
A Codex child today is one line in the chat strip that says only "a working child exists". It vanishes the moment its turn ends, even though the host already knows how it ended, what it said, its usage, and which tool it is running. This PR writes those facts into the one host record per child that #22536 created for Claude, so #22614 can make the sidebar and the strip read the same record for both providers.
The producer decodes, and the shared reducer decides. Codex has a terminal frame per child (
turn/completed), so this lane settles on that and on session end. It needs no roster inventory and no turn-end sweep.Alternatives considered:
backgroundresidency, which the reducer's turn sweep never touches. A test pins it.thread/status/changedactive flags, and that frame is the provider's own statement about the child.Deviation, recorded: a Codex child whose turn completed can be sent a follow-up. One could model the completed turn as an idle, resumable child. This PR records it as finished (outcome
succeeded) and reopens the same record as a new run when a follow-up turn starts. Otherwise a Codex child could never show "finished".Linked Issue
None — part of the structured chat status/orchestration program.
Visual Proof
N/A. Nothing reads the records yet. The sidebar and the chat strip keep their current sources until #22614. A parity test (below) asserts that the parent state the records imply equals today's at every frame.
Testing
All tests were run with
env -u ORCA_STRUCTURED_SESSION ORCA_BACKGROUND_LAUNCH=1, at the head rebased onto #22536'sfc7fd5d8ab.pnpm tc:node,pnpm tc:webandpnpm tc:cliall exit 0.codex-child-work-evidence.test.ts(23). The real tracker feeds evidence through the shared reducer into a real store. Covered:error(naming its turn, or none) or bythread/closedsettles the strip and the record on the same frame (3);errorand asystemErrorstatus end nothing, and a fatalerrornaming an already-finished turn ends nothing;monitoringthrough the shared fold, thendone;codex-structured-child-work-producer.test.ts(3), through the realCodexStructuredSessionAdapter:Ordering. For the announcing frame, the journal rows and the legacy republish precede the evidence.
Parity. A 22-frame session covers:
errorand noturn/completed;At every frame, the parent state folded from the records equals the one folded from today's
backgroundTasks, and so does the raw liveness (asserted at every frame, not only once the main agent is done). Liveness takes all three values across the script.closeSessionremoves every record.Limit of this proof: both sides read the same
CodexSubagentExecutions. The strip'stasks()isexecutions.workingChildren(), and the producer reads the same object. So parity proves the record pipeline (evidence → reducer → store) matches today's strip. It cannot see a defect in the executions' own set of ending observers, because such a defect strands both sides identically. The missing ending signals fixed here were exactly that kind of defect. feat(native-chat): the chat strip and the sidebar read the host's child records #22614 must not use this test alone as proof that the Codex cutover is invisible.Unexpected exit. An unexpected provider exit removes the records.
structured-codex-child-work-runtime.test.ts(1). Through the productionensureStructuredAgentSessionHost, a Codex session's child reaches the status sink under the session's own address with providercodex, and a provider exit deliverssession-endedthere.HEAD:turn_id(substituted withtool_use_id)runId)interrupted → cancelledmappingsession-endedon clearerrorends the child turnthread/closedends the child turnRegression.
src/main/codex,src/shared/agent-status*, the structured runtime tests,src/main/native-chat/agent-session-wire, and the relay store-context test. 356 files, 3,431 tests: 3,414 passed, 1 expected-fail, 13 skipped, 3 failed. All 3 are timing tests:refusal-retryhost oracle andlease-renewerproduction interval pass when run alone.codex-session-index-heal-state("main thread free while walking a large audit ledger") fails identically with HEAD detached at the basefc7fd5d8ab, with the machine at load average 25–36. It imports only filesystem and ledger modules.src/main/claude,src/main/agent-hooksandsrc/main/orcad(none of which the fix touches): 607 files, 5,952 tests: 5,916 passed, 6 failed.provider close unproven, timeouts).Quality.
ORCA_CODE_QUALITY_BASE=<base>), reports 0 new findings in every category.oxlinton all 18 changed files exits 0. A 310-line in-tree probe proves it reportsmax-lines.pnpm run audit:anti-slopexits 0.pnpm-lock.yamlis absent from the branch range, and there are no new docs.Platforms: tests ran on macOS. The code is platform-neutral.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
Review
Not verified:
No live Codex app-server capture was made for this PR. Frame shapes come from:
agentThreadId/agentPath);ThreadStatus::Active { activeFlags: waitingOnApproval | waitingOnUserInput }, and thecommandExecution/mcpToolCall/fileChangeitem fields).Not captured: a child thread's
thread/status/changed,errorandthread/closedarriving on the parent's connection. The child'sturn/*anditem/*frames do arrive there, which the recorded session shows. If the status frame did not arrive, a waiting child would readworking, as it does today.No Electron run. Nothing renders the records yet, and feat(native-chat): the chat strip and the sidebar read the host's child records #22614 carries the
$electroncheck.A
systemErrorstatus is deliberately not a turn end. This is verified in the Codex app-server source, and Orca's transcript already draws the same line (codex-structured-journal-provider-verdicts.ts):systemErrorfor every error event, including the two kinds that leave the turn running: a refused steer and a failed rollback.errornotification with the turn's id, which this PR now consumes.So
systemErrorwould add only false endings: a child still working would be recorded asfailed. Residual: a child that stops with none ofturn/completed, a fatalerror, orthread/closedstays live until the session ends. I found no Codex path that does that. feat(native-chat): register Codex default-mode helpers as subagents #22619 subsequently found one: a helper shut down by the parent'scloseAgentcall may emit no child-side ending in either mode (schema-verified; whetherthread/closedfollows a close is live-unverified). feat(native-chat): register Codex default-mode helpers as subagents #22619 ends the helper's running turn on a finishedcloseAgent(outcomecancelled; the first ending stands). At this PR's head the residual stands as written.The transcript's subagent group card is written by the journal roster, which re-serializes its row only on
turn/started/turn/completed. The fatal-error/thread/closedendings this PR adds reach the executions through the tracker, so at this head the strip and the record settle while the transcript row stays "working" — a disagreement main does not have (on main, none of the three settle). feat(native-chat): register Codex default-mode helpers as subagents #22619, stacked on this PR, closes it by making the roster followCodexSubagentExecutions.onExecutionChanged, the one owner of child turn state, so every ending settles all three together. (An earlier revision of this section deferred the fix to the fix(native-chat): record which Codex agent produced each journal row #22532 lane because fix(native-chat): record which Codex agent produced each journal row #22532 edits the same roster/translator files; the fix was routed to feat(native-chat): register Codex default-mode helpers as subagents #22619 instead.)Late usage is dropped. Usage that arrives after a child's definite ending is not written, because the settled outcome latches.
Nested children need a non-default Codex setting and were not captured live. Only the owner rule is tested.
Not run: mobile tests (no mobile code reads records) and the relay typecheck (no relay module imports these files).
Per-frame cost is one store mutation per child fact change. I did not measure it; feat(native-chat): the chat strip and the sidebar read the host's child records #22614 must measure the publish rate before it publishes records.
Decisions and deviations:
succeeded, and a follow-up turn reopens the same record as a new run. The alternative model, an idle resumable child, would never show "finished".unverifiable, never a record write.Bash,apply_patch,mcp__server__tool), so a structured Codex child's row matches a Codex CLI row.web_searchuses the transcript's name, because Codex hooks do not name it. Messages, reasoning, plans, collaboration calls and image views are not operations.description, notname, because that is where today's strip row carries it.providerIdis its thread id, not today'scodex-agent:wire id (see Notes).A merge of the two has exactly one content conflict, in
codex-subagent-executions.ts. The resolution must keep bothfind()/endTurn()(the records' reads and endings) and fix(native-chat): record which Codex agent produced each journal row #22532'sspawnerOf()/turnOrdinal()(the transcript's reads).spawnerThreadIdis the same field with the same rule on both sides.The two child-turn counters differ by construction:
They agree on the normal path and diverge on replayed or fenced turns. fix(native-chat): record which Codex agent produced each journal row #22532 merges first, so this PR owes a parity test between them when it is rebased over fix(native-chat): record which Codex agent produced each journal row #22532.
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
turn_idalias kind is host-internal.providerIdis its thread id, the join key the journal linkage uses. Today's strip publishescodex-agent:<thread>. When the legacy shapes are derived from views, that prefix must be mapped (or the new id accepted, since the id is only a row key). Command records keep today's strip id, so theirproviderIdis unchanged. The label is indescription, where today's strip row carries it.subagentsis unchanged.server-ingest-structured.ts,structured-agent-session-agent-status.ts, the fold and the renderer bridge are not touched.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)