@workerdeck/core 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { getSessionInfo, getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
4
- import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, imagePartRef, replayCoalesceKey, replayRetains, snapshotRetains, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
4
+ import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, contextReading, imagePartRef, replayCoalesceKey, replayRetains, snapshotRetains, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
5
5
  import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
6
6
  import { execFile, spawn } from "node:child_process";
7
- import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
7
+ import { appendFileSync, existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { createVfs, runScript } from "@workerdeck/sandbox";
9
9
  import { z } from "zod";
10
10
  import { lookup } from "node:dns/promises";
@@ -1100,6 +1100,13 @@ var SessionRunner = class {
1100
1100
  #events = [];
1101
1101
  #subscribers = new SubscriberSet();
1102
1102
  #seq = 0;
1103
+ /**
1104
+ * Latest context-window reading, retained from the last `context_usage` this
1105
+ * runner emitted so `GET /sessions` can answer it without an attach — see
1106
+ * `SessionInfo.contextUsage`. Folded in the emit path, so it is by
1107
+ * construction the same number the transcript last drew.
1108
+ */
1109
+ #contextUsage;
1103
1110
  #activityCount = 0;
1104
1111
  /**
1105
1112
  * Seq of the latest `conversation_reset` event, 0 when none. The log itself is
@@ -1194,6 +1201,7 @@ var SessionRunner = class {
1194
1201
  createdAt: this.createdAt,
1195
1202
  lastSeq: this.#seq,
1196
1203
  activityCount: this.#activityCount,
1204
+ contextUsage: this.#contextUsage,
1197
1205
  pendingPermissionCount: this.#pending.size,
1198
1206
  subagents: this.#subagents.list(),
1199
1207
  meta: this.#config.meta,
@@ -1768,7 +1776,11 @@ var SessionRunner = class {
1768
1776
  };
1769
1777
  this.#lastActivityAt = event.ts;
1770
1778
  this.#activityCount += transcriptActivity(body);
1771
- if (body.type === "conversation_reset") this.#resetSeq = event.seq;
1779
+ this.#contextUsage = contextReading(body) ?? this.#contextUsage;
1780
+ if (body.type === "conversation_reset") {
1781
+ this.#resetSeq = event.seq;
1782
+ this.#contextUsage = void 0;
1783
+ }
1772
1784
  this.#subagents.observe(body, event.ts);
1773
1785
  this.#events.push(event);
1774
1786
  this.#subscribers.emit(event);
@@ -1816,6 +1828,13 @@ var AiSdkRunner = class {
1816
1828
  #events = [];
1817
1829
  #subscribers = new SubscriberSet();
1818
1830
  #seq = 0;
1831
+ /**
1832
+ * Latest context-window reading, retained from the last `context_usage` this
1833
+ * runner emitted so `GET /sessions` can answer it without an attach — see
1834
+ * `SessionInfo.contextUsage`. Folded in the emit path, so it is by
1835
+ * construction the same number the transcript last drew.
1836
+ */
1837
+ #contextUsage;
1819
1838
  #activityCount = 0;
1820
1839
  #status = "starting";
1821
1840
  #permissionMode;
@@ -1865,7 +1884,12 @@ var AiSdkRunner = class {
1865
1884
  if (!state || !Array.isArray(state.messages)) throw new Error("session snapshot is missing its provider-engine state");
1866
1885
  this.#seq = snapshot.seq;
1867
1886
  this.#events = [...snapshot.events];
1868
- this.#activityCount = this.#events.reduce((total, event) => total + transcriptActivity(event), 0);
1887
+ this.#activityCount = 0;
1888
+ for (const event of this.#events) {
1889
+ this.#activityCount += transcriptActivity(event);
1890
+ if (event.type === "conversation_reset") this.#contextUsage = void 0;
1891
+ else this.#contextUsage = contextReading(event) ?? this.#contextUsage;
1892
+ }
1869
1893
  this.#messages = [...state.messages];
1870
1894
  for (const call of state.pendingToolCalls) this.#pendingToolCalls.set(call.toolCallId, call);
1871
1895
  this.#dispatched = new Set(state.dispatched);
@@ -1916,6 +1940,7 @@ var AiSdkRunner = class {
1916
1940
  createdAt: this.createdAt,
1917
1941
  lastSeq: this.#seq,
1918
1942
  activityCount: this.#activityCount,
1943
+ contextUsage: this.#contextUsage,
1919
1944
  pendingPermissionCount: 0,
1920
1945
  meta: this.#config.meta,
1921
1946
  scope: this.#config.scope,
@@ -2602,6 +2627,8 @@ var AiSdkRunner = class {
2602
2627
  };
2603
2628
  this.#lastActivityAt = event.ts;
2604
2629
  this.#activityCount += transcriptActivity(body);
2630
+ this.#contextUsage = contextReading(body) ?? this.#contextUsage;
2631
+ if (body.type === "conversation_reset") this.#contextUsage = void 0;
2605
2632
  this.#events.push(event);
2606
2633
  this.#subscribers.emit(event);
2607
2634
  }
@@ -3822,8 +3849,6 @@ const claudeAdapter = {
3822
3849
  }));
3823
3850
  }
3824
3851
  };
