feat(x): ephemeral CodeRunFeed for codex streaming + settle-time durable batch

The runs->turns migration broke codex live streaming in copilot chat: the
turns bridge's publish shim forwarded only tool-output-stream and silently
dropped code-run-event / code-run-permission-request (the latter would
deadlock a turn under policy 'ask'). An interim fix persisted every stream
event as durable tool_progress, but that wrote each text chunk to the turn
file — unsustainable.

Final architecture — live and durable paths split:

- Live (ephemeral, bypasses the turn runtime): code_agent_run broadcasts
  each ACP event on a new CodeRunFeed (core DI singleton), forwarded by
  main over a dedicated codeRun:events channel, buffered per toolCallId in
  a module-level renderer store and rendered by CodingRunBlock. The buffer
  survives session switches; nothing is persisted.

- Durable (one line per run): when the run settles (success, error, or
  cancel), code_agent_run publishes a single code-run-events-batch with
  the whole ordered timeline, consecutive same-role message chunks
  coalesced (display-lossless — the timeline concatenates them anyway).
  The turns bridge maps it to tool_progress {kind:'code-run-events'};
  turn-view derives the replay timeline from it, so reloads keep history.

- Permissions stay durable per-ask (request + resolved marker): the
  renderer overlay resets on session switch, so an ephemeral-only ask
  would strand a blocked turn with no card to answer. Pending = requests
  minus resolutions (handles concurrent asks), cleared on tool result.

The legacy code-section path (runs bus per-event) is untouched; its
per-event ctx.publish remains and is a no-op under the turns shim.

Also: repaired the two code_agent_run tests broken by the earlier cwd
existence check (they used a nonexistent /repo), and added coverage for
feed broadcast, batch coalescing, partial-batch-on-failure, bridge
durability routing, and pending-permission derivation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-06 14:04:33 +05:30
parent 7c06af04a7
commit 3bbed55735
15 changed files with 506 additions and 10 deletions

View file

@ -73,3 +73,11 @@ export const RunPromptResult = z.object({
sessionId: z.string(),
});
export type RunPromptResult = z.infer<typeof RunPromptResult>;
// One item on the ephemeral CodeRunFeed (`codeRun:events` broadcast): a live
// code-run event tagged with the tool call it belongs to. Fire-and-forget —
// the durable record is the code-run-events-batch written when the run settles.
export type CodeRunFeedEvent = {
toolCallId: string;
event: CodeRunEvent;
};

View file

@ -21,7 +21,7 @@ import { ZListToolkitsResponse } from './composio.js';
import { BrowserStateSchema, HttpAuthRequestSchema } from './browser-control.js';
import { BillingInfoSchema } from './billing.js';
import { EmailBlockSchema, GmailThreadSchema } from './blocks.js';
import { PermissionDecision, ApprovalPolicy, CodingAgent } from './code-mode.js';
import { PermissionDecision, ApprovalPolicy, CodingAgent, type CodeRunFeedEvent } from './code-mode.js';
import { NotificationSettingsSchema } from './notification-settings.js';
import { CodeProject, CodeSession, CodeSessionMode, CodeSessionStatus, GitRepoInfo, GitStatusFile, CodeAgentModelOptions } from './code-sessions.js';
import { ChannelsConfig, ChannelsStatus } from './channels.js';
@ -450,6 +450,14 @@ const ipcSchemas = {
req: z.null(),
res: z.null(),
},
// Ephemeral code-run stream (CodeRunFeed): per-event broadcast of a
// code_agent_run's live ACP activity, keyed by toolCallId. Never persisted —
// the durable record is the code-run-events-batch tool progress written when
// the run settles. Typed via z.custom like the other broadcast feeds.
'codeRun:events': {
req: z.custom<CodeRunFeedEvent>(),
res: z.null(),
},
// ── New runtime: sessions + turns (session-design.md) ────────────────────
// Turn-mutating calls return quickly; the renderer follows progress through
// the sessions:events feed and the shared reduceTurn reducer.

View file

@ -130,6 +130,17 @@ export const CodeRunPermissionRequestEvent = BaseRunEvent.extend({
ask: PermissionAsk,
});
// The complete, ordered code-run timeline, published ONCE when the coding turn
// settles (consecutive agent message chunks coalesced — display-lossless, the
// timeline concatenates them anyway). This is the durable record; the live
// per-event stream travels over the ephemeral CodeRunFeed (`codeRun:events`)
// and is never persisted.
export const CodeRunEventsBatchEvent = BaseRunEvent.extend({
type: z.literal("code-run-events-batch"),
toolCallId: z.string(),
events: z.array(CodeRunEventSchema),
});
export const ToolPermissionAutoDecisionEvent = BaseRunEvent.extend({
type: z.literal("tool-permission-auto-decision"),
toolCallId: z.string(),
@ -165,6 +176,7 @@ export const RunEvent = z.union([
ToolPermissionResponseEvent,
CodeRunStreamEvent,
CodeRunPermissionRequestEvent,
CodeRunEventsBatchEvent,
ToolPermissionAutoDecisionEvent,
RunErrorEvent,
RunStoppedEvent,