fix(codex): publish the turn tail for a subagent item the roster claims

Main's #19055 added a `subAgentActivity` arm to the provider activity table,
which is reached only through `publishActivity`. The roster's admission returned
above that call, so every `subAgentActivity` item bypassed it and a fan-out that
reports nothing else left the turn tail stuck on the previous frame's text.

`publishActivity` already no-ops on a refused admission and on a non-primary
thread, so routing the roster's admission through it is safe.

Also corrects a docstring the frames extraction copy-pasted onto
`settleOversizedNotification`.
This commit is contained in:
Merge Sim
2026-09-07 00:26:46 -07:00
parent 4f4a7d04c4
commit 319cedbcb0
3 changed files with 36 additions and 4 deletions
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key'
import type { AgentSessionTurnActivity } from '../../shared/agent-session-wire'
import type {
AgentJournalItemBody,
AgentJournalItemIdentity
@@ -17,11 +18,13 @@ type Row = { key: string; body: AgentJournalItemBody }
function harness() {
const rows: Row[] = []
const activities: (AgentSessionTurnActivity | null)[] = []
const sink: StructuredAgentSessionEventSink = {
appendItem: (identity: AgentJournalItemIdentity, body) =>
rows.push({ key: agentJournalItemKey(identity), body }),
appendTombstone: () => {},
publish: () => {}
publish: () => {},
setActivity: (activity) => activities.push(activity)
}
const translator = createCodexJournalTranslator({
sink,
@@ -31,7 +34,7 @@ function harness() {
return () => {}
}
})
return { translator, rows }
return { translator, rows, activities }
}
function notification(method: string, params: unknown): CodexStructuredSessionEvent {
@@ -88,6 +91,22 @@ describe('codex journal translation — subagents', () => {
expect(rows.filter((row) => row.key.startsWith('orca:codex-subagents'))).toHaveLength(1)
})
// The roster claims the item, but claiming it must not take the turn tail with
// it: the activity table is reached only through the publish arm, so a bare
// return leaves the tail stuck on whatever the previous frame said.
it('still publishes the turn tail for an item the roster claims', () => {
const { translator, activities } = harness()
translator.handle(notification('turn/started', { turn: { id: TURN_ID } }))
activities.length = 0
deliverActivity(translator, subagentItem('started', 'child-1', '/root/read'))
expect(activities.at(-1)).toEqual({
turnId: TURN_ID,
text: 'Coordinating with another agent'
})
})
it('consumes thread/tokenUsage/updated instead of swallowing it as chrome', () => {
const { translator, rows } = harness()
@@ -191,7 +191,9 @@ export function createCodexJournalTranslator(
})
: null
if (subagentAdmission) {
return subagentAdmission
// Not a bare return: the roster claiming the item must not skip the
// turn-tail arm, which is the only publisher of its activity copy.
return publishActivity(event, subagentAdmission)
}
const translated = items.handle(event)
return publishActivity(
@@ -224,7 +226,8 @@ export function createCodexJournalTranslator(
}
}
/** Routes a `subAgentActivity` item to the roster; null when it is not one. */
/** Settles the item a notification the transport refused to carry left
* mid-flight; null when the frame is not one. */
function settleOversizedNotification(event: {
sessionId: string
threadId: string
@@ -28,6 +28,16 @@ describe('provider frame activity', () => {
expect(codexProviderFrameActivity('item/reasoning/summaryPartAdded', {})).toBeNull()
})
it('names a fan-out from either Codex item type that reports one', () => {
for (const type of ['collabAgentToolCall', 'subAgentActivity']) {
expect(
codexProviderFrameActivity('item/started', {
item: { type, kind: 'started', agentThreadId: 'child-1', agentPath: '/root/read' }
})
).toBe('Coordinating with another agent')
}
})
it('uses Claude descriptions and safe semantic status without exposing tool labels', () => {
expect(
claudeProviderFrameActivity('message:system:task_started', {