3825
- //#endregion
3826
- //#region src/engines/codex/jsonrpc.ts
3827
3852
  /**
3828
3853
  * A JSON-RPC error response from the peer, or one we return to it. `code`
3829
3854
  * follows the JSON-RPC 2.0 reserved ranges (-32601 = method not found).
@@ -3855,9 +3880,12 @@ var JsonRpcStdioConnection = class {
3855
3880
  #buffer = "";
3856
3881
  #closed = false;
3857
3882
  #notificationHandler;
3883
+ /** Where {@link CODEX_TRACE_ENV} pointed, or undefined — read once. */
3884
+ #trace;
3858
3885
  #requestHandler;
3859
3886
  constructor(options) {
3860
3887
  this.#output = options.output;
3888
+ this.#trace = process.env["WORKERDECK_CODEX_TRACE"] || void 0;
3861
3889
  options.input.on("data", (chunk) => this.#feed(String(chunk)));
3862
3890
  options.input.on("error", () => {});
3863
3891
  options.output.on("error", () => {});
@@ -3918,9 +3946,28 @@ var JsonRpcStdioConnection = class {
3918
3946
  } catch {
3919
3947
  continue;
3920
3948
  }
3949
+ this.#traceLine(message);
3921
3950
  this.#dispatch(message);
3922
3951
  }
3923
3952
  }
3953
+ /**
3954
+ * Append one inbound message to the trace file, when the operator asked for
3955
+ * one. **Notifications and server→client requests only** — a response body is
3956
+ * not needed to answer the questions this exists for, and `account/*` results
3957
+ * are the one place app-server traffic can carry a masked credential
3958
+ * fragment, which nothing of ours writes to disk (see the auth red lines).
3959
+ * Best-effort and synchronous: a debug sink that loses lines proves nothing,
3960
+ * and a debug sink that throws must not take the session with it.
3961
+ */
3962
+ #traceLine(message) {
3963
+ if (!this.#trace) return;
3964
+ const method = message.method;
3965
+ if (typeof method !== "string") return;
3966
+ if (method.startsWith("account/") || method.startsWith("login")) return;
3967
+ try {
3968
+ appendFileSync(this.#trace, JSON.stringify(message) + "\n");
3969
+ } catch {}
3970
+ }
3924
3971
  #dispatch(message) {
3925
3972
  const { id, method } = message;
3926
3973
  if (typeof method === "string") {
@@ -3959,6 +4006,117 @@ var JsonRpcStdioConnection = class {
3959
4006
  }
3960
4007
  };
3961
4008
  //#endregion
4009
+ //#region src/engines/codex/subagents.ts
4010
+ /**
4011
+ * The codex side of `SessionInfo.subagents` — and the attribution table that
4012
+ * gives every event a spawned agent produces its `parentToolUseId`.
4013
+ *
4014
+ * Codex's signal is stronger than the claude engine's, so this is deliberately
4015
+ * NOT that tracker generalised (`engines/claude/subagents.ts` infers spawns
4016
+ * from tool names and verdicts from result-text sniffing, ~290 lines of module
4017
+ * doc explaining the inference). Here nothing is inferred: `subAgentActivity
4018
+ * {kind: 'started'}` on the owning thread positively announces an agent, names
4019
+ * it (`agentPath`), keys it (`agentThreadId` — the id every one of its later
4020
+ * notifications carries) and hands over the model's own `spawn_agent` call id;
4021
+ * the agent's end is its own thread's `turn/completed`, status included. So a
4022
+ * record is keyed by **thread id** — the wire's handle — while exposing a
4023
+ * **tool-use id** — the protocol's: `parentToolUseId` on nested events must
4024
+ * equal the anchor `tool_use`'s id for `subagentItems` (the frame membership
4025
+ * rule every client shares) to reassemble the sidechain, and this map is where
4026
+ * the two vocabularies meet.
4027
+ *
4028
+ * Two decisions worth their prose:
4029
+ *
4030
+ * **A record survives the runner's turns.** Codex agents are designed to
4031
+ * outlive the root turn that spawned them (`sendInput`/`resumeAgent` address a
4032
+ * thread that kept existing), so — unlike a pending approval — nothing here is
4033
+ * swept when a root turn ends. What does end every agent is the app-server
4034
+ * process itself: the runner calls {@link sweep} when the child dies or the
4035
+ * session closes, because an agent whose host process is gone can never report,
4036
+ * and `running` on a closed session would be a lie a polled list re-renders
4037
+ * forever (the claude tracker's argument, inherited whole).
4038
+ *
4039
+ * **The settled tail is bounded, running records never are** — the same
4040
+ * {@link SUBAGENT_HISTORY} discipline as the claude tracker, and enforced at
4041
+ * settle time for the same reason: a settle happens once per agent, `list()`
4042
+ * once per row of a 1.2s-polled sessions list.
4043
+ */
4044
+ var CodexAgentTracker = class {
4045
+ #byThread = /* @__PURE__ */ new Map();
4046
+ #settleCounter = 0;
4047
+ /** The record whose thread this is — the attribution lookup. */
4048
+ get(agentThreadId) {
4049
+ return this.#byThread.get(agentThreadId);
4050
+ }
4051
+ /** Open (or return) the record for a thread. Fill-in, never overwrite: a
4052
+ * label-less fallback record keeps its accumulated count and its already
4053
+ * published toolUseId when the announcing item arrives late. */
4054
+ open(agentThreadId, toolUseId, agentType, ts) {
4055
+ let record = this.#byThread.get(agentThreadId);
4056
+ if (!record) {
4057
+ record = {
4058
+ agentThreadId,
4059
+ toolUseId,
4060
+ status: "running",
4061
+ startedAt: ts,
4062
+ toolCount: 0,
4063
+ counted: /* @__PURE__ */ new Set()
4064
+ };
4065
+ this.#byThread.set(agentThreadId, record);
4066
+ }
4067
+ record.agentType ??= agentType;
4068
+ return record;
4069
+ }
4070
+ /** The agent's thread ran again (`kind: 'interacted'`, or a fresh
4071
+ * `turn/started` on its thread): a settled verdict no longer describes it. */
4072
+ revive(record) {
4073
+ record.status = "running";
4074
+ record.settledOrder = void 0;
4075
+ }
4076
+ #settle(record, status) {
4077
+ record.status = status;
4078
+ record.settledOrder = ++this.#settleCounter;
4079
+ let settled = 0;
4080
+ for (const r of this.#byThread.values()) if (r.settledOrder !== void 0) settled++;
4081
+ while (settled > SUBAGENT_HISTORY) {
4082
+ let oldest;
4083
+ for (const r of this.#byThread.values()) {
4084
+ if (r.settledOrder === void 0) continue;
4085
+ if (!oldest || r.settledOrder < oldest.settledOrder) oldest = r;
4086
+ }
4087
+ if (!oldest) break;
4088
+ this.#byThread.delete(oldest.agentThreadId);
4089
+ settled--;
4090
+ }
4091
+ }
4092
+ /** A real verdict for one agent — its thread's `turn/completed`, or the
4093
+ * `interrupted` activity edge. */
4094
+ settle(record, status) {
4095
+ if (record.status === status) return;
4096
+ this.#settle(record, status);
4097
+ }
4098
+ /** The process the agents lived in is gone (child death, session close):
4099
+ * everything still running is settled as failed — the report can never come. */
4100
+ sweep() {
4101
+ for (const record of this.#byThread.values()) if (record.status === "running") this.#settle(record, "failed");
4102
+ }
4103
+ /** The rollup as `SessionInfo.subagents` serves it — spawn order, fresh
4104
+ * objects, and `undefined` when there is nothing to say (absent and empty
4105
+ * mean the same thing to a client, and bytes on a polled list are paid for). */
4106
+ list() {
4107
+ if (this.#byThread.size === 0) return void 0;
4108
+ const out = [];
4109
+ for (const r of this.#byThread.values()) out.push({
4110
+ toolUseId: r.toolUseId,
4111
+ agentType: r.agentType,
4112
+ status: r.status,
4113
+ startedAt: r.startedAt,
4114
+ toolCount: r.toolCount
4115
+ });
4116
+ return out;
4117
+ }
4118
+ };
4119
+ //#endregion
3962
4120
  //#region src/engines/codex/runner.ts
3963
4121
  /**
3964
4122
  * thread/start's sandbox axis (string form) — our permission modes as codex
@@ -3999,16 +4157,27 @@ const GRANULAR_ASK = { granular: {
3999
4157
  request_permissions: true,
4000
4158
  skill_approval: true
4001
4159
  } };
4160
+ const GRANULAR_NEVER = { granular: {
4161
+ sandbox_approval: false,
4162
+ rules: false,
4163
+ mcp_elicitations: false,
4164
+ request_permissions: false,
4165
+ skill_approval: false
4166
+ } };
4167
+ /**
4168
+ * Notifications whose meaning is scoped to ONE thread, and which are therefore
4169
+ * only ever read off the session's own. Everything else (items, deltas) is
4170
+ * accepted from any thread on the connection — see `#handleNotification`.
4171
+ */
4172
+ const THREAD_SCOPED_NOTIFICATIONS = new Set([
4173
+ "turn/started",
4174
+ "turn/completed",
4175
+ "thread/tokenUsage/updated"
4176
+ ]);
4002
4177
  const APPROVAL_POLICY_BY_MODE = {
4003
4178
  default: GRANULAR_ASK,
4004
4179
  acceptEdits: GRANULAR_ASK,
4005
- bypassPermissions: { granular: {
4006
- sandbox_approval: false,
4007
- rules: false,
4008
- mcp_elicitations: false,
4009
- request_permissions: false,
4010
- skill_approval: false
4011
- } }
4180
+ bypassPermissions: GRANULAR_NEVER
4012
4181
  };
4013
4182
  /** Fallback timeout for a pending approval nobody answers — the SessionRunner
4014
4183
  * default, so unattended codex sessions land the same way Claude ones do. */
@@ -4019,6 +4188,47 @@ const DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
4019
4188
  * host filesystem, an inline preview) off it.
4020
4189
  */
4021
4190
  const CODEX_IMAGE_TOOL = "CodexImageGeneration";
4191
+ /**
4192
+ * Tool name for a spawned agent's anchor `tool_use` — the claude engine's
4193
+ * `Task` in this engine's vocabulary. Codex never sends such a call: the model's
4194
+ * `spawn_agent` surfaces only as the `subAgentActivity` marker item, so the
4195
+ * runner authors the call itself, because everything downstream is built on a
4196
+ * top-level `tool_use` existing — `terminalBlocks` absorbs a sidechain into the
4197
+ * call whose id its events carry as `parentToolUseId`, the takeover frames by
4198
+ * it, and `taskIdentity` labels it from the input's `subagent_type`. Not a new
4199
+ * wire idea, just a row: the same shape every other codex tool card uses.
4200
+ */
4201
+ const CODEX_AGENT_TOOL = "CodexAgent";
4202
+ /** Tool name for the model's collab-agent calls (`wait`, `sendInput`, …), the
4203
+ * `tool` field carried in the input. One name for the whole open axis rather
4204
+ * than a name per verb, so a future verb renders instead of vanishing. */
4205
+ const CODEX_COLLAB_TOOL = "CodexCollab";
4206
+ /** An agent's name is its path's basename: '/root/date_one' → 'date_one'. */
4207
+ function agentName(agentPath) {
4208
+ if (typeof agentPath !== "string") return void 0;
4209
+ return agentPath.split("/").filter(Boolean).at(-1) || void 0;
4210
+ }
4211
+ /** The collab card's input: the verb always, the rich fields only when codex
4212
+ * actually filled them (measured against 0.146.0 they arrive empty — the card
4213
+ * must not render five null columns to say 'wait'). */
4214
+ function collabInput(item) {
4215
+ return {
4216
+ tool: item.tool,
4217
+ ...item.receiverThreadIds?.length ? { receiverThreadIds: item.receiverThreadIds } : {},
4218
+ ...item.prompt ? { prompt: item.prompt } : {},
4219
+ ...item.model ? { model: item.model } : {}
4220
+ };
4221
+ }
4222
+ /** A completed turn's answer, from its summary `items` page — the last
4223
+ * `agentMessage` text. For a sub-agent's thread this is the agent's report,
4224
+ * which is exactly what belongs in the anchor's `tool_result`. */
4225
+ function turnReport(turn) {
4226
+ const items = Array.isArray(turn.items) ? turn.items : [];
4227
+ for (let index = items.length - 1; index >= 0; index--) {
4228
+ const item = items[index];
4229
+ if (item?.type === "agentMessage" && typeof item.text === "string" && item.text) return item.text;
4230
+ }
4231
+ }
4022
4232
  /** Longest `result` worth putting in a tool card. The field is free-form and
4023
4233
  * undocumented; anything past this is assumed to be an encoded image rather
4024
4234
  * than a sentence, and encoded images do not go in the event log. */
@@ -4362,6 +4572,13 @@ var CodexRunner = class {
4362
4572
  #events = [];
4363
4573
  #subscribers = new SubscriberSet();
4364
4574
  #seq = 0;
4575
+ /**
4576
+ * Latest context-window reading, retained from the last `context_usage` this
4577
+ * runner emitted so `GET /sessions` can answer it without an attach — see
4578
+ * `SessionInfo.contextUsage`. Folded in the emit path, so it is by
4579
+ * construction the same number the transcript last drew.
4580
+ */
4581
+ #contextUsage;
4365
4582
  #activityCount = 0;
4366
4583
  #status = "starting";
4367
4584
  #sdkSessionId;
@@ -4417,6 +4634,12 @@ var CodexRunner = class {
4417
4634
  * `mcpServerStatus/list` does not carry a status field at all, so without
4418
4635
  * this every server would read as "configured" and never as up or down. */
4419
4636
  #mcpStatus = /* @__PURE__ */ new Map();
4637
+ /** The spawned agents, keyed by their thread ids — the attribution table
4638
+ * behind `parentToolUseId` and the rollup behind `info().subagents`. Runner-
4639
+ * level, not per-turn: an agent's thread outlives the root turn that spawned
4640
+ * it, and only the child process dying (or the session closing) ends them
4641
+ * all — see the module doc in `subagents.ts`. */
4642
+ #agents = new CodexAgentTracker();
4420
4643
  constructor(config, id = randomUUID()) {
4421
4644
  const mode = config.permissionMode ?? "default";
4422
4645
  if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
@@ -4468,13 +4691,15 @@ var CodexRunner = class {
4468
4691
  createdAt: this.createdAt,
4469
4692
  lastSeq: this.#seq,
4470
4693
  activityCount: this.#activityCount,
4694
+ contextUsage: this.#contextUsage,
4471
4695
  pendingPermissionCount: this.#approvals.size,
4472
4696
  meta: this.#config.meta,
4473
4697
  scope: this.#config.scope,
4474
4698
  title: this.#title(),
4475
4699
  totalCostUsd: this.#totalCostUsd,
4476
4700
  numTurns: this.#numTurns || void 0,
4477
- lastActivityAt: this.#lastActivityAt
4701
+ lastActivityAt: this.#lastActivityAt,
4702
+ subagents: this.#agents.list()
4478
4703
  };
4479
4704
  }
4480
4705
  #title() {
@@ -4688,6 +4913,7 @@ var CodexRunner = class {
4688
4913
  }, "policy");
4689
4914
  this.#connection?.close();
4690
4915
  this.#connection = void 0;
4916
+ this.#agents.sweep();
4691
4917
  this.#activeTurn?.reject(/* @__PURE__ */ new Error("session closed"));
4692
4918
  if (this.#imageDir) try {
4693
4919
  rmSync(this.#imageDir, {
@@ -4738,6 +4964,7 @@ var CodexRunner = class {
4738
4964
  behavior: "deny",
4739
4965
  message
4740
4966
  }, "policy");
4967
+ this.#agents.sweep();
4741
4968
  this.#activeTurn?.reject(new Error(message));
4742
4969
  });
4743
4970
  try {
@@ -5033,11 +5260,79 @@ var CodexRunner = class {
5033
5260
  }
5034
5261
  #handleNotification(method, params) {
5035
5262
  if (this.#closed) return;
5263
+ if (THREAD_SCOPED_NOTIFICATIONS.has(method) && !this.#isRootThread(params)) {
5264
+ if (method === "turn/completed") this.#settleAgentTurn(params);
5265
+ else if (method === "turn/started") {
5266
+ const threadId = this.#threadIdOf(params);
5267
+ const record = threadId ? this.#agents.get(threadId) : void 0;
5268
+ if (record && record.status !== "running") this.#agents.revive(record);
5269
+ }
5270
+ return;
5271
+ }
5036
5272
  this.#notifications[method]?.(params);
5037
5273
  }
5274
+ /** Whether a notification is about the session's own thread. A notification
5275
+ * with no `threadId` counts as the root's: every thread-scoped method the
5276
+ * schema defines carries one, so an absent id means an older or narrower
5277
+ * shape, not a sub-agent. */
5278
+ #isRootThread(params) {
5279
+ const threadId = this.#threadIdOf(params);
5280
+ if (threadId === void 0) return true;
5281
+ return threadId === this.#sdkSessionId;
5282
+ }
5283
+ #threadIdOf(params) {
5284
+ const threadId = params?.threadId;
5285
+ return typeof threadId === "string" ? threadId : void 0;
5286
+ }
5287
+ /**
5288
+ * The agent behind a notification's `threadId` — the attribution every item
5289
+ * and delta handler asks before emitting, so two agents streaming
5290
+ * concurrently into this one connection come apart again by the id each
5291
+ * frame carries, never by any mutable "current agent".
5292
+ *
5293
+ * A non-root thread with no record still gets one: a thread emitting items on
5294
+ * this connection *is* an agent, whatever announced it (codex runs threads of
5295
+ * its own for review/compact, and a `subAgentActivity` could in principle be
5296
+ * missed) — the claude tracker's nested-event fallback, on a stronger signal.
5297
+ * The minted record is label-less and its anchor is authored here, because an
5298
+ * attributed event whose parent id matches no top-level `tool_use` would
5299
+ * render inline rather than as a frame; a late `started` edge fills the name
5300
+ * in. Root-thread traffic — and, defensively, the pre-thread shapes with no
5301
+ * id at all — stays unattributed (`undefined`).
5302
+ */
5303
+ #agentFor(params) {
5304
+ const threadId = this.#threadIdOf(params);
5305
+ if (threadId === void 0 || threadId === this.#sdkSessionId) return void 0;
5306
+ const known = this.#agents.get(threadId);
5307
+ if (known) return known;
5308
+ const nonce = this.#activeTurn?.nonce ?? "codex";
5309
+ const record = this.#agents.open(threadId, `${nonce}:agent:${threadId}`, void 0, Date.now());
5310
+ record.anchored = true;
5311
+ this.#emitToolUse(record.toolUseId, CODEX_AGENT_TOOL, { agentThreadId: threadId });
5312
+ return record;
5313
+ }
5314
+ /**
5315
+ * A child thread's `turn/completed` is that AGENT's completion — the one
5316
+ * codex sends (`subAgentActivity` has no 'completed' kind, verified live).
5317
+ * The verdict is the turn's own status, and the report is the completed
5318
+ * turn's final message, delivered as the anchor's `tool_result` so the row
5319
+ * settles exactly the way a claude `Task`'s does. Deliberately not gated on
5320
+ * `#activeTurn`: an agent finishing between root turns still finished.
5321
+ */
5322
+ #settleAgentTurn(params) {
5323
+ const threadId = this.#threadIdOf(params);
5324
+ const record = threadId ? this.#agents.get(threadId) : void 0;
5325
+ if (!record || record.status !== "running") return;
5326
+ const turn = params?.turn;
5327
+ const status = turn?.status === "completed" ? "done" : "failed";
5328
+ this.#agents.settle(record, status);
5329
+ const report = (turn ? turnReport(turn) : void 0) ?? turn?.error?.message ?? (status === "done" ? "" : turn?.status ?? "failed");
5330
+ this.#emitToolResult(record.toolUseId, report, status === "failed");
5331
+ }
5038
5332
  /** Reasoning deltas arrive on two methods that differ only in which section
5039
5333
  * counter they advance; the section key carries the method so the two streams
5040
- * never share a boundary. Section boundaries (a new summary/content entry)
5334
+ * never share a boundary (and item ids are per-thread, so two agents' streams
5335
+ * never share one either). Section boundaries (a new summary/content entry)
5041
5336
  * render as paragraph breaks — the completed item joins sections with '\n\n'. */
5042
5337
  #reasoningDelta(method) {
5043
5338
  return (params) => {
@@ -5053,7 +5348,7 @@ var CodexRunner = class {
5053
5348
  this.#emitDelta({
5054
5349
  type: "thinking_delta",
5055
5350
  thinking: separator + payload.delta
5056
- });
5351
+ }, this.#agentFor(params)?.toolUseId ?? null);
5057
5352
  };
5058
5353
  }
5059
5354
  /** One item-progress handler serves `item/started` and `item/updated`. */
@@ -5061,7 +5356,7 @@ var CodexRunner = class {
5061
5356
  const active = this.#activeTurn;
5062
5357
  if (!active) return;
5063
5358
  const item = params?.item;
5064
- if (item) this.#handleItemProgress(item, active);
5359
+ if (item) this.#handleItemProgress(item, active, this.#agentFor(params));
5065
5360
  };
5066
5361
  /** The notification dispatch table — every method the child emits that this
5067
5362
  * runner maps, in one place. Handlers read `this.#activeTurn` themselves:
@@ -5079,7 +5374,9 @@ var CodexRunner = class {
5079
5374
  "turn/completed": (params) => {
5080
5375
  const active = this.#activeTurn;
5081
5376
  const turn = params?.turn;
5082
- if (active && turn) active.resolve(turn);
5377
+ if (!active || !turn) return;
5378
+ if (active.turnId && turn.id && turn.id !== active.turnId) return;
5379
+ active.resolve(turn);
5083
5380
  },
5084
5381
  "item/started": this.#itemProgress,
5085
5382
  "item/updated": this.#itemProgress,
@@ -5087,7 +5384,7 @@ var CodexRunner = class {
5087
5384
  const active = this.#activeTurn;
5088
5385
  if (!active) return;
5089
5386
  const item = params?.item;
5090
- if (item) this.#handleItemCompleted(item, active);
5387
+ if (item) this.#handleItemCompleted(item, active, this.#agentFor(params));
5091
5388
  },
5092
5389
  "item/agentMessage/delta": (params) => {
5093
5390
  if (!this.#activeTurn) return;
@@ -5095,7 +5392,7 @@ var CodexRunner = class {
5095
5392
  if (typeof delta === "string" && delta) this.#emitDelta({
5096
5393
  type: "text_delta",
5097
5394
  text: delta
5098
- });
5395
+ }, this.#agentFor(params)?.toolUseId ?? null);
5099
5396
  },
5100
5397
  "item/reasoning/textDelta": this.#reasoningDelta("item/reasoning/textDelta"),
5101
5398
  "item/reasoning/summaryTextDelta": this.#reasoningDelta("item/reasoning/summaryTextDelta"),
@@ -5288,30 +5585,40 @@ var CodexRunner = class {
5288
5585
  if (!this.#closed && this.#approvals.size === 0 && this.#status === "awaiting_approval") this.#setStatus("running");
5289
5586
  }
5290
5587
  /** Tool calls surface as tool_use when they start; text and reasoning stream
5291
- * natively via the delta notifications. */
5292
- #handleItemProgress(item, active) {
5588
+ * natively via the delta notifications. `agent` is the sub-agent whose thread
5589
+ * the item arrived on — undefined for the session's own. */
5590
+ #handleItemProgress(item, active, agent) {
5293
5591
  const id = `${active.nonce}:${item.id}`;
5592
+ if (item.type === "subAgentActivity") {
5593
+ this.#itemCompleted.subAgentActivity(item, active, id, agent);
5594
+ return;
5595
+ }
5294
5596
  if (item.type === "commandExecution" && !active.toolUseEmitted.has(id)) {
5295
5597
  active.toolUseEmitted.add(id);
5296
- this.#emitToolUse(id, "CodexCommand", { command: item.command });
5598
+ this.#emitToolUse(id, "CodexCommand", { command: item.command }, agent);
5297
5599
  return;
5298
5600
  }
5299
5601
  if (item.type === "mcpToolCall" && !active.toolUseEmitted.has(id)) {
5300
5602
  active.toolUseEmitted.add(id);
5301
- this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
5603
+ this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments, agent);
5604
+ return;
5605
+ }
5606
+ if (item.type === "collabAgentToolCall" && !active.toolUseEmitted.has(id)) {
5607
+ active.toolUseEmitted.add(id);
5608
+ this.#emitToolUse(id, CODEX_COLLAB_TOOL, collabInput(item), agent);
5302
5609
  return;
5303
5610
  }
5304
5611
  if (item.type === "imageGeneration" && !active.toolUseEmitted.has(id)) {
5305
5612
  active.toolUseEmitted.add(id);
5306
- this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
5613
+ this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item), agent);
5307
5614
  if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
5308
5615
  }
5309
5616
  }
5310
- #handleItemCompleted(item, active) {
5617
+ #handleItemCompleted(item, active, agent) {
5311
5618
  const id = `${active.nonce}:${item.id}`;
5312
5619
  const handler = this.#itemCompleted[item.type];
5313
5620
  if (handler) {
5314
- handler(item, active, id);
5621
+ handler(item, active, id, agent);
5315
5622
  return;
5316
5623
  }
5317
5624
  const unknown = item;
@@ -5332,67 +5639,121 @@ var CodexRunner = class {
5332
5639
  * union has never heard of; those take the passthrough above.)
5333
5640
  */
5334
5641
  #itemCompleted = {
5335
- userMessage: () => {},
5336
- agentMessage: (item, active, id) => {
5642
+ userMessage: (item, active, _id, agent) => {
5643
+ if (!agent) return;
5644
+ const text = historyUserText(item);
5645
+ if (!text) return;
5646
+ this.#emit({
5647
+ type: "user_message",
5648
+ message: {
5649
+ role: "user",
5650
+ content: text
5651
+ },
5652
+ parentToolUseId: agent.toolUseId,
5653
+ uuid: `${active.nonce}:${item.id}`
5654
+ });
5655
+ },
5656
+ agentMessage: (item, active, id, agent) => {
5337
5657
  const text = typeof item.text === "string" ? item.text : "";
5338
5658
  this.#emitAssistant(id, [{
5339
5659
  type: "text",
5340
5660
  text
5341
- }]);
5342
- active.finalText = text;
5661
+ }], agent?.toolUseId ?? null);
5662
+ if (!agent) active.finalText = text;
5343
5663
  },
5344
- reasoning: (item, _active, id) => {
5664
+ reasoning: (item, _active, id, agent) => {
5345
5665
  const summary = Array.isArray(item.summary) ? item.summary.filter(Boolean) : [];
5346
5666
  const content = Array.isArray(item.content) ? item.content.filter(Boolean) : [];
5347
5667
  const thinking = (summary.length > 0 ? summary : content).join("\n\n");
5348
5668
  if (thinking) this.#emitAssistant(id, [{
5349
5669
  type: "thinking",
5350
5670
  thinking
5351
- }]);
5671
+ }], agent?.toolUseId ?? null);
5352
5672
  },
5353
- commandExecution: (item, active, id) => {
5673
+ commandExecution: (item, active, id, agent) => {
5354
5674
  if (!active.toolUseEmitted.has(id)) {
5355
5675
  active.toolUseEmitted.add(id);
5356
- this.#emitToolUse(id, "CodexCommand", { command: item.command });
5676
+ this.#emitToolUse(id, "CodexCommand", { command: item.command }, agent);
5357
5677
  }
5358
5678
  const exitCode = item.exitCode ?? void 0;
5359
5679
  const failed = item.status === "failed" || item.status === "declined" || exitCode !== void 0 && exitCode !== 0;
5360
5680
  const output = (item.aggregatedOutput ?? "") + (exitCode !== void 0 && exitCode !== 0 ? `\n(exit code ${exitCode})` : "");
5361
- this.#emitToolResult(id, output, failed);
5681
+ this.#emitToolResult(id, output, failed, void 0, agent?.toolUseId ?? null);
5362
5682
  },
5363
- fileChange: (item, _active, id) => {
5364
- this.#emitToolUse(id, "CodexFileChange", { changes: item.changes });
5683
+ fileChange: (item, _active, id, agent) => {
5684
+ this.#emitToolUse(id, "CodexFileChange", { changes: item.changes }, agent);
5365
5685
  const lines = item.changes.map((change) => {
5366
5686
  return `${(typeof change.kind === "string" ? change.kind : change.kind?.type) ?? "change"}: ${change.path}`;
5367
5687
  });
5368
5688
  const only = item.changes.length === 1 ? item.changes[0] : void 0;
5369
- this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined", only?.diff ? parseUnifiedDiff(only.diff, only.path) : void 0);
5689
+ this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined", only?.diff ? parseUnifiedDiff(only.diff, only.path) : void 0, agent?.toolUseId ?? null);
5370
5690
  },
5371
- mcpToolCall: (item, active, id) => {
5691
+ mcpToolCall: (item, active, id, agent) => {
5372
5692
  if (!active.toolUseEmitted.has(id)) {
5373
5693
  active.toolUseEmitted.add(id);
5374
- this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
5694
+ this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments, agent);
5375
5695
  }
5376
5696
  const isError = item.error !== void 0 && item.error !== null || item.status === "failed";
5377
- this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError);
5697
+ this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError, void 0, agent?.toolUseId ?? null);
5378
5698
  },
5379
- webSearch: (item, _active, id) => {
5380
- this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
5381
- this.#emitToolResult(id, "", false);
5699
+ webSearch: (item, _active, id, agent) => {
5700
+ this.#emitToolUse(id, "CodexWebSearch", { query: item.query }, agent);
5701
+ this.#emitToolResult(id, "", false, void 0, agent?.toolUseId ?? null);
5382
5702
  },
5383
- imageGeneration: (item, active, id) => {
5703
+ imageGeneration: (item, active, id, agent) => {
5384
5704
  active.toolUseEmitted.add(id);
5385
- this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
5705
+ this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item), agent);
5386
5706
  if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
5387
5707
  const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
5388
- this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
5708
+ this.#emitToolResult(id, lines.join("\n"), item.status === "failed", void 0, agent?.toolUseId ?? null);
5389
5709
  },
5390
- imageView: (item, _active, id) => {
5391
- this.#emitToolUse(id, "CodexImageView", { path: item.path });
5392
- this.#emitToolResult(id, item.path, false);
5710
+ imageView: (item, _active, id, agent) => {
5711
+ this.#emitToolUse(id, "CodexImageView", { path: item.path }, agent);
5712
+ this.#emitToolResult(id, item.path, false, void 0, agent?.toolUseId ?? null);
5713
+ },
5714
+ subAgentActivity: (item, _active, id, agent) => {
5715
+ if (this.#replayingHistory) {
5716
+ if (item.kind !== "started") return;
5717
+ this.#emitToolUse(id, CODEX_AGENT_TOOL, {
5718
+ ...agentName(item.agentPath) ? { subagent_type: agentName(item.agentPath) } : {},
5719
+ agentThreadId: item.agentThreadId,
5720
+ ...item.agentPath ? { agentPath: item.agentPath } : {}
5721
+ }, agent);
5722
+ this.#emitToolResult(id, "(ran in its own thread — its work is not part of this thread's stored history)", false, void 0, agent?.toolUseId ?? null);
5723
+ return;
5724
+ }
5725
+ const record = this.#agents.get(item.agentThreadId) ?? this.#agents.open(item.agentThreadId, id, void 0, Date.now());
5726
+ const name = agentName(item.agentPath);
5727
+ const relabel = record.agentType === void 0 && name !== void 0;
5728
+ if (relabel) record.agentType = name;
5729
+ if (!record.anchored || relabel) {
5730
+ record.anchored = true;
5731
+ this.#emitToolUse(record.toolUseId, CODEX_AGENT_TOOL, {
5732
+ ...record.agentType ? { subagent_type: record.agentType } : {},
5733
+ agentThreadId: item.agentThreadId,
5734
+ ...item.agentPath ? { agentPath: item.agentPath } : {}
5735
+ }, agent);
5736
+ }
5737
+ if (item.kind === "interrupted") {
5738
+ if (record.status === "running") {
5739
+ this.#agents.settle(record, "failed");
5740
+ this.#emitToolResult(record.toolUseId, "interrupted", true);
5741
+ }
5742
+ return;
5743
+ }
5744
+ if (item.kind !== "started" && record.status !== "running") this.#agents.revive(record);
5745
+ },
5746
+ collabAgentToolCall: (item, active, id, agent) => {
5747
+ if (!active.toolUseEmitted.has(id)) {
5748
+ active.toolUseEmitted.add(id);
5749
+ this.#emitToolUse(id, CODEX_COLLAB_TOOL, collabInput(item), agent);
5750
+ }
5751
+ if (item.status === "inProgress") return;
5752
+ const failed = item.status === "failed" || item.status === "declined";
5753
+ this.#emitToolResult(id, failed ? item.status : "", failed, void 0, agent?.toolUseId ?? null);
5393
5754
  }
5394
5755
  };
5395
- #emitDelta(delta) {
5756
+ #emitDelta(delta, parent) {
5396
5757
  if (this.#config.includePartialMessages === false) return;
5397
5758
  this.#emit({
5398
5759
  type: "stream_delta",
@@ -5400,11 +5761,11 @@ var CodexRunner = class {
5400
5761
  type: "content_block_delta",
5401
5762
  delta
5402
5763
  },
5403
- parentToolUseId: null,
5764
+ parentToolUseId: parent,
5404
5765
  uuid: randomUUID()
5405
5766
  });
5406
5767
  }
5407
- #emitAssistant(uuid, content) {
5768
+ #emitAssistant(uuid, content, parent) {
5408
5769
  this.#emit({
5409
5770
  type: "assistant_message",
5410
5771
  message: {
@@ -5412,11 +5773,19 @@ var CodexRunner = class {
5412
5773
  content,
5413
5774
  model: this.#model ?? this.#resolvedModel
5414
5775
  },
5415
- parentToolUseId: null,
5776
+ parentToolUseId: parent,
5416
5777
  uuid
5417
5778
  });
5418
5779
  }
5419
- #emitToolUse(id, name, input) {
5780
+ /** `agent` (rather than a bare parent id) because a nested call is also the
5781
+ * agent's progress reading: `SubagentInfo.toolCount` ticks here, once per
5782
+ * card — the `counted` set is what keeps an upserted re-emission (the
5783
+ * finished imageGeneration input) from counting one picture twice. */
5784
+ #emitToolUse(id, name, input, agent) {
5785
+ if (agent && !agent.counted.has(id)) {
5786
+ agent.counted.add(id);
5787
+ agent.toolCount += 1;
5788
+ }
5420
5789
  this.#emit({
5421
5790
  type: "assistant_message",
5422
5791
  message: {
@@ -5429,11 +5798,11 @@ var CodexRunner = class {
5429
5798
  }],
5430
5799
  model: this.#model ?? this.#resolvedModel
5431
5800
  },
5432
- parentToolUseId: null,
5801
+ parentToolUseId: agent?.toolUseId ?? null,
5433
5802
  uuid: `${id}-use`
5434
5803
  });
5435
5804
  }
5436
- #emitToolResult(toolUseId, content, isError, patch) {
5805
+ #emitToolResult(toolUseId, content, isError, patch, parent = null) {
5437
5806
  this.#emit({
5438
5807
  type: "user_message",
5439
5808
  message: {
@@ -5445,7 +5814,7 @@ var CodexRunner = class {
5445
5814
  is_error: isError || void 0
5446
5815
  }]
5447
5816
  },
5448
- parentToolUseId: null,
5817
+ parentToolUseId: parent,
5449
5818
  synthetic: true,
5450
5819
  patch,
5451
5820
  uuid: `${toolUseId}-result`
@@ -5573,6 +5942,8 @@ var CodexRunner = class {
5573
5942
  };
5574
5943
  this.#lastActivityAt = event.ts;
5575
5944
  this.#activityCount += transcriptActivity(body);
5945
+ this.#contextUsage = contextReading(body) ?? this.#contextUsage;
5946
+ if (body.type === "conversation_reset") this.#contextUsage = void 0;
5576
5947
  this.#events.push(event);
5577
5948
  this.#subscribers.emit(event);
5578
5949
  }