@workerdeck/core 0.23.0 → 1.1.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,7 +1,7 @@
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, contextReading, 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, transcriptProse } from "@workerdeck/protocol";
5
5
  import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
6
6
  import { execFile, spawn } from "node:child_process";
7
7
  import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -11,16 +11,13 @@ import { lookup } from "node:dns/promises";
11
11
  import { homedir, tmpdir } from "node:os";
12
12
  import { dirname, join, resolve, sep } from "node:path";
13
13
  //#region src/lib/attachments.ts
14
- /** The four the Anthropic API accepts. Notably absent: image/heic — an iPhone's
15
- * native photo format, which clients must transcode before upload. */
16
- const IMAGE_TYPES = new Set([
14
+ const IMAGE_TYPES = /* @__PURE__ */ new Set([
17
15
  "image/jpeg",
18
16
  "image/png",
19
17
  "image/gif",
20
18
  "image/webp"
21
19
  ]);
22
- /** Textual types whose media type doesn't start with `text/`. */
23
- const TEXT_TYPES = new Set([
20
+ const TEXT_TYPES = /* @__PURE__ */ new Set([
24
21
  "application/json",
25
22
  "application/xml",
26
23
  "application/yaml",
@@ -32,11 +29,9 @@ const TEXT_TYPES = new Set([
32
29
  "application/x-httpd-php",
33
30
  "application/sql"
34
31
  ]);
35
- /** Strips any `; charset=…` parameter and lowercases. */
36
32
  function normalizeMediaType(mediaType) {
37
33
  return mediaType.split(";")[0].trim().toLowerCase();
38
34
  }
39
- /** How this media type can be sent, or null if it can't be. */
40
35
  function attachmentKind(mediaType) {
41
36
  const type = normalizeMediaType(mediaType);
42
37
  if (IMAGE_TYPES.has(type)) return "image";
@@ -44,23 +39,11 @@ function attachmentKind(mediaType) {
44
39
  if (type.startsWith("text/") || TEXT_TYPES.has(type)) return "text";
45
40
  return null;
46
41
  }
47
- /** Human-readable list for the 415 an unsupported upload gets. */
48
42
  const SUPPORTED_ATTACHMENT_TYPES = [
49
43
  ...IMAGE_TYPES,
50
44
  "application/pdf",
51
45
  "text/*"
52
46
  ].join(", ");
53
- /**
54
- * Anthropic content blocks for a set of attachments, in the given order.
55
- *
56
- * Blocks lead the message and the user's text follows: the model reads the
57
- * picture, then the instruction about it. Text files are inlined in a named
58
- * envelope rather than as a bare block, so "here is my config" doesn't read as
59
- * something the user typed.
60
- *
61
- * Structurally typed — `packages/core` models Anthropic content the way
62
- * `packages/protocol` does, and the caller casts into the SDK's own param type.
63
- */
64
47
  function attachmentContentBlocks(attachments) {
65
48
  return attachments.map((attachment) => {
66
49
  const mediaType = normalizeMediaType(attachment.mediaType);
@@ -90,7 +73,6 @@ function attachmentContentBlocks(attachments) {
90
73
  }
91
74
  });
92
75
  }
93
- /** Strip the bytes: the log-safe half of an attachment. */
94
76
  function attachmentRef(attachment) {
95
77
  return {
96
78
  id: attachment.id,
@@ -104,10 +86,6 @@ function decodeText(base64) {
104
86
  }
105
87
  //#endregion
106
88
  //#region src/lib/input-queue.ts
107
- /**
108
- * Push-based single-consumer AsyncIterable bridging imperative sendMessage() calls
109
- * into the streaming `prompt` the Agent SDK consumes.
110
- */
111
89
  var InputQueue = class {
112
90
  #buffer = [];
113
91
  #waiter = null;
@@ -163,30 +141,8 @@ var InputQueue = class {
163
141
  };
164
142
  //#endregion
165
143
  //#region src/lib/patch.ts
166
- /**
167
- * Turning an engine's edit output into the wire's {@link FilePatch}.
168
- *
169
- * Both engines know exactly which lines of which file changed, and both say so
170
- * in their own vocabulary: the Claude SDK hands over a `structuredPatch` array
171
- * on `SDKUserMessage.tool_use_result`, codex puts a unified diff string on each
172
- * `fileChange` item. A client can reconstruct neither — it has never seen the
173
- * file — so anything not normalized here is a diff that renders without line
174
- * numbers.
175
- *
176
- * Normalizing in the runner rather than in each client is the point: one shape
177
- * reaches the wire, and the dashboard, the extension and the phone all render
178
- * from it without a per-engine branch or a diff parser of their own.
179
- */
180
- /**
181
- * The most lines a patch may put on the wire.
182
- *
183
- * A patch is replayed on every attach and captured into parking snapshots, so
184
- * "the diff is big" must not become "this session is expensive to open forever".
185
- * Whole hunks are kept or dropped — half a hunk has misleading line numbers —
186
- * and the drop is flagged so a renderer can say the diff is partial instead of
187
- * presenting it as the whole change.
188
- */
189
144
  const MAX_PATCH_LINES = 400;
145
+ const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
190
146
  function capHunks(hunks) {
191
147
  const kept = [];
192
148
  let lines = 0;
@@ -200,25 +156,15 @@ function capHunks(hunks) {
200
156
  }
201
157
  return { hunks: kept };
202
158
  }
203
- /** Structural, not `instanceof`: this reads a field the SDK types as `unknown`,
204
- * and a shape check is the only honest way to know what arrived. */
205
159
  function isHunk(value) {
206
160
  const hunk = value;
207
161
  return !!hunk && typeof hunk.oldStart === "number" && typeof hunk.oldLines === "number" && typeof hunk.newStart === "number" && typeof hunk.newLines === "number" && Array.isArray(hunk.lines) && hunk.lines.every((line) => typeof line === "string");
208
162
  }
209
- /**
210
- * A {@link FilePatch} from the Claude SDK's structured tool output
211
- * (`SDKUserMessage.tool_use_result` for Edit/Write/NotebookEdit).
212
- *
213
- * Everything else on that object is deliberately left behind — `originalFile`
214
- * alone is the entire pre-edit file, which is precisely what must not be logged
215
- * (see `FilePatch`'s own note).
216
- */
217
163
  function filePatchFromToolResult(result) {
218
164
  const output = result;
219
- if (!output || !Array.isArray(output.structuredPatch)) return void 0;
165
+ if (!output || !Array.isArray(output.structuredPatch)) return;
220
166
  const hunks = output.structuredPatch.filter(isHunk);
221
- if (hunks.length === 0) return void 0;
167
+ if (hunks.length === 0) return;
222
168
  const { hunks: kept, truncated } = capHunks(hunks);
223
169
  return {
224
170
  ...typeof output.filePath === "string" && { path: output.filePath },
@@ -227,20 +173,6 @@ function filePatchFromToolResult(result) {
227
173
  ...truncated && { truncated }
228
174
  };
229
175
  }
230
- /** `@@ -oldStart,oldLines +newStart,newLines @@` — the counts are optional and
231
- * mean 1 when absent, which is what a single-line hunk looks like. */
232
- const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
233
- /**
234
- * A {@link FilePatch} from a unified diff — codex's `fileChange.diff`.
235
- *
236
- * Only the hunks are read. A diff's `---`/`+++` header names the file, but codex
237
- * already reports the path on the change itself, and a header path is often
238
- * relative or `/dev/null`, so the caller's path is the one worth trusting.
239
- *
240
- * Returns undefined when there is no hunk header at all: that is not a unified
241
- * diff, and inventing hunk numbers for it would put wrong line numbers on screen
242
- * — worse than none.
243
- */
244
176
  function parseUnifiedDiff(diff, path) {
245
177
  const hunks = [];
246
178
  let current;
@@ -262,7 +194,7 @@ function parseUnifiedDiff(diff, path) {
262
194
  else if (line === "") current.lines.push(" ");
263
195
  else current = void 0;
264
196
  }
265
- if (hunks.length === 0) return void 0;
197
+ if (hunks.length === 0) return;
266
198
  const { hunks: kept, truncated } = capHunks(hunks);
267
199
  return {
268
200
  ...path && { path },
@@ -272,43 +204,18 @@ function parseUnifiedDiff(diff, path) {
272
204
  }
273
205
  //#endregion
274
206
  //#region src/lib/normalize.ts
275
- /** Does this message answer exactly one tool call? A patch is per-file-edit and
276
- * the message says nothing about which of two results it describes, so anything
277
- * else gets no patch rather than a diff pinned to the wrong call. */
207
+ const FAMILY_ORDER = [
208
+ "fable",
209
+ "opus",
210
+ "sonnet",
211
+ "haiku"
212
+ ];
278
213
  function singleToolResult(message) {
279
214
  const content = message.content;
280
215
  if (!Array.isArray(content)) return false;
281
216
  return content.filter((block) => block.type === "tool_result").length === 1;
282
217
  }
283
- /**
284
- * The wrappers the CLI writes into the transcript when the *harness* is talking
285
- * to the model rather than a person talking to the session.
286
- *
287
- * Deliberately a text test, and only these two. The live path has structure to
288
- * go on (`isSynthetic`, `origin.kind`), but **the resumed path has none**: the
289
- * SDK's `SessionMessage` carries exactly `message`, `uuid`, `session_id`,
290
- * `parent_tool_use_id`, `parent_agent_id` and `timestamp` — every one of
291
- * `isMeta`, `isSidechain`, `promptSource` and `origin` is dropped between the
292
- * stored JSONL and what `getSessionMessages` hands back (verified against real
293
- * transcripts). So on resume this is the only signal there is, and without it a
294
- * `<task-notification>` blob comes back as a blue user row and a scrubber mark,
295
- * as if someone had typed it.
296
- *
297
- * `<local-command-caveat>` is here for symmetry and cheap insurance: the SDK
298
- * filters `isMeta` entries out of a resumed transcript itself today, which is
299
- * not a contract anyone wrote down.
300
- *
301
- * What is *not* here matters as much:
302
- * - `<local-command-stdout>` — the reducer turns it into a notice row on
303
- * purpose; marking it synthetic would delete a row both paths show.
304
- * - `<command-name>` — that is a person running a slash command. The reducer
305
- * renders it as the command line they typed; hiding it would erase the turn's
306
- * cause.
307
- */
308
218
  const SYNTHETIC_USER_PREFIXES = ["<task-notification>", "<local-command-caveat>"];
309
- /** First text block's leading tag, for the test above. Tool results and images
310
- * carry no text and are never synthetic by this rule (a tool result is already
311
- * a tool result to every renderer). */
312
219
  function isSyntheticUserText(message) {
313
220
  const content = message.content;
314
221
  const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((block) => block.type === "text")?.text : void 0;
@@ -326,20 +233,6 @@ function toApiMessage(message) {
326
233
  usage: m.usage
327
234
  };
328
235
  }
329
- /**
330
- * Plan rate-limit windows from the CLI's structured `/usage` data, as `rate_limit`
331
- * events — the same shape a live `rate_limit_event` produces.
332
- *
333
- * Without this a client shows no usage at all until a window *changes*, which the
334
- * CLI only reports after a turn moves the needle, and never for a session that is
335
- * only being watched. Polling the snapshot and forwarding it through the existing
336
- * event means replay, the dashboard and the iOS app all get it for free, with no
337
- * new protocol surface.
338
- *
339
- * `status` is not per-window in the usage payload — 'allowed' is what a session
340
- * the CLI is running for us is, by construction. A window with no utilization is
341
- * unknown, not zero, and is dropped rather than reported at 0%.
342
- */
343
236
  function rateLimitEventsFromUsage(usage) {
344
237
  if (!usage.rate_limits_available || !usage.rate_limits) return [];
345
238
  const limits = usage.rate_limits;
@@ -370,15 +263,6 @@ function rateLimitEventsFromUsage(usage) {
370
263
  }
371
264
  return events;
372
265
  }
373
- /**
374
- * The CLI's MCP status, as `McpServerStatusInfo`.
375
- *
376
- * The narrowing is the point: the SDK's config object carries `env` for stdio
377
- * servers and `headers` for HTTP ones, and both routinely hold API tokens. This
378
- * is the one place they are dropped, so no client — dashboard, phone, or a host
379
- * app reading the REST route — can turn "show me my MCP servers" into a
380
- * credential dump. Only the connection's identity survives.
381
- */
382
266
  function mcpStatusInfo(status) {
383
267
  const config = status.config;
384
268
  const transport = config?.type ?? (config?.command ? "stdio" : void 0);
@@ -399,26 +283,6 @@ function mcpStatusInfo(status) {
399
283
  }))
400
284
  };
401
285
  }
402
- /**
403
- * The CLI's model list, as `ModelOption[]`.
404
- *
405
- * Two decisions live here rather than in each client:
406
- *
407
- * - **`default` is dropped.** The CLI offers a row whose id is literally
408
- * `default` ("Default (recommended)"), meaning "whatever I would have picked".
409
- * It is a legal id to send, but it is not a model: a session running on it
410
- * reports a real model, so a picker showing it has a row that can never be
411
- * checked, and a status bar naming it would say "Default" for a session
412
- * answering as Opus. Which model the default resolved to is a different
413
- * question, and `system_init` answers it.
414
- * - **`primary` is derived.** The CLI reports one flat list; Claude Code's own
415
- * picker shows the newest of each family and files the rest under "more
416
- * models". The list arrives newest-first, so the first row of each family is
417
- * the primary one. A heuristic, but a stable one — and doing it once here
418
- * means the dashboard and the phone group identically.
419
- */
420
- /** What the CLI's `default` row resolves to — the model a session will answer as
421
- * before it has answered anything. Dropped from the list, kept as this. */
422
286
  function defaultModelFromSdk(models) {
423
287
  return models.find((model) => model.value === "default")?.resolvedModel;
424
288
  }
@@ -452,50 +316,26 @@ function modelOptionsFromSdk(models) {
452
316
  return rankA === rankB ? a.index - b.index : rankA - rankB;
453
317
  }).map(({ option }) => option);
454
318
  }
455
- const FAMILY_ORDER = [
456
- "fable",
457
- "opus",
458
- "sonnet",
459
- "haiku"
460
- ];
461
319
  function familyRank(option) {
462
320
  const rank = FAMILY_ORDER.indexOf(modelFamily(option.resolvedModel ?? option.value));
463
321
  return rank === -1 ? FAMILY_ORDER.length : rank;
464
322
  }
465
- /**
466
- * The name a person says, from a wire model id: 'claude-opus-5[1m]' → "Opus 5",
467
- * 'claude-haiku-4-5-20251001' → "Haiku 4.5".
468
- *
469
- * The CLI's own `displayName` is the family alone ("Opus", "Haiku") or carries a
470
- * variant instead of a version ("Opus (1M context)"), and the version is the part
471
- * that answers "is this the current one". It is only ever in the id, so it is
472
- * read from there. Returns null when the id has no version to read — a bare
473
- * alias like 'sonnet' — and the CLI's name stands.
474
- */
475
- function friendlyModelName(id) {
323
+ function modelIdParts(id) {
476
324
  const parts = (id.split("[")[0] ?? id).toLowerCase().split("-").filter(Boolean);
477
325
  if (parts[0] === "claude") parts.shift();
326
+ return parts;
327
+ }
328
+ function friendlyModelName(id) {
329
+ const parts = modelIdParts(id);
478
330
  const family = parts.shift();
479
331
  if (!family) return null;
480
332
  const version = parts.filter((part) => !/^\d{8}$/.test(part));
481
333
  if (version.length === 0 || version.some((part) => !/^\d+$/.test(part))) return null;
482
334
  return `${family.charAt(0).toUpperCase()}${family.slice(1)} ${version.join(".")}`;
483
335
  }
484
- /** 'claude-opus-4-8[1m]' → "opus". The vendor prefix, the context-window suffix
485
- * and the version tail are all dropped; what is left is the family a person
486
- * names. Unrecognisable ids become their own family, so a model this rule has
487
- * never seen lands in the main list rather than being hidden. */
488
336
  function modelFamily(id) {
489
- const withoutVariant = id.split("[")[0] ?? id;
490
- const parts = withoutVariant.toLowerCase().split("-");
491
- if (parts[0] === "claude") parts.shift();
492
- return parts[0] ?? withoutVariant;
337
+ return modelIdParts(id)[0] ?? id;
493
338
  }
494
- /**
495
- * Map one SDKMessage to a wire-protocol event body, or null for messages the runner
496
- * consumes itself (system_init and session-state changes carry runner state and are
497
- * emitted by the runner with extra context).
498
- */
499
339
  function normalizeSdkMessage(msg) {
500
340
  switch (msg.type) {
501
341
  case "assistant": return {
@@ -560,26 +400,73 @@ function normalizeSdkMessage(msg) {
560
400
  }
561
401
  }
562
402
  //#endregion
403
+ //#region src/lib/event-log.ts
404
+ var EventLog = class {
405
+ #events = [];
406
+ #seq = 0;
407
+ #activityCount = 0;
408
+ #proseCount = 0;
409
+ #contextUsage;
410
+ #resetSeq = 0;
411
+ #lastActivityAt;
412
+ get events() {
413
+ return this.#events;
414
+ }
415
+ get seq() {
416
+ return this.#seq;
417
+ }
418
+ get activityCount() {
419
+ return this.#activityCount;
420
+ }
421
+ /** The unread badge's unit — see `transcriptProse`. Folded here so a restored log recomputes it. */
422
+ get proseCount() {
423
+ return this.#proseCount;
424
+ }
425
+ get contextUsage() {
426
+ return this.#contextUsage;
427
+ }
428
+ get resetSeq() {
429
+ return this.#resetSeq;
430
+ }
431
+ get lastActivityAt() {
432
+ return this.#lastActivityAt;
433
+ }
434
+ append(body) {
435
+ const event = {
436
+ ...body,
437
+ seq: ++this.#seq,
438
+ ts: Date.now()
439
+ };
440
+ this.#lastActivityAt = event.ts;
441
+ this.#fold(event);
442
+ this.#events.push(event);
443
+ return event;
444
+ }
445
+ at(seq) {
446
+ return this.#events.find((event) => event.seq === seq);
447
+ }
448
+ restore(events, seq, lastActivityAt) {
449
+ this.#events = [...events];
450
+ this.#seq = seq;
451
+ this.#activityCount = 0;
452
+ this.#proseCount = 0;
453
+ this.#contextUsage = void 0;
454
+ this.#resetSeq = 0;
455
+ for (const event of this.#events) this.#fold(event);
456
+ this.#lastActivityAt = lastActivityAt;
457
+ }
458
+ #fold(event) {
459
+ this.#activityCount += transcriptActivity(event);
460
+ this.#proseCount += transcriptProse(event);
461
+ this.#contextUsage = contextReading(event) ?? this.#contextUsage;
462
+ if (event.type === "conversation_reset") {
463
+ this.#resetSeq = event.seq;
464
+ this.#contextUsage = void 0;
465
+ }
466
+ }
467
+ };
468
+ //#endregion
563
469
  //#region src/lib/replay.ts
564
- /**
565
- * Which buffered events a coalesced replay should skip: everything superseded
566
- * by a later event with the same {@link replayCoalesceKey}.
567
- *
568
- * A **backwards** scan, keeping the first occurrence of each key — which is the
569
- * whole trick. Walking forwards would need a second pass to know which of the
570
- * fifty context readings was the last one; walking backwards, the first one you
571
- * meet *is* the last one, and everything after it (in scan order) is history.
572
- *
573
- * Note what this does **not** do: it never reorders and never touches an event
574
- * with no key. Transcript content is an ordered fold — a stream delta
575
- * accumulates onto a message, a tool result attaches to a call that came
576
- * earlier, a turn result finalizes — so it must arrive exactly as it was
577
- * emitted. Only last-write-wins *state* is eligible, and `replayCoalesceKey`
578
- * is where that judgement lives.
579
- *
580
- * `afterSeq` is honoured so the scan agrees with the caller's replay window: an
581
- * event the caller was never going to send must not suppress one it was.
582
- */
583
470
  function staleReplaySeqs(events, afterSeq) {
584
471
  const stale = /* @__PURE__ */ new Set();
585
472
  const seen = /* @__PURE__ */ new Set();
@@ -593,45 +480,6 @@ function staleReplaySeqs(events, afterSeq) {
593
480
  }
594
481
  return stale;
595
482
  }
596
- /**
597
- * The one replay body, and what a socket receives from it.
598
- *
599
- * Every runner had a byte-identical copy of this loop — three spellings of four
600
- * rules, one of which ("never drop the highest-seq event, whatever the rule
601
- * says") is load-bearing and was three copies of a comment. Not a base class:
602
- * the runners share nothing else, and a base class would have to own `#emit`,
603
- * the most engine-specific method each of them has.
604
- *
605
- * The rules, in the order they are applied:
606
- *
607
- * 1. `afterSeq` — the caller already holds everything at or below it.
608
- * 2. `resetSeq` — transcript *content* strictly below the latest
609
- * `conversation_reset` is skipped, so a re-attach cannot resurrect a cleared
610
- * conversation while state events still replay. **Every engine that can emit
611
- * a reset must track it and pass it** — this was Claude's alone only for as
612
- * long as Claude's was the only engine that could produce the event, and the
613
- * failure when a runner forgets is quiet: the end state is right for a
614
- * current reducer, so nothing looks broken while every attach re-sends the
615
- * whole cleared conversation for the process's lifetime.
616
- * 3. `coalesceReplay` — last-write-wins state readings superseded later in the
617
- * same replay (`staleReplaySeqs`), plus everything `replayRetains` says the
618
- * reducer reads and discards. Opt-in, and only sound for a consumer whose
619
- * handling of those events is last-write-wins.
620
- * 4. `imageRefs` — a base64 image part is delivered as an `image_ref` address
621
- * (protocol's {@link imagePartRef}), its bytes one REST fetch away. Applied
622
- * **before** rule 5, because it stamps indices from the stored part array
623
- * which rule 5 then reshapes. Unlike rule 5 this also applies to the live
624
- * path (see `SubscriberSet`), which is the one place these two rules differ.
625
- * 5. `truncateResults` — a huge `tool_result` block is delivered as its head
626
- * plus the markers that say so. **Never mutates the stored event**: the live
627
- * path, the parking snapshot and the fetch route all need the whole thing,
628
- * so this builds a copy and the log stays the log.
629
- *
630
- * The highest-seq event is delivered whatever rules 2 and 3 say — a client's
631
- * replay hold waits for `state.lastSeq` to reach the attach's and would
632
- * otherwise hang forever — but it is still *truncated* when rule 4 applies. A
633
- * session that ends on a `find /` puts its 641 KB frame exactly there.
634
- */
635
483
  function replaySlice(events, options) {
636
484
  const { afterSeq, resetSeq = 0, coalesceReplay, truncateResults, imageRefs } = options;
637
485
  const stale = coalesceReplay ? staleReplaySeqs(events, afterSeq) : void 0;
@@ -649,16 +497,6 @@ function replaySlice(events, options) {
649
497
  }
650
498
  return out;
651
499
  }
652
- /**
653
- * A copy of `event` whose oversized `tool_result` blocks carry their head and
654
- * say so — or `event` itself, unchanged and un-copied, when nothing is over the
655
- * budget. That identity matters: an attach is mostly small events, and a fresh
656
- * object for every one of them would cost more than the feature saves.
657
- *
658
- * Blocks are measured and cut **individually**. A message answering three calls
659
- * where one is a `find /` keeps the two small results whole, which is what makes
660
- * the per-block marker (rather than a per-event one) honest.
661
- */
662
500
  function truncateResultBlocks(event) {
663
501
  if (event.type !== "user_message") return event;
664
502
  const content = event.message.content;
@@ -687,22 +525,11 @@ function truncateResultBlocks(event) {
687
525
  }
688
526
  };
689
527
  }
690
- /** Characters in a result's content, in the same terms a reader sees it: the
691
- * string itself, or every text part of a block list joined by newlines — which
692
- * is exactly what `blockText` in the reducer builds. Non-text parts (an image
693
- * block) contribute nothing, because they are not what is large here and
694
- * slicing them would corrupt them. */
695
528
  function resultChars(content) {
696
529
  if (typeof content === "string") return content.length;
697
530
  if (!Array.isArray(content)) return 0;
698
531
  return content.reduce((total, part, index) => total + (typeof part.text === "string" ? part.text.length + (index > 0 ? 1 : 0) : 0), 0);
699
532
  }
700
- /** The first `chars` characters, in the content's own shape — a string stays a
701
- * string, a block list stays a block list (cut at the part that crosses the
702
- * budget, with the remaining parts dropped). Shape-preserving on purpose: the
703
- * reducer, both renderers and the copy button all read this the same way they
704
- * read a whole one, so truncation is a shorter result and never a different
705
- * kind of one. */
706
533
  function headOf(content, chars) {
707
534
  if (typeof content === "string") return content.slice(0, chars);
708
535
  if (!Array.isArray(content)) return content;
@@ -724,23 +551,6 @@ function headOf(content, chars) {
724
551
  }
725
552
  return parts;
726
553
  }
727
- /**
728
- * A copy of `event` whose `tool_result` blocks carry `image_ref` addresses in
729
- * place of their base64 image parts — or `event` itself, unchanged and
730
- * un-copied, when it holds none. Same identity rule as
731
- * {@link truncateResultBlocks}, and it matters more here: an event carrying an
732
- * image at all is the exception, so the common path must not allocate.
733
- *
734
- * **Never mutates the stored event.** The log is what the parking snapshot
735
- * embeds, what `Runner.eventAt` reads, and therefore what the fetch route
736
- * serves the bytes back from — a drop that reached the log would 404 the very
737
- * lazy-load this rule promises.
738
- *
739
- * Indices are stamped from the **stored** array, which is why this runs *before*
740
- * truncation rather than after: `headOf` reshapes a block's parts, so an address
741
- * computed on its output would name the wrong part of the stored block. That
742
- * ordering is asserted in `replay-image-ref.test.ts`, not merely intended.
743
- */
744
554
  function refImageParts(event) {
745
555
  if (event.type !== "user_message") return event;
746
556
  const content = event.message.content;
@@ -778,14 +588,6 @@ function refImageParts(event) {
778
588
  //#region src/lib/subscribers.ts
779
589
  var SubscriberSet = class {
780
590
  #listeners = /* @__PURE__ */ new Map();
781
- /**
782
- * Replay `events` to `listener` under `options`, then hold it for live
783
- * delivery. Returns the unsubscribe.
784
- *
785
- * The replay runs *before* the listener joins the set, which is the ordering
786
- * every runner already had and is load-bearing: joining first would deliver a
787
- * live event emitted mid-replay ahead of the buffered events preceding it.
788
- */
789
591
  subscribe(events, listener, afterSeq = 0, options, resetSeq = 0) {
790
592
  const asked = options ?? {};
791
593
  for (const event of replaySlice(events, {
@@ -798,11 +600,9 @@ var SubscriberSet = class {
798
600
  this.#listeners.delete(listener);
799
601
  };
800
602
  }
801
- /** Drop every subscriber — a park, which ends the session's live stream. */
802
603
  clear() {
803
604
  this.#listeners.clear();
804
605
  }
805
- /** Fan one event out, transformed per subscriber. */
806
606
  emit(event) {
807
607
  for (const [listener, asked] of this.#listeners) try {
808
608
  listener(asked.imageRefs ? refImageParts(event) : event);
@@ -810,71 +610,34 @@ var SubscriberSet = class {
810
610
  }
811
611
  };
812
612
  //#endregion
613
+ //#region src/lib/title.ts
614
+ function hostTitle(meta) {
615
+ const title = meta?.title;
616
+ return typeof title === "string" && title.length > 0 ? title : void 0;
617
+ }
618
+ function sessionTitle(config, engineTitle) {
619
+ const host = hostTitle(config.meta);
620
+ if (host) return host;
621
+ if (engineTitle) return engineTitle;
622
+ const prompt = config.prompt;
623
+ if (!prompt) return;
624
+ return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
625
+ }
626
+ function withTitle(config, title) {
627
+ const meta = { ...config.meta };
628
+ if (title) meta.title = title;
629
+ else delete meta.title;
630
+ return {
631
+ ...config,
632
+ meta
633
+ };
634
+ }
635
+ //#endregion
813
636
  //#region src/engines/claude/subagents.ts
814
- /**
815
- * The rollup behind `SessionInfo.subagents` — what a sessions list (which never
816
- * attaches) can know about the sub-agents running inside a session. Fed from
817
- * `SessionRunner.#emit`, the one chokepoint every event passes through, so the
818
- * resume backfill — which replays history through the same path — reconstructs
819
- * it with no persistence of its own. Grouping is by Task id throughout, never
820
- * adjacency: parallel sub-agents interleave in the stream, the same fact that
821
- * broke the terminal theme's positional row model.
822
- *
823
- * Three decisions live here rather than in the protocol doc:
824
- *
825
- * **What counts as a spawn.** A record opens when a *top-level* assistant
826
- * message carries a `tool_use` named `Task` or `Agent` — the moment the
827
- * sub-agent exists, so a just-spawned agent is visible before its first nested
828
- * event, with the block's input in hand for its labels. Both names are observed
829
- * SDK spellings (`Task` synchronous, `Agent` async), and the name is a
830
- * convention, not a law — a session that spawned three `Agent`s under a tracker
831
- * that only knew `Task` reported all three as label-less failures. So three
832
- * more openers back the allowlist up: the CLI's own `task_started` system event
833
- * (which positively names the `tool_use_id` an agent runs under, with the brief
834
- * as labels), the launch acknowledgement (below), and — as before — any nested
835
- * event whose `parentToolUseId` has no record: an id that events demonstrably
836
- * nest under *is* a sub-agent, whatever the spawning call was named. A fallback
837
- * record never saw an input, so it stays label-less until a named signal fills
838
- * it in rather than resetting an accumulated count.
839
- *
840
- * **A background agent's `tool_result` is a launch receipt, not a verdict.**
841
- * An async agent's spawn call resolves seconds after the spawn with "Async
842
- * agent launched successfully. (This tool result is internal metadata …)" —
843
- * long before the agent has done anything — and its actual outcome travels on
844
- * a `task_notification` system event instead (`status: 'completed'` is `done`,
845
- * any other way of stopping is `failed`: the report the notification exists to
846
- * deliver never came). Settling on the receipt would read "0 of 3 agents
847
- * running" while three agents burn tokens, so a non-error result on a record
848
- * known to be background never settles it. Known how: the `task_started` event
849
- * live, or the receipt's own wrapper text on a resume — the stored transcript
850
- * carries none of the CLI's system events, so, exactly as
851
- * `isSyntheticUserText` documents for the `<task-notification>` blob, the text
852
- * is the only signal the replayed path has.
853
- *
854
- * **What an interrupted turn leaves behind.** A Task whose `tool_result` never
855
- * arrives — interrupt, session error, a turn or budget cap — would otherwise
856
- * read `running` on an idle session forever, a lie a list re-renders at every
857
- * poll. So the end of a turn settles every still-running record as `failed`:
858
- * the report never came, which is the one thing `done` could have claimed. The
859
- * sweep keys on `turn_result`, on the status coming to rest (`idle` — which is
860
- * how a resumed history that ends mid-Task settles, since the backfill replays
861
- * no `turn_result` — or a terminal state), and on the session closing. A real
862
- * verdict arriving anyway outranks the sweep's inference. The sweep's premise
863
- * — "the turn ended, so anything still running was cut off" — is false for a
864
- * background agent, which is *designed* to outlive its turn: the real session
865
- * behind this file ended three turns while its agents ran, and every
866
- * `turn_result` re-branded live, working agents as failures. So the turn and
867
- * idle sweeps spare a record marked background by a **live** signal. They do
868
- * not spare one whose only evidence is replayed: the backfill describes a
869
- * process that is gone, and a background agent the old process died inside can
870
- * never notify — `running` would be the forever-lie again. `session_closed`
871
- * and the terminal statuses settle everything, background included, for the
872
- * same reason: the process hosting those agents is gone.
873
- */
637
+ const SPAWNER_NAMES = /* @__PURE__ */ new Set(["Task", "Agent"]);
874
638
  var SubagentTracker = class {
875
639
  #records = /* @__PURE__ */ new Map();
876
640
  #settleCounter = 0;
877
- /** Fold one emitted event body into the rollup, in log order. */
878
641
  observe(body, ts) {
879
642
  switch (body.type) {
880
643
  case "assistant_message":
@@ -954,14 +717,8 @@ var SubagentTracker = class {
954
717
  default: return;
955
718
  }
956
719
  }
957
- /**
958
- * The rollup as `SessionInfo.subagents` serves it: spawn order (the
959
- * transcript's own), fresh objects, and `undefined` when there is nothing to
960
- * say — absent and empty mean the same thing to a client, and an empty array
961
- * on every row of a 1.2s-polled list is bytes spent saying nothing.
962
- */
963
720
  list() {
964
- if (this.#records.size === 0) return void 0;
721
+ if (this.#records.size === 0) return;
965
722
  const out = [];
966
723
  for (const r of this.#records.values()) out.push({
967
724
  toolUseId: r.toolUseId,
@@ -992,13 +749,6 @@ var SubagentTracker = class {
992
749
  record.agentType ??= cleaned(input?.subagent_type);
993
750
  record.description ??= cleaned(input?.description);
994
751
  }
995
- /**
996
- * End of turn (`final: false`): anything still running was cut off before
997
- * its report — except a background agent the live process still hosts, which
998
- * is designed to outlive the turn and settles by notification instead. End
999
- * of session (`final: true`): everything, background included, because the
1000
- * process those agents lived in is gone.
1001
- */
1002
752
  #sweep(final) {
1003
753
  for (const record of this.#records.values()) {
1004
754
  if (record.status !== "running") continue;
@@ -1025,53 +775,32 @@ var SubagentTracker = class {
1025
775
  }
1026
776
  }
1027
777
  };
1028
- /** The spawner names observed in the wild: `Task` runs the agent inside the
1029
- * turn, `Agent` launches it in the background. Deliberately just these two —
1030
- * a third spelling is caught by `task_started`, the launch receipt, or the
1031
- * nested-event fallback, so widening this to every tool would only turn
1032
- * ordinary calls into phantom agents. */
1033
- const SPAWNER_NAMES = new Set(["Task", "Agent"]);
1034
- /** The async spawn's immediate `tool_result` — "Async agent launched
1035
- * successfully. (This tool result is internal metadata …)" — recognized by its
1036
- * wrapper text because on a resume that text is the only signal there is (the
1037
- * `SYNTHETIC_USER_PREFIXES` argument; the CLI's system events are not stored).
1038
- * Live, `task_started` marks the record first and this is redundant armor. */
1039
- const isLaunchAck = (content) => {
778
+ function isLaunchAck(content) {
1040
779
  const text = typeof content === "string" ? content : firstText(Array.isArray(content) ? content : []);
1041
780
  return typeof text === "string" && text.trimStart().startsWith("Async agent launched");
1042
- };
1043
- /** A background agent stopping, parsed from the `<task-notification>` wrapper
1044
- * the CLI writes into the transcript. Field-tolerant on purpose: only the
1045
- * `tool-use-id` (this rollup's key) and the `status` verdict are read. */
1046
- const parseTaskNotification = (text) => {
1047
- if (text === void 0 || !text.trimStart().startsWith("<task-notification>")) return void 0;
781
+ }
782
+ function parseTaskNotification(text) {
783
+ if (text === void 0 || !text.trimStart().startsWith("<task-notification>")) return;
1048
784
  const toolUseId = /<tool-use-id>\s*([^<\s]+)\s*<\/tool-use-id>/.exec(text)?.[1];
1049
- if (toolUseId === void 0) return void 0;
785
+ if (toolUseId === void 0) return;
1050
786
  return {
1051
787
  toolUseId,
1052
788
  status: /<status>\s*([^<]*?)\s*<\/status>/.exec(text)?.[1] ?? ""
1053
789
  };
1054
- };
1055
- /** The first text of a message body, however the content is spelled — the
1056
- * stored transcript uses bare strings, the live stream uses blocks. */
1057
- const firstText = (content) => {
790
+ }
791
+ function firstText(content) {
1058
792
  if (typeof content === "string") return content;
1059
793
  for (const block of content) {
1060
794
  const b = block;
1061
795
  if (b?.type === "text" && typeof b.text === "string") return b.text;
1062
796
  }
1063
- };
1064
- /** Trim, drop blank, clip at the same 80 the terminal theme's `taskLabel` uses.
1065
- * Model-authored input rides every row of a polled sessions list, so it is
1066
- * bounded here rather than trusted — a 10KB `description` would be paid for at
1067
- * every poll. */
1068
- const cleaned = (value) => {
1069
- if (typeof value !== "string") return void 0;
797
+ }
798
+ function cleaned(value) {
799
+ if (typeof value !== "string") return;
1070
800
  const text = value.trim();
1071
- if (text === "") return void 0;
801
+ if (text === "") return;
1072
802
  return text.length > 80 ? text.slice(0, 79) + "…" : text;
1073
- };
1074
- /** The `tool_use` blocks of a message body, however the content is spelled. */
803
+ }
1075
804
  function toolUseBlocks(content) {
1076
805
  if (typeof content === "string") return [];
1077
806
  const blocks = [];
@@ -1090,37 +819,13 @@ function toolUseBlocks(content) {
1090
819
  //#endregion
1091
820
  //#region src/engines/claude/runner.ts
1092
821
  const DEFAULT_APPROVAL_TIMEOUT_MS$1 = 3e5;
1093
- /**
1094
- * One live Agent SDK session: owns the query() call, the streaming input queue, the
1095
- * pending-approval table, and a seq-numbered event log that subscribers can replay.
1096
- * No transport — the server (or any host) subscribes and bridges to the wire.
1097
- */
1098
822
  var SessionRunner = class {
1099
823
  id;
1100
824
  createdAt;
1101
825
  #config;
1102
- /** {@link SessionRunnerConfig.cwd}, checked once in the constructor. */
1103
826
  #cwd;
1104
- #events = [];
827
+ #log = new EventLog();
1105
828
  #subscribers = new SubscriberSet();
1106
- #seq = 0;
1107
- /**
1108
- * Latest context-window reading, retained from the last `context_usage` this
1109
- * runner emitted so `GET /sessions` can answer it without an attach — see
1110
- * `SessionInfo.contextUsage`. Folded in the emit path, so it is by
1111
- * construction the same number the transcript last drew.
1112
- */
1113
- #contextUsage;
1114
- #activityCount = 0;
1115
- /**
1116
- * Seq of the latest `conversation_reset` event, 0 when none. The log itself is
1117
- * never truncated — it still carries the state-bearing events (`capabilities`,
1118
- * `system_init`, …) a fresh attacher depends on and which are not re-emitted —
1119
- * but `subscribe()` skips transcript *content* strictly below this mark, so a
1120
- * replay does not resurrect a cleared conversation. A later reset supersedes
1121
- * an earlier one by overwriting it.
1122
- */
1123
- #resetSeq = 0;
1124
829
  #status = "starting";
1125
830
  #statusDetail;
1126
831
  #sdkSessionId;
@@ -1128,39 +833,14 @@ var SessionRunner = class {
1128
833
  #apiKeySource;
1129
834
  #permissionMode;
1130
835
  #pending = /* @__PURE__ */ new Map();
1131
- /**
1132
- * The turn ended while an approval was standing, and nothing has started a
1133
- * new one since.
1134
- *
1135
- * `awaiting_approval` rightly outranks `idle` for display, so a turn-over
1136
- * signal arriving under a standing approval cannot be applied when it lands.
1137
- * It used to be **discarded** for that reason, which is a different thing
1138
- * from outranked: the settle path then asserted `running` on the assumption
1139
- * that an answered approval means work resumes, and when the turn was already
1140
- * over — an interrupt, a timeout — the session claimed to be running one that
1141
- * had produced its result. Status is purely edge-driven here, with no poll and
1142
- * no reconciliation anywhere, so that single dropped edge never came back and
1143
- * every client rendered it faithfully for the life of the session.
1144
- *
1145
- * So the fact is *deferred* rather than dropped, and it is deliberately
1146
- * cleared the moment work genuinely resumes — a turn-over belongs to the turn
1147
- * that produced it and must not settle the next one.
1148
- */
1149
836
  #turnOverWhileBlocked = false;
1150
- /** The read-time sub-agent rollup (`SessionInfo.subagents`), fed from #emit —
1151
- * the one chokepoint — so the resume backfill reconstructs it for free. */
1152
837
  #subagents = new SubagentTracker();
1153
838
  #totalCostUsd;
1154
839
  #numTurns;
1155
- #lastActivityAt;
1156
840
  #input = new InputQueue();
1157
841
  #query;
1158
842
  #capabilitiesEmitted = false;
1159
- /** Last plan reported by the usage poll, so `plan_info` is emitted on change
1160
- * rather than once per turn. */
1161
843
  #subscriptionType;
1162
- /** The title the CLI gave this thread (see `#fetchEngineTitle`). Undefined
1163
- * until it has one — a session gets its summary a turn or two in. */
1164
844
  #engineTitle;
1165
845
  #started = false;
1166
846
  #closed = false;
@@ -1180,9 +860,8 @@ var SessionRunner = class {
1180
860
  return this.#sdkSessionId;
1181
861
  }
1182
862
  get lastSeq() {
1183
- return this.#seq;
863
+ return this.#log.seq;
1184
864
  }
1185
- /** 'oauth' = claude.ai subscription credentials; other values are API-key provenance. */
1186
865
  get apiKeySource() {
1187
866
  return this.#apiKeySource;
1188
867
  }
@@ -1203,49 +882,23 @@ var SessionRunner = class {
1203
882
  canBypassPermissions: this.#config.permissionMode === "bypassPermissions" || this.#config.allowDangerouslySkipPermissions === true,
1204
883
  apiKeySource: this.#apiKeySource,
1205
884
  createdAt: this.createdAt,
1206
- lastSeq: this.#seq,
1207
- activityCount: this.#activityCount,
1208
- contextUsage: this.#contextUsage,
885
+ lastSeq: this.#log.seq,
886
+ activityCount: this.#log.activityCount,
887
+ proseCount: this.#log.proseCount,
888
+ contextUsage: this.#log.contextUsage,
1209
889
  pendingPermissionCount: this.#pending.size,
1210
890
  subagents: this.#subagents.list(),
1211
891
  meta: this.#config.meta,
1212
892
  scope: this.#config.scope,
1213
- title: this.#title(),
893
+ title: sessionTitle(this.#config, this.#engineTitle),
1214
894
  totalCostUsd: this.#totalCostUsd,
1215
895
  numTurns: this.#numTurns,
1216
- lastActivityAt: this.#lastActivityAt
896
+ lastActivityAt: this.#log.lastActivityAt
1217
897
  };
1218
898
  }
1219
- /**
1220
- * Three sources, most-deliberate first: the host's own rename (`meta.title`),
1221
- * the title the CLI gave this thread (`#engineTitle`), then the first prompt
1222
- * truncated.
1223
- *
1224
- * The rename outranks everything by design — a person naming a session must
1225
- * not have it renamed under them by a model — which is also why the engine
1226
- * title is *only ever read* while `meta.title` is unset (see
1227
- * `#fetchEngineTitle`), rather than read and then discarded here.
1228
- */
1229
- #title() {
1230
- const metaTitle = this.#config.meta?.title;
1231
- if (typeof metaTitle === "string" && metaTitle.length > 0) return metaTitle;
1232
- if (this.#engineTitle) return this.#engineTitle;
1233
- const prompt = this.#config.prompt;
1234
- if (!prompt) return void 0;
1235
- return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
1236
- }
1237
- /** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
1238
- * it (undefined) restores the derived title. The engine is never told. */
1239
899
  setTitle(title) {
1240
- const meta = { ...this.#config.meta };
1241
- if (title) meta.title = title;
1242
- else delete meta.title;
1243
- this.#config = {
1244
- ...this.#config,
1245
- meta
1246
- };
900
+ this.#config = withTitle(this.#config, title);
1247
901
  }
1248
- /** Begin the session. Idempotent; returns the run promise (resolves when the query ends). */
1249
902
  start() {
1250
903
  if (this.#started) return this.#runPromise;
1251
904
  this.#started = true;
@@ -1253,11 +906,6 @@ var SessionRunner = class {
1253
906
  this.#runPromise = this.#run();
1254
907
  return this.#runPromise;
1255
908
  }
1256
- /** Queue a user message for the session (starts the next turn when idle).
1257
- *
1258
- * `attachments` carry their own bytes; they reach the CLI as content blocks and
1259
- * are logged as references. A message may be attachments alone — an empty text
1260
- * block is not valid API input, so the text is only added when there is some. */
1261
909
  sendMessage(text, attachments) {
1262
910
  if (this.#closed) throw new Error("session is closed");
1263
911
  const blocks = attachments?.length ? attachmentContentBlocks(attachments) : [];
@@ -1285,12 +933,9 @@ var SessionRunner = class {
1285
933
  uuid: randomUUID()
1286
934
  });
1287
935
  }
1288
- /** Live MCP server status, straight from the CLI. Undefined when the engine
1289
- * can't answer (an injected fake query in tests) — the caller 501s rather than
1290
- * pretending the session has no servers. */
1291
936
  async mcpServers() {
1292
937
  const query = this.#query;
1293
- if (typeof query?.mcpServerStatus !== "function") return void 0;
938
+ if (typeof query?.mcpServerStatus !== "function") return;
1294
939
  return (await query.mcpServerStatus()).map(mcpStatusInfo);
1295
940
  }
1296
941
  async reconnectMcpServer(name) {
@@ -1303,7 +948,6 @@ var SessionRunner = class {
1303
948
  if (typeof query?.toggleMcpServer !== "function") throw new Error("this session cannot enable or disable MCP servers");
1304
949
  await query.toggleMcpServer(name, enabled);
1305
950
  }
1306
- /** Resolve a pending permission request. Returns false if the id is unknown (e.g. timed out). */
1307
951
  resolvePermission(requestId, decision) {
1308
952
  const pending = this.#pending.get(requestId);
1309
953
  if (!pending) return false;
@@ -1313,24 +957,6 @@ var SessionRunner = class {
1313
957
  async interrupt() {
1314
958
  await this.#query?.interrupt();
1315
959
  }
1316
- /**
1317
- * Reset the conversation by sending the `/clear` the CLI already honors.
1318
- *
1319
- * Deliberately not a second mechanism: this engine's reset arrives *from the
1320
- * SDK*, and `normalizeSdkMessage` turns the CLI's report of it into the
1321
- * `conversation_reset` event (adopting the new conversation id and re-polling
1322
- * context usage on the way through). Reimplementing the clear here would give
1323
- * one engine two ways to reach the same state, and only one of them would get
1324
- * the id adoption right. So the command and the composer's `/clear` are one
1325
- * behaviour, and this method is the thin end of it.
1326
- *
1327
- * The one place it differs from the other two engines: this resolves when the
1328
- * `/clear` has been **handed to the CLI**, not when the reset has happened —
1329
- * the CLI queues its own streamed input, so waiting is its job, and there is
1330
- * no chain here to ride. The observable contract is the same (a clear sent
1331
- * mid-turn queues rather than cutting the turn short); only the moment the
1332
- * promise settles is weaker, and no caller depends on it.
1333
- */
1334
960
  async clearContext() {
1335
961
  if (this.#status === "closed" || this.#status === "failed") throw new Error("session is closed");
1336
962
  this.sendMessage("/clear");
@@ -1343,7 +969,6 @@ var SessionRunner = class {
1343
969
  mode
1344
970
  });
1345
971
  }
1346
- /** Switch the model for subsequent responses; undefined = back to the default. */
1347
972
  async setModel(model) {
1348
973
  await this.#query?.setModel(model);
1349
974
  this.#model = model;
@@ -1352,7 +977,6 @@ var SessionRunner = class {
1352
977
  model
1353
978
  });
1354
979
  }
1355
- /** Emit a session_error and terminate. For host-enforced policy (e.g. requireApiKey). */
1356
980
  fail(message) {
1357
981
  if (this.#closed) return;
1358
982
  this.#emit({
@@ -1362,7 +986,6 @@ var SessionRunner = class {
1362
986
  this.#setStatus("failed");
1363
987
  this.close("error");
1364
988
  }
1365
- /** Terminate the session and the underlying CLI subprocess. */
1366
989
  close(reason = "client") {
1367
990
  if (this.#closed) return;
1368
991
  this.#closed = true;
@@ -1378,27 +1001,11 @@ var SessionRunner = class {
1378
1001
  });
1379
1002
  this.#setStatus("closed");
1380
1003
  }
1381
- /** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
1382
- * "show everything" on one row, so a per-runner seq index would be a map
1383
- * maintained on every emit to save a walk nobody makes twice a minute. */
1384
1004
  eventAt(seq) {
1385
- return this.#events.find((event) => event.seq === seq);
1005
+ return this.#log.at(seq);
1386
1006
  }
1387
- /**
1388
- * Replay buffered events with seq > afterSeq, then deliver live events.
1389
- * Returns an unsubscribe function.
1390
- *
1391
- * Replay honours the reset watermark: transcript content below the latest
1392
- * `conversation_reset` is skipped (the reducer would clear it again anyway,
1393
- * and a pre-reset client that never learned the reducer's case would render
1394
- * a conversation the engine has discarded), while state-bearing events —
1395
- * which are emitted once and never again — always replay. The reset event
1396
- * itself replays (the skip is strictly-below), which is what clears a
1397
- * reconnecting client still holding pre-reset rows; superseded resets are
1398
- * content below the newer one and are skipped with what they cleared.
1399
- */
1400
1007
  subscribe(listener, afterSeq = 0, options) {
1401
- return this.#subscribers.subscribe(this.#events, listener, afterSeq, options, this.#resetSeq);
1008
+ return this.#subscribers.subscribe(this.#log.events, listener, afterSeq, options, this.#log.resetSeq);
1402
1009
  }
1403
1010
  async #run() {
1404
1011
  const queryFn = this.#config.queryFn ?? query;
@@ -1436,12 +1043,6 @@ var SessionRunner = class {
1436
1043
  }
1437
1044
  }
1438
1045
  }
1439
- /**
1440
- * On resume, emit the prior session's transcript as replay events (seq'd before any
1441
- * live event). The SDK only re-streams *user* messages on resume; assistant history
1442
- * would otherwise be lost to clients attaching after a server restart. Duplicated
1443
- * user messages are deduped client-side by uuid.
1444
- */
1445
1046
  async #backfillHistory() {
1446
1047
  const c = this.#config;
1447
1048
  if (!c.resume || c.backfillHistory === false) return;
@@ -1552,11 +1153,6 @@ var SessionRunner = class {
1552
1153
  }
1553
1154
  }
1554
1155
  }
1555
- /** Ask the CLI what models/commands it supports and surface them as an event
1556
- * (replayed to late attachers). Called eagerly for promptless sessions and again
1557
- * on init — the flag keeps it a single emit. Optional-chained: injected fake
1558
- * queries in tests may not implement these, and a failure must not affect the
1559
- * session. */
1560
1156
  async #fetchCapabilities() {
1561
1157
  if (this.#capabilitiesEmitted) return;
1562
1158
  const query = this.#query;
@@ -1578,33 +1174,8 @@ var SessionRunner = class {
1578
1174
  });
1579
1175
  } catch {}
1580
1176
  }
1581
- /**
1582
- * Adopt the title the CLI gave this thread — the "friendly title" it writes a
1583
- * turn or two into a session, and the name a resumed thread already carries.
1584
- *
1585
- * A **poll, not an observation**, and unavoidably so: no member of the SDK's
1586
- * `SDKMessage` union carries it (the whole union was checked). It lives on
1587
- * `SDKSessionInfo`, which only `getSessionInfo` / `listSessions` return — the
1588
- * same record `GET /sdk-sessions` already serves as `SdkSessionSummary`. So it
1589
- * is read at init and after each turn, which is also roughly the rate at which
1590
- * it changes.
1591
- *
1592
- * Two rules:
1593
- * - **Never while `meta.title` is set.** A rename is a person's decision and a
1594
- * generated summary must not overwrite it. Not read at all in that case, so
1595
- * there is no stored value waiting to resurface if the rename is cleared —
1596
- * the next turn simply fetches it again.
1597
- * - `summary` falls back to the first prompt when the session has no real
1598
- * title yet, so it is taken only when it *differs* from `firstPrompt`.
1599
- * Otherwise `#title()`'s own prompt fallback covers it, and the two would
1600
- * disagree only in how they truncate.
1601
- *
1602
- * Best-effort throughout: an unreadable transcript, a session file that is not
1603
- * there yet, an SDK without the function — all leave the title as it was.
1604
- */
1605
1177
  async #fetchEngineTitle() {
1606
- const metaTitle = this.#config.meta?.title;
1607
- if (typeof metaTitle === "string" && metaTitle.length > 0) return;
1178
+ if (hostTitle(this.#config.meta)) return;
1608
1179
  const sdkSessionId = this.#sdkSessionId;
1609
1180
  if (!sdkSessionId) return;
1610
1181
  const read = this.#config.sessionInfoFn ?? getSessionInfo;
@@ -1616,8 +1187,6 @@ var SessionRunner = class {
1616
1187
  if (title) this.#engineTitle = title;
1617
1188
  } catch {}
1618
1189
  }
1619
- /** Snapshot the context window after a turn and surface it as an event. Optional-chained
1620
- * and best-effort for the same reasons as #fetchCapabilities. */
1621
1190
  async #fetchContextUsage() {
1622
1191
  const query = this.#query;
1623
1192
  if (typeof query?.getContextUsage !== "function") return;
@@ -1640,17 +1209,6 @@ var SessionRunner = class {
1640
1209
  });
1641
1210
  } catch {}
1642
1211
  }
1643
- /**
1644
- * Snapshot the plan's rate-limit windows and surface them as `rate_limit`
1645
- * events — the same event a live `rate_limit_event` produces, so clients need
1646
- * nothing new to render it.
1647
- *
1648
- * The CLI only *pushes* a window when it changes, which for a session being
1649
- * watched rather than driven can be never; polling is what makes usage show up
1650
- * at all. The control request is marked experimental in the SDK, name included,
1651
- * so it is probed for by name and every failure is silent — one more reason
1652
- * this can only ever be decoration.
1653
- */
1654
1212
  async #fetchRateLimits() {
1655
1213
  const query = this.#query;
1656
1214
  const fetchUsage = query?.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
@@ -1716,10 +1274,6 @@ var SessionRunner = class {
1716
1274
  this.#setStatus("awaiting_approval");
1717
1275
  });
1718
1276
  };
1719
- /** 'auto'/'deny' sessions settle AskUserQuestion synchronously instead of pending:
1720
- * 'auto' picks each question's first (recommended) option, 'deny' sends the model
1721
- * back to decide for itself. Request/resolved events still fire so transcripts and
1722
- * job webhooks show what was chosen. */
1723
1277
  #resolveQuestionByPolicy(request, mode) {
1724
1278
  this.#emit({
1725
1279
  type: "permission_requested",
@@ -1795,26 +1349,11 @@ var SessionRunner = class {
1795
1349
  });
1796
1350
  }
1797
1351
  #emit(body) {
1798
- const event = {
1799
- ...body,
1800
- seq: ++this.#seq,
1801
- ts: Date.now()
1802
- };
1803
- this.#lastActivityAt = event.ts;
1804
- this.#activityCount += transcriptActivity(body);
1805
- this.#contextUsage = contextReading(body) ?? this.#contextUsage;
1806
- if (body.type === "conversation_reset") {
1807
- this.#resetSeq = event.seq;
1808
- this.#contextUsage = void 0;
1809
- }
1352
+ const event = this.#log.append(body);
1810
1353
  this.#subagents.observe(body, event.ts);
1811
- this.#events.push(event);
1812
1354
  this.#subscribers.emit(event);
1813
1355
  }
1814
1356
  };
1815
- /** Answer each AskUserQuestion question with its first option's label — the tool's
1816
- * convention puts the recommended choice first. Keyed by question text, the shape the
1817
- * CLI expects back in `updatedInput.answers`. */
1818
1357
  function recommendedAnswers(input) {
1819
1358
  const answers = {};
1820
1359
  const questions = Array.isArray(input.questions) ? input.questions : [];
@@ -1828,60 +1367,26 @@ function recommendedAnswers(input) {
1828
1367
  }
1829
1368
  //#endregion
1830
1369
  //#region src/engines/provider/runner.ts
1831
- /** Permission modes this engine can honor. The rest of the protocol vocabulary
1832
- * (acceptEdits/plan/auto) is Claude Code CLI semantics with no meaning here —
1833
- * setPermissionMode rejects them, which the server surfaces as protocol_error. */
1834
1370
  const SUPPORTED_PERMISSION_MODES = [
1835
1371
  "default",
1836
1372
  "bypassPermissions",
1837
1373
  "dontAsk"
1838
1374
  ];
1839
- /**
1840
- * Model-agnostic runner over the AI SDK v7 ToolLoopAgent. The session's durable
1841
- * state is its ModelMessage history: every turn — including continuation after an
1842
- * externally-executed tool call — is a fresh streamed call over that history
1843
- * (message-state replay; the loop cannot be suspended). Output is emitted as it
1844
- * happens: `stream_delta` per token (unless includePartialMessages is false) and
1845
- * assistant/tool messages per step. Emits the same seq-numbered SessionEvent log
1846
- * as SessionRunner; engine-specific CLI telemetry (system_init, capabilities,
1847
- * rate_limit, ...) is simply never emitted.
1848
- */
1849
1375
  var AiSdkRunner = class {
1850
1376
  id;
1851
1377
  createdAt;
1852
1378
  #config;
1853
1379
  #model;
1854
- #events = [];
1380
+ #log = new EventLog();
1855
1381
  #subscribers = new SubscriberSet();
1856
- #seq = 0;
1857
- /**
1858
- * Latest context-window reading, retained from the last `context_usage` this
1859
- * runner emitted so `GET /sessions` can answer it without an attach — see
1860
- * `SessionInfo.contextUsage`. Folded in the emit path, so it is by
1861
- * construction the same number the transcript last drew.
1862
- */
1863
- #contextUsage;
1864
- #activityCount = 0;
1865
- /**
1866
- * Seq of the latest `conversation_reset` event, 0 when none. The log itself is
1867
- * never truncated — it still carries the state-bearing events (`capabilities`,
1868
- * `system_init`, …) a fresh attacher depends on and which are not re-emitted —
1869
- * but `subscribe()` skips transcript *content* strictly below this mark, so a
1870
- * replay does not resurrect a cleared conversation. A later reset supersedes
1871
- * an earlier one by overwriting it.
1872
- */
1873
- #resetSeq = 0;
1874
1382
  #status = "starting";
1383
+ #statusDetail;
1875
1384
  #permissionMode;
1876
1385
  #messages = [];
1877
1386
  #pendingToolCalls = /* @__PURE__ */ new Map();
1878
- /** Calls already handed to the executor, so a re-park never double-dispatches. */
1879
1387
  #dispatched = /* @__PURE__ */ new Set();
1880
1388
  #turnChain = Promise.resolve();
1881
1389
  #abort;
1882
- /** Accumulates across every leg of one turn. A turn that parks on external
1883
- * tool calls spans several generate() calls; usage and elapsed time must
1884
- * cover all of them, not just the leg that happens to finish. */
1885
1390
  #turnAccum;
1886
1391
  #numTurns = 0;
1887
1392
  #totalUsage = {
@@ -1890,16 +1395,10 @@ var AiSdkRunner = class {
1890
1395
  cacheWrite: 0,
1891
1396
  cacheRead: 0
1892
1397
  };
1893
- #lastActivityAt;
1894
1398
  #started = false;
1895
1399
  #closed = false;
1896
- /** Parked: state has been snapshotted and this instance is inert. Not closed —
1897
- * the session lives on in the snapshot and resumes as a new instance. */
1898
1400
  #parked = false;
1899
- /** Model alias as requested (not the resolved provider id) — what set_model was
1900
- * given, so a rehydrated session can re-resolve the same choice. */
1901
1401
  #modelAlias;
1902
- /** Permission prompts awaiting a client decision. Keyed by request id. */
1903
1402
  #pendingApprovals = /* @__PURE__ */ new Map();
1904
1403
  constructor(config, id = randomUUID()) {
1905
1404
  const mode = config.permissionMode ?? "default";
@@ -1912,23 +1411,11 @@ var AiSdkRunner = class {
1912
1411
  this.createdAt = config.restore?.createdAt ?? Date.now();
1913
1412
  if (config.restore) this.#restore(config.restore);
1914
1413
  }
1915
- /** Adopt a parked session's state. The event log and seq counter come back
1916
- * verbatim: a client reattaching with `afterSeq` must see one unbroken stream
1917
- * across the teardown, not a second session that restarts at 1. */
1918
1414
  #restore(snapshot) {
1919
1415
  if (snapshot.engine !== "provider") throw new Error(`cannot restore a '${snapshot.engine}' snapshot into the AI SDK engine`);
1920
1416
  const state = snapshot.state;
1921
1417
  if (!state || !Array.isArray(state.messages)) throw new Error("session snapshot is missing its provider-engine state");
1922
- this.#seq = snapshot.seq;
1923
- this.#events = [...snapshot.events];
1924
- this.#activityCount = 0;
1925
- for (const event of this.#events) {
1926
- this.#activityCount += transcriptActivity(event);
1927
- if (event.type === "conversation_reset") {
1928
- this.#resetSeq = event.seq;
1929
- this.#contextUsage = void 0;
1930
- } else this.#contextUsage = contextReading(event) ?? this.#contextUsage;
1931
- }
1418
+ this.#log.restore(snapshot.events, snapshot.seq, state.lastActivityAt);
1932
1419
  this.#messages = [...state.messages];
1933
1420
  for (const call of state.pendingToolCalls) this.#pendingToolCalls.set(call.toolCallId, call);
1934
1421
  this.#dispatched = new Set(state.dispatched);
@@ -1937,7 +1424,6 @@ var AiSdkRunner = class {
1937
1424
  this.#turnAccum = state.turnAccum ? { ...state.turnAccum } : void 0;
1938
1425
  if (this.#turnAccum && state.parkedAt !== void 0) this.#turnAccum.startedAt += Date.now() - state.parkedAt;
1939
1426
  this.#permissionMode = state.permissionMode;
1940
- this.#lastActivityAt = state.lastActivityAt;
1941
1427
  this.#status = this.#pendingToolCalls.size > 0 ? "parked" : "idle";
1942
1428
  if (state.model !== void 0 && state.model !== this.#modelAlias && this.#config.resolveModel) {
1943
1429
  this.#modelAlias = state.model;
@@ -1948,21 +1434,17 @@ var AiSdkRunner = class {
1948
1434
  return this.#status;
1949
1435
  }
1950
1436
  get lastSeq() {
1951
- return this.#seq;
1437
+ return this.#log.seq;
1952
1438
  }
1953
- /** The session's durable state — persist to park, replay to rehydrate. */
1954
1439
  get messages() {
1955
1440
  return [...this.#messages];
1956
1441
  }
1957
- /** External tool calls the loop is currently parked on. */
1958
1442
  get pendingToolCalls() {
1959
1443
  return [...this.#pendingToolCalls.values()];
1960
1444
  }
1961
1445
  get pendingApprovals() {
1962
1446
  return [...this.#pendingApprovals.values()].map((a) => a.request);
1963
1447
  }
1964
- /** The session's scratch filesystem (see Runner.vfs) — the server's file
1965
- * routes serve deliverables straight from it. */
1966
1448
  get vfs() {
1967
1449
  return this.#config.vfs;
1968
1450
  }
@@ -1980,15 +1462,16 @@ var AiSdkRunner = class {
1980
1462
  model: this.#modelId(),
1981
1463
  permissionMode: this.#permissionMode,
1982
1464
  createdAt: this.createdAt,
1983
- lastSeq: this.#seq,
1984
- activityCount: this.#activityCount,
1985
- contextUsage: this.#contextUsage,
1465
+ lastSeq: this.#log.seq,
1466
+ activityCount: this.#log.activityCount,
1467
+ proseCount: this.#log.proseCount,
1468
+ contextUsage: this.#log.contextUsage,
1986
1469
  pendingPermissionCount: this.#pendingApprovals.size,
1987
1470
  meta: this.#config.meta,
1988
1471
  scope: this.#config.scope,
1989
- title: this.#title(),
1472
+ title: sessionTitle(this.#config),
1990
1473
  numTurns: this.#numTurns || void 0,
1991
- lastActivityAt: this.#lastActivityAt
1474
+ lastActivityAt: this.#log.lastActivityAt
1992
1475
  };
1993
1476
  }
1994
1477
  start() {
@@ -1999,15 +1482,9 @@ var AiSdkRunner = class {
1999
1482
  if (this.#config.prompt) this.sendMessage(this.#config.prompt);
2000
1483
  return this.#turnChain;
2001
1484
  }
2002
- /**
2003
- * Snapshot durable state, release engine resources, and go inert — the session
2004
- * continues in the snapshot, not in this object. Returns undefined when parking
2005
- * would lose work or has nothing to wait for: a turn in flight, no parked call,
2006
- * or an already-closed/parked runner.
2007
- */
2008
1485
  park() {
2009
- if (this.#closed || this.#parked) return void 0;
2010
- if (this.#abort || !this.#restingOnDeferred()) return void 0;
1486
+ if (this.#closed || this.#parked) return;
1487
+ if (this.#abort || !this.#restingOnDeferred()) return;
2011
1488
  this.#setStatus("parked");
2012
1489
  const snapshot = this.#buildSnapshot();
2013
1490
  this.#parked = true;
@@ -2017,52 +1494,11 @@ var AiSdkRunner = class {
2017
1494
  } catch {}
2018
1495
  return snapshot;
2019
1496
  }
2020
- /**
2021
- * The same snapshot, taken without ending anything.
2022
- *
2023
- * `park()` and this are two operations that happen to produce the same value,
2024
- * and the difference is the whole point: `park()` *ends* the live runner
2025
- * (inert, listeners dropped, `onClose` called), which is right for deferred
2026
- * execution — the session has nothing to do for possibly days — and wrong for
2027
- * restart-survival, where the session is active and someone is mid-
2028
- * conversation. This one changes nothing at all: no status emit, no listener
2029
- * clear, no disposer. The host writes the value through to durable storage
2030
- * after each turn and keeps the runner live and warm, so a restart rebuilds
2031
- * from the last write through the existing `restore` path and the next message
2032
- * costs no wake.
2033
- *
2034
- * The gate is `park()`'s minus the requirement that there be something parked:
2035
- *
2036
- * - `#abort` set is refused for the reason it always was — a `generate()` in
2037
- * flight has produced messages that are not in the history yet, so the
2038
- * snapshot would be of a turn that half-happened.
2039
- * - Pending calls that are **not** all deferred are refused, which is
2040
- * `park()`'s rule wearing a different hat. An in-process execution's result
2041
- * is coming back to *this* runner and dies with the process; a restore would
2042
- * wait on it forever, and `state.dispatched` is what would stop the rebuilt
2043
- * runner from simply calling it again.
2044
- * - Idle with nothing pending — the case `park()` exists to refuse — is
2045
- * exactly the case this exists to allow.
2046
- */
2047
1497
  snapshot() {
2048
- if (this.#closed || this.#parked || this.#abort) return void 0;
2049
- if (this.#pendingToolCalls.size > 0 && !this.#restingOnDeferred()) return void 0;
1498
+ if (this.#closed || this.#parked || this.#abort) return;
1499
+ if (this.#pendingToolCalls.size > 0 && !this.#restingOnDeferred()) return;
2050
1500
  return this.#buildSnapshot();
2051
1501
  }
2052
- /**
2053
- * The snapshot value itself, shared so a park and a write-through cannot
2054
- * disagree about what a session *is*.
2055
- *
2056
- * The event log is filtered through {@link snapshotRetains} — the persisted
2057
- * log drops stream deltas, which are superseded by the `assistant_message`
2058
- * that flushes them and would otherwise be tens of times the size of the text
2059
- * they spell. Parks get it too, and should: a park sits on disk for days.
2060
- *
2061
- * The `parked` list and `state.parkedAt` are honest under both callers. An
2062
- * idle write-through has no pending calls, so `parked` is empty and the host
2063
- * arms no watchdogs; `parkedAt` is "when this was taken", which is what
2064
- * `#restore` needs to discount a turn's clock either way.
2065
- */
2066
1502
  #buildSnapshot() {
2067
1503
  const parked = [...this.#pendingToolCalls.values()].map((call) => ({
2068
1504
  executionId: call.toolCallId,
@@ -2078,15 +1514,15 @@ var AiSdkRunner = class {
2078
1514
  turnAccum: this.#turnAccum ? { ...this.#turnAccum } : void 0,
2079
1515
  permissionMode: this.#permissionMode,
2080
1516
  model: this.#modelAlias,
2081
- lastActivityAt: this.#lastActivityAt,
1517
+ lastActivityAt: this.#log.lastActivityAt,
2082
1518
  parkedAt: Date.now()
2083
1519
  };
2084
1520
  return {
2085
1521
  engine: "provider",
2086
1522
  id: this.id,
2087
1523
  createdAt: this.createdAt,
2088
- seq: this.#seq,
2089
- events: this.#events.filter((event) => snapshotRetains(event)),
1524
+ seq: this.#log.seq,
1525
+ events: this.#log.events.filter((event) => snapshotRetains(event)),
2090
1526
  vfs: this.#config.vfs?.snapshot(),
2091
1527
  parked,
2092
1528
  state
@@ -2120,19 +1556,11 @@ var AiSdkRunner = class {
2120
1556
  });
2121
1557
  this.#scheduleTurn();
2122
1558
  }
2123
- /**
2124
- * Deliver the result of an external (execute-less) tool call. Appends the
2125
- * tool-result message and, once no calls remain pending, re-enters the loop.
2126
- * Idempotent per toolCallId: unknown/already-settled ids return false.
2127
- */
2128
1559
  resolveToolCall(toolCallId, output, options) {
2129
1560
  if (!this.#settlePendingCall(toolCallId, output, options?.isError === true)) return false;
2130
1561
  if (this.#pendingToolCalls.size === 0) this.#scheduleTurn();
2131
1562
  return true;
2132
1563
  }
2133
- /** Record a parked call's outcome into the message history (so it stays
2134
- * replayable — a dangling tool call without a result is invalid input for
2135
- * providers) and the event log. Does NOT re-enter the loop. */
2136
1564
  #settlePendingCall(toolCallId, output, isError) {
2137
1565
  const pending = this.#pendingToolCalls.get(toolCallId);
2138
1566
  if (!pending || this.#closed || this.#parked) return false;
@@ -2198,11 +1626,8 @@ var AiSdkRunner = class {
2198
1626
  });
2199
1627
  if (decision.interrupt) this.interrupt();
2200
1628
  }
2201
- if (this.#pendingApprovals.size === 0 && this.#pendingToolCalls.size === 0) {}
2202
1629
  return true;
2203
1630
  }
2204
- /** Emit `file_delivered` — the deliver_file tool's hand-over event (wired by
2205
- * createEngineSession via ToolContextOptions.onFileDelivered). */
2206
1631
  emitFileDelivered(file) {
2207
1632
  if (this.#closed || this.#parked) return;
2208
1633
  this.#emit({
@@ -2210,11 +1635,6 @@ var AiSdkRunner = class {
2210
1635
  ...file
2211
1636
  });
2212
1637
  }
2213
- /**
2214
- * One plain generateText over the session's current model, billed into the
2215
- * running turn's usage accumulator — the web_fetch digest pass uses this so
2216
- * its tokens are never lost from the turn's accounting.
2217
- */
2218
1638
  async generateDigest(prompt) {
2219
1639
  const result = await generateText({
2220
1640
  model: this.#model,
@@ -2230,19 +1650,6 @@ var AiSdkRunner = class {
2230
1650
  }
2231
1651
  return result.text;
2232
1652
  }
2233
- /**
2234
- * Reset the conversation: drop the message array the next turn would have
2235
- * been built from. There is no engine round trip — this runner *is* where the
2236
- * transcript lives, so clearing it is the whole operation.
2237
- *
2238
- * Two things ride along, both already written elsewhere and both load-bearing
2239
- * here. `#emit`'s `conversation_reset` arm retires `#contextUsage` (the
2240
- * reading described a conversation that no longer exists), and the same arm
2241
- * in `restore` keeps a parked session that comes back after a clear from
2242
- * resurrecting it. Pending tool calls are NOT swept: a parked call is work a
2243
- * backend still owes an answer for, and a clear is not an interrupt — the
2244
- * refusal below is what keeps the two apart.
2245
- */
2246
1653
  async clearContext() {
2247
1654
  if (this.#status === "closed" || this.#status === "failed") throw new Error("session is closed");
2248
1655
  const run = this.#turnChain.then(() => {
@@ -2329,31 +1736,21 @@ var AiSdkRunner = class {
2329
1736
  Promise.resolve(this.#config.onClose?.()).catch(() => {});
2330
1737
  } catch {}
2331
1738
  }
2332
- /** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
2333
- * "show everything" on one row, so a per-runner seq index would be a map
2334
- * maintained on every emit to save a walk nobody makes twice a minute. */
2335
1739
  eventAt(seq) {
2336
- return this.#events.find((event) => event.seq === seq);
1740
+ return this.#log.at(seq);
2337
1741
  }
2338
1742
  subscribe(listener, afterSeq = 0, options) {
2339
- return this.#subscribers.subscribe(this.#events, listener, afterSeq, options, this.#resetSeq);
1743
+ return this.#subscribers.subscribe(this.#log.events, listener, afterSeq, options, this.#log.resetSeq);
2340
1744
  }
2341
1745
  #scheduleTurn() {
2342
1746
  this.#turnChain = this.#turnChain.then(() => this.#runTurn());
2343
1747
  }
2344
- /**
2345
- * Deliver the result of an execution this runner dispatched. Used by the host
2346
- * when a backend settled out-of-band (a browser bridge answering later, a
2347
- * deferred executor). Idempotent by executionId.
2348
- */
2349
1748
  settleExecution(executionId, result) {
2350
1749
  if (this.#closed || this.#parked) return false;
2351
1750
  if (!this.#pendingToolCalls.has(executionId)) return false;
2352
1751
  this.#applyExecutionResult(executionId, result);
2353
1752
  return true;
2354
1753
  }
2355
- /** Hand every parked call the executor owns to it, gating on approval when
2356
- * the permission mode requires it. */
2357
1754
  #dispatchPending() {
2358
1755
  const executor = this.#config.executor;
2359
1756
  if (!executor) return;
@@ -2411,7 +1808,6 @@ var AiSdkRunner = class {
2411
1808
  if (anyAwaiting) this.#setStatus("awaiting_approval");
2412
1809
  if (anyDeferred) Promise.allSettled(inFlight).then(() => this.#announceParked());
2413
1810
  }
2414
- /** Dispatch a single tool call that was held behind an approval gate. */
2415
1811
  #dispatchSingle(toolCallId) {
2416
1812
  const executor = this.#config.executor;
2417
1813
  if (!executor) return;
@@ -2421,7 +1817,6 @@ var AiSdkRunner = class {
2421
1817
  const dispatched = this.#dispatchCall(executor, call);
2422
1818
  if (dispatched.deferred) dispatched.promise.then(() => this.#announceParked());
2423
1819
  }
2424
- /** The actual dispatch + event emission for one tool call. */
2425
1820
  #dispatchCall(executor, call) {
2426
1821
  const toolCall = {
2427
1822
  executionId: call.toolCallId,
@@ -2457,25 +1852,15 @@ var AiSdkRunner = class {
2457
1852
  promise
2458
1853
  };
2459
1854
  }
2460
- /**
2461
- * The turn has come to rest on deferred executions: nothing is in flight, and
2462
- * only a host-delivered result can move it. `status_changed: 'parked'` is the
2463
- * host's cue to snapshot via {@link park} — a single, correctly-timed signal
2464
- * rather than an inference from individual dispatch events.
2465
- */
2466
1855
  #announceParked() {
2467
1856
  if (this.#closed || this.#parked || this.#abort) return;
2468
1857
  if (this.#restingOnDeferred()) this.#setStatus("parked");
2469
1858
  }
2470
- /** The loop is waiting, and everything it waits on can only be answered from
2471
- * outside this process. One still-live in-process execution means a result is
2472
- * coming back to THIS runner, and tearing it down would strand it. */
2473
1859
  #restingOnDeferred() {
2474
1860
  if (this.#pendingToolCalls.size === 0) return false;
2475
1861
  for (const call of this.#pendingToolCalls.values()) if (call.deferred !== true) return false;
2476
1862
  return true;
2477
1863
  }
2478
- /** Fold an execution's outcome back into the loop, whichever way it went. */
2479
1864
  #applyExecutionResult(executionId, result) {
2480
1865
  if (this.#closed || this.#parked) return;
2481
1866
  this.#dispatched.delete(executionId);
@@ -2637,10 +2022,7 @@ var AiSdkRunner = class {
2637
2022
  case "finish-step":
2638
2023
  flush();
2639
2024
  break;
2640
- case "error":
2641
- streamError ??= part.error;
2642
- break;
2643
- default: break;
2025
+ case "error": streamError ??= part.error;
2644
2026
  }
2645
2027
  }
2646
2028
  flush();
@@ -2705,9 +2087,6 @@ var AiSdkRunner = class {
2705
2087
  if (this.#abort === abort) this.#abort = void 0;
2706
2088
  }
2707
2089
  }
2708
- /** Emit the turn's result from the whole-turn accumulator, so a turn that
2709
- * parked on external tool calls reports every leg's tokens and the full
2710
- * elapsed time (including the time spent executing those tools). */
2711
2090
  #finishTurn(text) {
2712
2091
  const accum = this.#turnAccum ?? {
2713
2092
  startedAt: Date.now(),
@@ -2739,39 +2118,17 @@ var AiSdkRunner = class {
2739
2118
  if (typeof model === "string") return model;
2740
2119
  return model.modelId;
2741
2120
  }
2742
- #title() {
2743
- const metaTitle = this.#config.meta?.title;
2744
- if (typeof metaTitle === "string" && metaTitle.length > 0) return metaTitle;
2745
- const prompt = this.#config.prompt;
2746
- if (!prompt) return void 0;
2747
- return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
2748
- }
2749
- /**
2750
- * This session's MCP servers, as the host assembled them.
2751
- *
2752
- * Always answers — an empty list when no MCP was wired — because the
2753
- * alternative (undefined, which the server turns into a 501) says "this
2754
- * engine cannot tell you", and this engine can: the host that built the
2755
- * session is the only party who knows, and it has been asked.
2756
- */
2757
2121
  async mcpServers() {
2758
2122
  return await this.#config.reportMcpServers?.() ?? [];
2759
2123
  }
2760
- /** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
2761
- * it (undefined) restores the derived title. The engine is never told. */
2762
2124
  setTitle(title) {
2763
- const meta = { ...this.#config.meta };
2764
- if (title) meta.title = title;
2765
- else delete meta.title;
2766
- this.#config = {
2767
- ...this.#config,
2768
- meta
2769
- };
2125
+ this.#config = withTitle(this.#config, title);
2770
2126
  }
2771
2127
  #setStatus(status, detail) {
2772
- if (this.#status === status) return;
2128
+ if (this.#status === status && this.#statusDetail === detail) return;
2773
2129
  if (this.#status === "closed" || this.#status === "failed") return;
2774
2130
  this.#status = status;
2131
+ this.#statusDetail = detail;
2775
2132
  this.#emit({
2776
2133
  type: "status_changed",
2777
2134
  status,
@@ -2779,20 +2136,7 @@ var AiSdkRunner = class {
2779
2136
  });
2780
2137
  }
2781
2138
  #emit(body) {
2782
- const event = {
2783
- ...body,
2784
- seq: ++this.#seq,
2785
- ts: Date.now()
2786
- };
2787
- this.#lastActivityAt = event.ts;
2788
- this.#activityCount += transcriptActivity(body);
2789
- this.#contextUsage = contextReading(body) ?? this.#contextUsage;
2790
- if (body.type === "conversation_reset") {
2791
- this.#resetSeq = event.seq;
2792
- this.#contextUsage = void 0;
2793
- }
2794
- this.#events.push(event);
2795
- this.#subscribers.emit(event);
2139
+ this.#subscribers.emit(this.#log.append(body));
2796
2140
  }
2797
2141
  };
2798
2142
  function turnUsage(accum) {
@@ -2811,19 +2155,10 @@ function errorText(error) {
2811
2155
  }
2812
2156
  //#endregion
2813
2157
  //#region src/engines/claude/auth.ts
2814
- /**
2815
- * The native Claude Code binary the Agent SDK itself spawns, resolved the way
2816
- * the SDK resolves it: the platform-specific optional dependency installed next
2817
- * to the SDK package (`@anthropic-ai/claude-agent-sdk-<platform>-<arch>/claude`).
2818
- * Probing this binary rather than whatever `claude` is on PATH means an auth
2819
- * check answers for the executable sessions will actually run — the two can be
2820
- * different versions logged into different places. Returns undefined when it
2821
- * can't be found (optional dep skipped, unsupported platform); callers degrade
2822
- * to 'unknown', and the SDK surfaces its own error if a session is created.
2823
- */
2824
2158
  function resolveBundledClaudeExecutable() {
2825
2159
  try {
2826
- const fromSdk = createRequire(createRequire(import.meta.url).resolve("@anthropic-ai/claude-agent-sdk"));
2160
+ const fromHere = createRequire(import.meta.url);
2161
+ const fromSdk = createRequire(fromHere.resolve("@anthropic-ai/claude-agent-sdk"));
2827
2162
  const suffix = process.platform === "win32" ? ".exe" : "";
2828
2163
  const platforms = process.platform === "linux" ? [`linux-${process.arch}`, `linux-${process.arch}-musl`] : [`${process.platform}-${process.arch}`];
2829
2164
  for (const platform of platforms) try {
@@ -2832,18 +2167,6 @@ function resolveBundledClaudeExecutable() {
2832
2167
  } catch {}
2833
2168
  } catch {}
2834
2169
  }
2835
- /**
2836
- * Ask the CLI whether `env` holds usable credentials: `claude auth status`
2837
- * prints a JSON verdict covering every source the CLI itself consults for that
2838
- * environment — `<CLAUDE_CONFIG_DIR>/.credentials.json`, the macOS login
2839
- * Keychain, ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, Bedrock/Vertex
2840
- * (verified against 2.1.217). Only the `loggedIn` boolean is ever read; the
2841
- * identity fields in the payload (email, org, subscription) never leave the
2842
- * parse. The exit code is deliberately ignored — 2.1.217 exits 1 on a
2843
- * logged-out verdict where other versions exit 0 — and anything that doesn't
2844
- * parse to a `loggedIn` boolean is 'unknown', because `auth status` is not a
2845
- * stable contract. Never rejects.
2846
- */
2847
2170
  function checkClaudeAuth(env, options = {}) {
2848
2171
  const executable = options.executable ?? resolveBundledClaudeExecutable();
2849
2172
  if (!executable) return Promise.resolve("unknown");
@@ -2865,11 +2188,6 @@ function checkClaudeAuth(env, options = {}) {
2865
2188
  }
2866
2189
  //#endregion
2867
2190
  //#region src/executors/quickjs-executor.ts
2868
- /**
2869
- * In-process execution backend: runs a tool's untrusted script in the QuickJS
2870
- * WASM guest. Always settles inline — nothing downstream assumes that, which is
2871
- * what lets a deferred backend replace it behind the same seam.
2872
- */
2873
2191
  var QuickJsExecutor = class {
2874
2192
  #options;
2875
2193
  constructor(options) {
@@ -2899,7 +2217,7 @@ var QuickJsExecutor = class {
2899
2217
  vfs: call.vfs,
2900
2218
  signal: call.signal,
2901
2219
  timeoutMs: call.limits?.timeoutMs ?? this.#options.defaultTimeoutMs ?? 5e3,
2902
- memoryLimitBytes: call.limits?.memoryLimitBytes ?? this.#options.defaultMemoryLimitBytes ?? 64 * 1024 * 1024,
2220
+ memoryLimitBytes: call.limits?.memoryLimitBytes ?? this.#options.defaultMemoryLimitBytes ?? 67108864,
2903
2221
  fetchText: this.#allowsNetwork() ? (url) => this.#fetchText(url, call.signal) : void 0
2904
2222
  });
2905
2223
  const logs = result.logs.map((l) => `[${l.level}] ${l.text}`);
@@ -2943,8 +2261,6 @@ function safeHost(url) {
2943
2261
  return;
2944
2262
  }
2945
2263
  }
2946
- /** Exact hostname match, or a single leading `*.` wildcard covering subdomains
2947
- * (not the bare parent). Only http(s) — no file:, data:, or other schemes. */
2948
2264
  function isHostAllowed(url, allowedHosts) {
2949
2265
  let parsed;
2950
2266
  try {
@@ -2968,14 +2284,6 @@ var PendingRequestRegistry = class {
2968
2284
  get size() {
2969
2285
  return this.#slots.size;
2970
2286
  }
2971
- /**
2972
- * Register a request and get a promise for its outcome. The promise **never
2973
- * rejects**: a timeout or cancellation resolves with `ok: false` so callers
2974
- * feed the failure back into the agent loop instead of unwinding it.
2975
- *
2976
- * Re-registering a live id throws — silently replacing it would strand the
2977
- * first waiter forever.
2978
- */
2979
2287
  register(options) {
2980
2288
  if (this.#slots.has(options.id)) throw new Error(`pending request '${options.id}' is already registered`);
2981
2289
  const entry = {
@@ -3007,8 +2315,6 @@ var PendingRequestRegistry = class {
3007
2315
  this.#slots.set(options.id, slot);
3008
2316
  });
3009
2317
  }
3010
- /** Deliver a result. Returns false for unknown or already-settled ids —
3011
- * duplicate and late deliveries are no-ops, never a second application. */
3012
2318
  settle(id, value, settledBy = "client") {
3013
2319
  return this.#settle(id, {
3014
2320
  ok: true,
@@ -3016,7 +2322,6 @@ var PendingRequestRegistry = class {
3016
2322
  settledBy
3017
2323
  });
3018
2324
  }
3019
- /** Fail a request. Same idempotence guarantee as {@link settle}. */
3020
2325
  fail(id, reason, error, settledBy = "server") {
3021
2326
  return this.#settle(id, {
3022
2327
  ok: false,
@@ -3036,7 +2341,6 @@ var PendingRequestRegistry = class {
3036
2341
  const entries = [...this.#slots.values()].map(toEntry);
3037
2342
  return kind ? entries.filter((e) => e.kind === kind) : entries;
3038
2343
  }
3039
- /** Fail everything (optionally of one kind) — session close, turn interrupt. */
3040
2344
  cancelAll(reason, error, kind) {
3041
2345
  let canceled = 0;
3042
2346
  for (const slot of Array.from(this.#slots.values())) {
@@ -3070,21 +2374,9 @@ function toEntry(slot) {
3070
2374
  }
3071
2375
  //#endregion
3072
2376
  //#region src/executors/browser-bridge-executor.ts
3073
- /**
3074
- * Executes tool calls in the attached client's own sandbox. The first backend
3075
- * that genuinely returns `pending`: dispatch puts a request on the wire and
3076
- * returns, and the result arrives later through {@link resolve}.
3077
- *
3078
- * Data locality is the point — documents can stay in the browser and never
3079
- * reach the server. The tradeoff is trust: whatever comes back is untrusted
3080
- * input, fine for the user's own data but never a source for authoritative
3081
- * server state (that is why MCP and secret-bearing tools are never bridged).
3082
- */
3083
2377
  var BrowserBridgeExecutor = class {
3084
2378
  registry;
3085
2379
  #options;
3086
- /** Results that arrive before dispatch registers them (fast client, slow
3087
- * bookkeeping) would otherwise be dropped — hold them briefly. */
3088
2380
  #early = /* @__PURE__ */ new Map();
3089
2381
  constructor(options) {
3090
2382
  this.#options = options;
@@ -3138,10 +2430,6 @@ var BrowserBridgeExecutor = class {
3138
2430
  status: "pending"
3139
2431
  };
3140
2432
  }
3141
- /**
3142
- * Apply a client's answer. Returns false when the id is unknown or already
3143
- * settled — a late result after a timeout must not re-open a settled call.
3144
- */
3145
2433
  resolve(executionId, answer) {
3146
2434
  if (!this.registry.has(executionId)) {
3147
2435
  this.#early.set(executionId, answer);
@@ -3154,13 +2442,12 @@ var BrowserBridgeExecutor = class {
3154
2442
  return "output" in answer ? this.registry.settle(executionId, answer, "client") : this.registry.fail(executionId, answer.reason, answer.error, "client");
3155
2443
  }
3156
2444
  };
3157
- /** Map a registry outcome onto the executor's result contract. */
3158
2445
  function toExecutionResult(outcome) {
3159
2446
  if (outcome.ok && "output" in outcome.value) {
3160
2447
  const { output, logs } = outcome.value;
3161
2448
  return {
3162
2449
  status: "ok",
3163
- output: output.type === "text" ? output.value : output.value,
2450
+ output: output.value,
3164
2451
  logs
3165
2452
  };
3166
2453
  }
@@ -3181,17 +2468,6 @@ function toExecutionResult(outcome) {
3181
2468
  }
3182
2469
  //#endregion
3183
2470
  //#region src/executors/deferred-executor.ts
3184
- /**
3185
- * The executor for work that outlives the session's process residency: dispatch
3186
- * hands the call off and returns `pending` **without holding a promise**, because
3187
- * the runner it would resolve into is about to be torn down. The result can only
3188
- * come back through the host — the execution-result route → `settleExecution` on a
3189
- * rehydrated runner — which is exactly what makes a park durable rather than a
3190
- * long in-memory await.
3191
- *
3192
- * Contrast {@link BrowserBridgeExecutor}, which is also `pending` but keeps its
3193
- * answer in memory for the ~60s the tab has to reply.
3194
- */
3195
2471
  var DeferredExecutor = class {
3196
2472
  backend;
3197
2473
  timeoutMs;
@@ -3201,8 +2477,6 @@ var DeferredExecutor = class {
3201
2477
  this.backend = options.backend ?? "remote";
3202
2478
  this.timeoutMs = options.timeoutMs;
3203
2479
  }
3204
- /** Every call this executor takes is deferred — route only the tools that
3205
- * belong on the remote side to it. */
3206
2480
  describe() {
3207
2481
  return {
3208
2482
  backend: this.backend,
@@ -3228,16 +2502,7 @@ var DeferredExecutor = class {
3228
2502
  };
3229
2503
  //#endregion
3230
2504
  //#region src/engines/provider/tools.ts
3231
- const MAX_FILE_BYTES = 1024 * 1024;
3232
- /**
3233
- * Build the capability-scoped tool set for a session.
3234
- *
3235
- * The agent's authority is exactly what is granted here — there are no built-in
3236
- * filesystem or shell tools, and nothing reaches the host filesystem: `fs_*`
3237
- * operate on an in-memory scratch VFS. Tools whose backend is not supplied are
3238
- * simply absent rather than present-and-failing, so a model cannot be tempted
3239
- * by a capability the operator did not grant.
3240
- */
2505
+ const MAX_FILE_BYTES = 1048576;
3241
2506
  function createToolContext(options) {
3242
2507
  const vfs = options.vfs ?? createVfs();
3243
2508
  const definitions = [];
@@ -3390,29 +2655,12 @@ function createToolContext(options) {
3390
2655
  sandboxedToolNames: definitions.filter((d) => d.trust === "sandboxed").map((d) => d.name)
3391
2656
  };
3392
2657
  }
3393
- /** Add host-side MCP tools to a context. They are ALWAYS authoritative: they run
3394
- * server-side with server credentials, and must never be handed to a browser. */
3395
2658
  function withMcpTools(context, mcpTools) {
3396
2659
  return withHostTools(context, Object.fromEntries(Object.entries(mcpTools).map(([name, mcpTool]) => [name, {
3397
2660
  tool: mcpTool,
3398
2661
  trust: "authoritative"
3399
2662
  }])), "MCP tool");
3400
2663
  }
3401
- /**
3402
- * Add host-supplied tools to a context at an explicit trust level.
3403
- *
3404
- * The trust level is the whole point of the seam: {@link withMcpTools} can only
3405
- * produce authoritative tools, so a host tool that *should* be sandboxed — and
3406
- * therefore executable in the browser tab that asked for it — had no way to be
3407
- * expressed at all. Here the host says which it is, and the contradictions are
3408
- * refused rather than silently resolved:
3409
- *
3410
- * - a `sandboxed` tool carrying `execute` would run inline in this process with
3411
- * the gateway's ambient authority, which is exactly what sandboxing it was
3412
- * meant to prevent;
3413
- * - an `authoritative` tool *without* `execute` would park the turn on a call no
3414
- * executor claims, and the session would simply stop.
3415
- */
3416
2664
  function withHostTools(context, hostTools, kind = "host tool") {
3417
2665
  const entries = Object.entries(hostTools);
3418
2666
  if (entries.length === 0) return context;
@@ -3448,9 +2696,9 @@ const MAX_CACHE_ENTRIES = 64;
3448
2696
  const MAX_REDIRECTS = 5;
3449
2697
  function createWebFetch(options = {}) {
3450
2698
  const fetchImpl = options.fetchImpl ?? fetch;
3451
- const maxContentBytes = options.maxContentBytes ?? 1024 * 1024;
3452
- const maxMarkdownBytes = options.maxMarkdownBytes ?? 50 * 1024;
3453
- const cacheTtlMs = options.cacheTtlMs ?? 900 * 1e3;
2699
+ const maxContentBytes = options.maxContentBytes ?? 1048576;
2700
+ const maxMarkdownBytes = options.maxMarkdownBytes ?? 51200;
2701
+ const cacheTtlMs = options.cacheTtlMs ?? 9e5;
3454
2702
  const cache = /* @__PURE__ */ new Map();
3455
2703
  const fetchPage = async (rawUrl) => {
3456
2704
  const cached = cache.get(rawUrl);
@@ -3559,10 +2807,6 @@ function parseUrl(raw) {
3559
2807
  return;
3560
2808
  }
3561
2809
  }
3562
- /** SSRF guard: resolve the hostname and refuse private, loopback, and link-local
3563
- * destinations. Checked per redirect hop. Resolution happens once here and again
3564
- * inside fetch (a DNS-rebinding TOCTOU); this tier accepts that — operators who
3565
- * need pinning can supply `fetchImpl` with a pinned agent. */
3566
2810
  async function denyReason(url, allowedHosts) {
3567
2811
  const host = url.hostname.toLowerCase();
3568
2812
  if (allowedHosts && allowedHosts.length > 0 && !hostMatches(host, allowedHosts)) return `host not allowed: ${host}`;
@@ -3587,7 +2831,6 @@ function hostMatches(host, allowedHosts) {
3587
2831
  return host === pattern;
3588
2832
  });
3589
2833
  }
3590
- /** Private / loopback / link-local / unspecified, IPv4 and IPv6 (incl. v4-mapped). */
3591
2834
  function isPrivateAddress(address) {
3592
2835
  const ip = address.toLowerCase();
3593
2836
  if (ip.includes(":")) {
@@ -3628,12 +2871,6 @@ async function readCapped(response, maxBytes) {
3628
2871
  function looksLikeHtml(body) {
3629
2872
  return /<(!doctype|html|head|body)[\s>]/i.test(body.slice(0, 1024));
3630
2873
  }
3631
- /**
3632
- * Dependency-free HTML → markdown, tuned for "give the model readable text":
3633
- * drops non-content subtrees, keeps headings/lists/links/emphasis/code, strips
3634
- * everything else. Not a spec-grade converter on purpose — a small predictable
3635
- * transform beats dragging a DOM into core.
3636
- */
3637
2874
  function htmlToMarkdown(html) {
3638
2875
  let text = html.replace(/<!--[\s\S]*?-->/g, "").replace(/<(script|style|noscript|svg|template|iframe)\b[\s\S]*?<\/\1>/gi, "").replace(/<(head)\b[\s\S]*?<\/\1>/gi, "");
3639
2876
  text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, body) => {
@@ -3656,25 +2893,12 @@ function decodeEntities(text) {
3656
2893
  }
3657
2894
  //#endregion
3658
2895
  //#region src/engines/provider/session.ts
3659
- /** Which capability a wired backend yields, for grant filtering. */
3660
2896
  const CAPABILITY_TOOLS = {
3661
2897
  search: "web_search",
3662
2898
  download: "download",
3663
2899
  webFetch: "web_fetch",
3664
2900
  deliverFiles: "deliver_file"
3665
2901
  };
3666
- /**
3667
- * Assemble a model-agnostic session: provider model, capability-scoped tools,
3668
- * a scratch VFS, and the executor that runs the sandboxed ones.
3669
- *
3670
- * This is the piece an operator wires into the server's `createEngineRunner`.
3671
- *
3672
- * The host wires the *backends*; the profile and the session request decide which
3673
- * of them are actually granted (`profile.session`, `config.capabilities`). A
3674
- * backend that isn't granted is simply not built into the tool set, so withholding
3675
- * a capability costs the host no branching. No declaration anywhere = everything
3676
- * the host wired, which is what a host that ignores profiles gets.
3677
- */
3678
2902
  function createEngineSession(options) {
3679
2903
  const vfs = options.config.vfs ?? createVfs(options.config.restore ? options.config.restore.vfs : options.seedVfs);
3680
2904
  const executor = options.selectExecutor.length > 0 ? {
@@ -3731,20 +2955,6 @@ function createEngineSession(options) {
3731
2955
  }, options.id);
3732
2956
  return runner;
3733
2957
  }
3734
- /**
3735
- * Refuse to build a session whose profile names an MCP server that isn't there.
3736
- *
3737
- * A profile's `mcpServers` list is a **declaration**, not a filter: an embedder
3738
- * who wrote it meant the agent to have those tools. Honouring it partially is
3739
- * the worst failure mode this engine has — the session starts, reports healthy,
3740
- * and the agent apologises its way through every request that needed the server,
3741
- * with one warning line in a log nobody is reading.
3742
- *
3743
- * With a {@link McpConnection} the check is exact (did this server connect?).
3744
- * With a bare tool set all we can see is whether any tool carries the server's
3745
- * namespace, so a genuinely tool-less server would trip it — the fix there is to
3746
- * pass `mcp` rather than to weaken this.
3747
- */
3748
2958
  function requireDeclaredServers(profileName, declared, mcp, tools) {
3749
2959
  if (!declared || declared.length === 0) return;
3750
2960
  const missing = declared.filter((name) => {
@@ -3761,33 +2971,11 @@ function requireDeclaredServers(profileName, declared, mcp, tools) {
3761
2971
  }).join(", ");
3762
2972
  throw new Error(`profile '${profileName}' declares MCP server(s) that are not connected: ${reasons}. A session missing a declared server is a session whose agent silently cannot do its job.`);
3763
2973
  }
3764
- /**
3765
- * Restrict a connected tool set to the MCP servers a profile grants, by the
3766
- * `<server>__<tool>` namespace {@link connectMcpTools} assigns. Undefined `servers`
3767
- * = no declaration, so every connected server passes through.
3768
- *
3769
- * This is how one process-wide MCP connection serves a mixed fleet: the host
3770
- * connects everything once, each profile grants a subset. The transport configs —
3771
- * and any credentials in their headers — never leave the host for a profile.
3772
- */
3773
2974
  function selectMcpTools(tools, servers) {
3774
2975
  if (!tools || servers === void 0) return tools;
3775
2976
  const allowed = new Set(servers);
3776
2977
  return Object.fromEntries(Object.entries(tools).filter(([name]) => allowed.has(name.split("__")[0])));
3777
2978
  }
3778
- /**
3779
- * Connect to MCP servers and return their tools, ready for {@link withMcpTools}.
3780
- *
3781
- * Server-side only, with server credentials: these tools are authoritative and
3782
- * must never be bridged to a browser. `@ai-sdk/mcp` is imported lazily and is an
3783
- * optional dependency — an operator who wires no MCP servers never needs it.
3784
- *
3785
- * **A stateless MCP server must answer `GET` with 405.** The client opens the
3786
- * SSE stream with a `GET` before it sends anything, and a POST-only server
3787
- * mounted under a framework's default 404 makes the whole connect fail with an
3788
- * error that names neither the method nor the route. This is the single most
3789
- * common way an otherwise-correct MCP mount fails.
3790
- */
3791
2979
  async function connectMcpTools(servers, options = {}) {
3792
2980
  const entries = Object.entries(servers);
3793
2981
  if (entries.length === 0) return {
@@ -3829,7 +3017,7 @@ async function connectMcpTools(servers, options = {}) {
3829
3017
  options.onError?.(name, error);
3830
3018
  if (options.required) {
3831
3019
  await closeAll();
3832
- throw new Error(`MCP server '${name}' failed to connect: ${message}`);
3020
+ throw new Error(`MCP server '${name}' failed to connect: ${message}`, { cause: error });
3833
3021
  }
3834
3022
  }
3835
3023
  }
@@ -3839,7 +3027,6 @@ async function connectMcpTools(servers, options = {}) {
3839
3027
  close: closeAll
3840
3028
  };
3841
3029
  }
3842
- /** The connection's identity, minus its secrets — `headers` never travel. */
3843
3030
  function describeServer(server) {
3844
3031
  if ("url" in server) return {
3845
3032
  transport: server.type === "sse" ? "sse" : "http",
@@ -3851,12 +3038,6 @@ function describeServer(server) {
3851
3038
  args: server.args
3852
3039
  };
3853
3040
  }
3854
- /**
3855
- * The AI SDK hands back its own `Tool`, whose `inputSchema` may be a zod schema
3856
- * or a `jsonSchema()` wrapper. Only the latter carries a JSON Schema document,
3857
- * so that is the only case where parameters are reported — `McpServerToolInfo`
3858
- * models the absence deliberately, and inventing one here would be worse.
3859
- */
3860
3041
  function toToolInfo(name, mcpTool) {
3861
3042
  const { description, inputSchema } = mcpTool ?? {};
3862
3043
  return {
@@ -3865,12 +3046,6 @@ function toToolInfo(name, mcpTool) {
3865
3046
  inputSchema: inputSchema?.jsonSchema
3866
3047
  };
3867
3048
  }
3868
- /**
3869
- * Only http/sse: the AI SDK's built-in transports are the remote ones, and its
3870
- * own docs mark stdio local-only and not deployable. A stdio server here is a
3871
- * misconfiguration worth surfacing rather than silently dropping — the Claude
3872
- * engine still supports stdio, since the CLI spawns those itself.
3873
- */
3874
3049
  function toTransport(server) {
3875
3050
  if (!("url" in server)) throw new Error("stdio MCP servers are not supported by the model-agnostic engine (use an http or sse server, or run this session under a Claude profile)");
3876
3051
  return server.type === "sse" ? {
@@ -3885,38 +3060,14 @@ function toTransport(server) {
3885
3060
  }
3886
3061
  //#endregion
3887
3062
  //#region src/engines/claude/catalog.ts
3888
- /**
3889
- * The Claude engine's model catalog — what a create form offers before any
3890
- * session has run.
3891
- *
3892
- * **Refresh procedure** (release checklist): run `supportedModels()` on a
3893
- * throwaway SDK query (no tokens spent) and re-apply the shaping rules of
3894
- * `modelOptionsFromSdk` (`src/normalize.ts`) at authoring time: drop the
3895
- * `default` sentinel row, derive display names from resolved ids where
3896
- * unambiguous, mark the newest of each family `primary`, sort by family rank.
3897
- * A unit test replays the raw extraction through `modelOptionsFromSdk` and
3898
- * asserts these rows match, so the rules cannot drift.
3899
- *
3900
- * Two things the live `capabilities` event can never offer:
3901
- * - rows for **older models** the CLI no longer reports (hand-maintained, the
3902
- * accepted cost of a static catalog; the CLI silently downgrades an effort a
3903
- * model doesn't support, so `reasoningEfforts` is omitted on them and the
3904
- * engine default set applies);
3905
- * - an answer on a **cold server**. The live event still exists and remains
3906
- * the in-session truth for the model switcher; this catalog is the
3907
- * create-form truth.
3908
- *
3909
- * `defaultModel` is deliberately NOT here: a claude profile's default is the
3910
- * operator's CLI config, unknowable statically.
3911
- */
3912
3063
  const CLAUDE_CATALOG = {
3913
- provenance: "supportedModels() of @anthropic-ai/claude-agent-sdk 0.3.221 (Claude Code CLI), extracted 2026-08-05; older-model rows hand-maintained",
3064
+ provenance: "supportedModels() of @anthropic-ai/claude-agent-sdk 0.3.258 (Claude Code CLI), extracted 2026-09-02; older-model rows hand-maintained",
3914
3065
  models: [
3915
3066
  {
3916
- value: "claude-fable-5[1m]",
3917
- resolvedModel: "claude-fable-5",
3918
- displayName: "Fable 5",
3919
- description: "Fable 5 · Most capable for your hardest and longest-running tasks",
3067
+ value: "claude-fable-5-1[1m]",
3068
+ resolvedModel: "claude-fable-5-1",
3069
+ displayName: "Fable 5.1",
3070
+ description: "Fable 5.1 · Most capable for your hardest and longest-running tasks",
3920
3071
  primary: true,
3921
3072
  reasoningEfforts: [
3922
3073
  "low",
@@ -3926,6 +3077,12 @@ const CLAUDE_CATALOG = {
3926
3077
  "max"
3927
3078
  ]
3928
3079
  },
3080
+ {
3081
+ value: "claude-fable-5[1m]",
3082
+ resolvedModel: "claude-fable-5",
3083
+ displayName: "Fable 5",
3084
+ description: "Fable 5 · Previous Fable generation"
3085
+ },
3929
3086
  {
3930
3087
  value: "opus[1m]",
3931
3088
  resolvedModel: "claude-opus-5[1m]",
@@ -3978,13 +3135,6 @@ const CLAUDE_CATALOG = {
3978
3135
  };
3979
3136
  //#endregion
3980
3137
  //#region src/engines/claude/adapter.ts
3981
- /**
3982
- * The Claude engine as an adapter — a thin, behaviourally inert wrapper:
3983
- * `SessionRunner` unchanged, `checkClaudeAuth` as the probe, the static
3984
- * catalog for create forms. Exists so catalogs, capabilities and availability
3985
- * have one shape across engines; the runner itself is exactly what
3986
- * `registry.prepare()` builds.
3987
- */
3988
3138
  const claudeAdapter = {
3989
3139
  engine: "claude",
3990
3140
  capabilities: ENGINE_CAPABILITIES.claude,
@@ -4002,12 +3152,6 @@ const claudeAdapter = {
4002
3152
  if (restore) throw new Error("the Claude engine cannot rebuild a parked session");
4003
3153
  return new SessionRunner(config, id);
4004
3154
  },
4005
- /**
4006
- * The Agent SDK's on-disk session store, mapped browser-safe. The SDK reads
4007
- * the store of the *process* environment — it takes no config dir — so a
4008
- * profile pin cannot narrow this listing; that matches the route's
4009
- * pre-adapter behavior exactly (the listing was always process-global).
4010
- */
4011
3155
  async listSessions({ dir, limit, offset }) {
4012
3156
  return (await listSessions({
4013
3157
  dir,
@@ -4025,10 +3169,28 @@ const claudeAdapter = {
4025
3169
  }));
4026
3170
  }
4027
3171
  };
3172
+ //#endregion
3173
+ //#region src/engines/codex/connect.ts
4028
3174
  /**
4029
- * A JSON-RPC error response from the peer, or one we return to it. `code`
4030
- * follows the JSON-RPC 2.0 reserved ranges (-32601 = method not found).
3175
+ * The operator's environment reaches the child whole WorkerDeck resolves no credential of its
3176
+ * own — and the profile's CODEX_HOME is the one key it pins, last, so a profile always wins over
3177
+ * an inherited value. Every path that spawns or connects to an app-server goes through here.
4031
3178
  */
3179
+ function codexChildEnv(base, codexHome) {
3180
+ const env = {};
3181
+ for (const [key, value] of Object.entries(base)) if (value !== void 0) env[key] = value;
3182
+ if (codexHome) env.CODEX_HOME = codexHome;
3183
+ return env;
3184
+ }
3185
+ /** `experimentalApi` gates the granular approval policy and there is no non-experimental fallback, so it is not per-call-site. */
3186
+ const INITIALIZE_PARAMS = {
3187
+ clientInfo: {
3188
+ name: "workerdeck",
3189
+ title: "WorkerDeck",
3190
+ version: `protocol-${PROTOCOL_VERSION}`
3191
+ },
3192
+ capabilities: { experimentalApi: true }
3193
+ };
4032
3194
  var JsonRpcError = class extends Error {
4033
3195
  code;
4034
3196
  constructor(code, message) {
@@ -4037,18 +3199,6 @@ var JsonRpcError = class extends Error {
4037
3199
  this.code = code;
4038
3200
  }
4039
3201
  };
4040
- /**
4041
- * JSON-RPC over a `codex app-server` child's stdio: **newline-delimited JSON**,
4042
- * one message per line, and — verified against 0.146.0 — an envelope *without*
4043
- * the `jsonrpc: "2.0"` field (`{id, method, params}` / `{id, result}` /
4044
- * `{id, error}`; the binary's own schema marks only those required). Server→
4045
- * client notifications additionally carry a top-level `emittedAtMs`, ignored
4046
- * here.
4047
- *
4048
- * Transport only: no method knowledge, no process ownership. The process
4049
- * wrapper (`process.ts`) owns the child and calls {@link fail} when it dies so
4050
- * every in-flight request rejects instead of hanging.
4051
- */
4052
3202
  var JsonRpcStdioConnection = class {
4053
3203
  #output;
4054
3204
  #nextId = 1;
@@ -4056,7 +3206,6 @@ var JsonRpcStdioConnection = class {
4056
3206
  #buffer = "";
4057
3207
  #closed = false;
4058
3208
  #notificationHandler;
4059
- /** Where {@link CODEX_TRACE_ENV} pointed, or undefined — read once. */
4060
3209
  #trace;
4061
3210
  #requestHandler;
4062
3211
  constructor(options) {
@@ -4095,8 +3244,6 @@ var JsonRpcStdioConnection = class {
4095
3244
  onRequest(handler) {
4096
3245
  this.#requestHandler = handler;
4097
3246
  }
4098
- /** Reject everything in flight and refuse new traffic — the child is gone
4099
- * (or the session is over). Idempotent. */
4100
3247
  fail(message) {
4101
3248
  if (this.#closed) return;
4102
3249
  this.#closed = true;
@@ -4126,15 +3273,6 @@ var JsonRpcStdioConnection = class {
4126
3273
  this.#dispatch(message);
4127
3274
  }
4128
3275
  }
4129
- /**
4130
- * Append one inbound message to the trace file, when the operator asked for
4131
- * one. **Notifications and server→client requests only** — a response body is
4132
- * not needed to answer the questions this exists for, and `account/*` results
4133
- * are the one place app-server traffic can carry a masked credential
4134
- * fragment, which nothing of ours writes to disk (see the auth red lines).
4135
- * Best-effort and synchronous: a debug sink that loses lines proves nothing,
4136
- * and a debug sink that throws must not take the session with it.
4137
- */
4138
3276
  #traceLine(message) {
4139
3277
  if (!this.#trace) return;
4140
3278
  const method = message.method;
@@ -4183,50 +3321,12 @@ var JsonRpcStdioConnection = class {
4183
3321
  };
4184
3322
  //#endregion
4185
3323
  //#region src/engines/codex/subagents.ts
4186
- /**
4187
- * The codex side of `SessionInfo.subagents` — and the attribution table that
4188
- * gives every event a spawned agent produces its `parentToolUseId`.
4189
- *
4190
- * Codex's signal is stronger than the claude engine's, so this is deliberately
4191
- * NOT that tracker generalised (`engines/claude/subagents.ts` infers spawns
4192
- * from tool names and verdicts from result-text sniffing, ~290 lines of module
4193
- * doc explaining the inference). Here nothing is inferred: `subAgentActivity
4194
- * {kind: 'started'}` on the owning thread positively announces an agent, names
4195
- * it (`agentPath`), keys it (`agentThreadId` — the id every one of its later
4196
- * notifications carries) and hands over the model's own `spawn_agent` call id;
4197
- * the agent's end is its own thread's `turn/completed`, status included. So a
4198
- * record is keyed by **thread id** — the wire's handle — while exposing a
4199
- * **tool-use id** — the protocol's: `parentToolUseId` on nested events must
4200
- * equal the anchor `tool_use`'s id for `subagentItems` (the frame membership
4201
- * rule every client shares) to reassemble the sidechain, and this map is where
4202
- * the two vocabularies meet.
4203
- *
4204
- * Two decisions worth their prose:
4205
- *
4206
- * **A record survives the runner's turns.** Codex agents are designed to
4207
- * outlive the root turn that spawned them (`sendInput`/`resumeAgent` address a
4208
- * thread that kept existing), so — unlike a pending approval — nothing here is
4209
- * swept when a root turn ends. What does end every agent is the app-server
4210
- * process itself: the runner calls {@link sweep} when the child dies or the
4211
- * session closes, because an agent whose host process is gone can never report,
4212
- * and `running` on a closed session would be a lie a polled list re-renders
4213
- * forever (the claude tracker's argument, inherited whole).
4214
- *
4215
- * **The settled tail is bounded, running records never are** — the same
4216
- * {@link SUBAGENT_HISTORY} discipline as the claude tracker, and enforced at
4217
- * settle time for the same reason: a settle happens once per agent, `list()`
4218
- * once per row of a 1.2s-polled sessions list.
4219
- */
4220
3324
  var CodexAgentTracker = class {
4221
3325
  #byThread = /* @__PURE__ */ new Map();
4222
3326
  #settleCounter = 0;
4223
- /** The record whose thread this is — the attribution lookup. */
4224
3327
  get(agentThreadId) {
4225
3328
  return this.#byThread.get(agentThreadId);
4226
3329
  }
4227
- /** Open (or return) the record for a thread. Fill-in, never overwrite: a
4228
- * label-less fallback record keeps its accumulated count and its already
4229
- * published toolUseId when the announcing item arrives late. */
4230
3330
  open(agentThreadId, toolUseId, agentType, ts) {
4231
3331
  let record = this.#byThread.get(agentThreadId);
4232
3332
  if (!record) {
@@ -4243,8 +3343,6 @@ var CodexAgentTracker = class {
4243
3343
  record.agentType ??= agentType;
4244
3344
  return record;
4245
3345
  }
4246
- /** The agent's thread ran again (`kind: 'interacted'`, or a fresh
4247
- * `turn/started` on its thread): a settled verdict no longer describes it. */
4248
3346
  revive(record) {
4249
3347
  record.status = "running";
4250
3348
  record.settledOrder = void 0;
@@ -4265,41 +3363,21 @@ var CodexAgentTracker = class {
4265
3363
  settled--;
4266
3364
  }
4267
3365
  }
4268
- /** A real verdict for one agent — its thread's `turn/completed`, or the
4269
- * `interrupted` activity edge. */
4270
3366
  settle(record, status) {
4271
3367
  if (record.status === status) return;
4272
3368
  this.#settle(record, status);
4273
3369
  }
4274
- /** The process the agents lived in is gone (child death, session close):
4275
- * everything still running is settled as failed — the report can never come. */
4276
3370
  sweep() {
4277
3371
  for (const record of this.#byThread.values()) if (record.status === "running") this.#settle(record, "failed");
4278
3372
  }
4279
- /**
4280
- * The conversation these agents belonged to is gone (a `conversation_reset`).
4281
- *
4282
- * Deliberately NOT {@link CodexAgentTracker.sweep}: that settles the running
4283
- * ones as failed and keeps the rows, which is right when the *process* dies —
4284
- * the transcript still holds the anchor `tool_use` each row points at, and a
4285
- * row that vanished would leave that card unexplained. A clear is the other
4286
- * way round. The anchors go with the transcript, so a surviving row would
4287
- * publish a `toolUseId` that resolves to nothing — and clients key a
4288
- * pressable, enterable agent line off exactly that id.
4289
- */
4290
3373
  forget() {
4291
3374
  this.#byThread.clear();
4292
3375
  }
4293
- /** The thread ids currently tracked — what a clear remembers so a still-running
4294
- * agent's later traffic can be dropped rather than re-anchored. */
4295
3376
  threadIds() {
4296
3377
  return Array.from(this.#byThread.keys());
4297
3378
  }
4298
- /** The rollup as `SessionInfo.subagents` serves it — spawn order, fresh
4299
- * objects, and `undefined` when there is nothing to say (absent and empty
4300
- * mean the same thing to a client, and bytes on a polled list are paid for). */
4301
3379
  list() {
4302
- if (this.#byThread.size === 0) return void 0;
3380
+ if (this.#byThread.size === 0) return;
4303
3381
  const out = [];
4304
3382
  for (const r of this.#byThread.values()) out.push({
4305
3383
  toolUseId: r.toolUseId,
@@ -4313,63 +3391,12 @@ var CodexAgentTracker = class {
4313
3391
  };
4314
3392
  //#endregion
4315
3393
  //#region src/engines/codex/trust.ts
4316
- /**
4317
- * Codex project trust: will this session's cwd get its `.codex/config.toml`?
4318
- *
4319
- * Codex only layers a project's `.codex/config.toml` onto the operator's base
4320
- * config when the project is *trusted* (a `[projects."<path>"]
4321
- * trust_level = "trusted"` entry in `$CODEX_HOME/config.toml`), and the
4322
- * app-server surface has no trust prompt — that lives in the TUI. So under
4323
- * WorkerDeck an untrusted project's config, MCP servers included, is silently
4324
- * ignored: no error, no notice, servers just missing. The runner asks this
4325
- * module at session start whether that is about to happen, so the transcript
4326
- * can say so.
4327
- *
4328
- * Semantics, all measured against both the bundled 0.146.0 and 0.149.0
4329
- * (2026-08-22, via `codex mcp list` from probe cwds and via `thread/start` +
4330
- * `mcpServerStatus/list` on the app-server surface — identical answers):
4331
- *
4332
- * - **Discovery**: config layers come from the cwd and its ancestors up to and
4333
- * including the nearest directory containing `.git` (dir or file). With no
4334
- * git anywhere above, the cwd alone is consulted. Directories above the
4335
- * nearest git root never contribute, trusted or not.
4336
- * - **Trust per layer**: an exact entry for the layer's own canonical path
4337
- * decides (an explicit `"untrusted"` beats inherited trust); without one the
4338
- * layer inherits from the chain's git root — trusted iff the git root has a
4339
- * trusted entry, where a linked worktree's root also counts its main
4340
- * repository's entry (the `.git` file's gitdir names it). A trusted
4341
- * mid-chain directory does NOT trust its children, and plain path
4342
- * containment without git confers nothing.
4343
- * - **Canonical paths**: codex matches entries against the canonicalized cwd —
4344
- * a macOS `/tmp/...` entry never matches the `/private/tmp/...` it points
4345
- * at, while the reverse spelling works (and the app-server canonicalizes its
4346
- * `cwd` param too). Both sides here are realpath'd, which can only err
4347
- * toward silence.
4348
- * - **The gate is sandbox-scoped**: `thread/start` under `workspace-write` or
4349
- * `danger-full-access` (permission modes `acceptEdits`/`bypassPermissions`)
4350
- * WRITES the trust entry itself and loads the config — only `read-only`
4351
- * (mode `default`) leaves the project untrusted and the config ignored. A
4352
- * later `turn/start` with a wider sandboxPolicy does not heal the thread
4353
- * (measured): the caller probes `default`-mode sessions only, and the notice
4354
- * stays true for the session it opens.
4355
- * - `trust_level`'s vocabulary is exactly `trusted`/`untrusted`; any other
4356
- * value fails codex's bootstrap outright ("unknown variant"), so a config
4357
- * carrying one probes silent — that session announces its own failure.
4358
- *
4359
- * The correctness bar for every degrade path: a FALSE notice — warning about a
4360
- * project codex actually trusts — is worse than a missed one. The narrow TOML
4361
- * reader below refuses (→ silence) anything it cannot interpret with
4362
- * certainty, rather than guessing.
4363
- */
4364
3394
  const BARE_KEY = /[A-Za-z0-9_-]/;
4365
3395
  function skipWs(text, pos) {
4366
3396
  let i = pos;
4367
3397
  while (i < text.length && (text[i] === " " || text[i] === " ")) i++;
4368
3398
  return i;
4369
3399
  }
4370
- /** One-line TOML basic string starting at `pos` (which must be `"`). Undefined
4371
- * on an escape TOML doesn't define or a close quote that never comes — the
4372
- * caller refuses the file rather than guessing what codex would read. */
4373
3400
  function parseBasicString(text, pos) {
4374
3401
  let out = "";
4375
3402
  let i = pos + 1;
@@ -4391,12 +3418,12 @@ function parseBasicString(text, pos) {
4391
3418
  else if (esc === "u" || esc === "U") {
4392
3419
  const width = esc === "u" ? 4 : 8;
4393
3420
  const hex = text.slice(i + 2, i + 2 + width);
4394
- if (hex.length !== width || !/^[0-9A-Fa-f]+$/.test(hex)) return void 0;
3421
+ if (hex.length !== width || !/^[0-9A-Fa-f]+$/.test(hex)) return;
4395
3422
  const code = Number.parseInt(hex, 16);
4396
- if (code > 1114111) return void 0;
3423
+ if (code > 1114111) return;
4397
3424
  out += String.fromCodePoint(code);
4398
3425
  i += width;
4399
- } else return void 0;
3426
+ } else return;
4400
3427
  i += 2;
4401
3428
  continue;
4402
3429
  }
@@ -4404,17 +3431,14 @@ function parseBasicString(text, pos) {
4404
3431
  i++;
4405
3432
  }
4406
3433
  }
4407
- /** One-line TOML literal string starting at `pos` (which must be `'`). */
4408
3434
  function parseLiteralString(text, pos) {
4409
3435
  const close = text.indexOf("'", pos + 1);
4410
- if (close === -1) return void 0;
3436
+ if (close === -1) return;
4411
3437
  return {
4412
3438
  value: text.slice(pos + 1, close),
4413
3439
  end: close + 1
4414
3440
  };
4415
3441
  }
4416
- /** A dotted key path — bare, `"basic"` and `'literal'` keys, whitespace around
4417
- * the dots — as found in table headers and on the left of assignments. */
4418
3442
  function parseKeyPath(text, pos) {
4419
3443
  const keys = [];
4420
3444
  let i = pos;
@@ -4423,7 +3447,7 @@ function parseKeyPath(text, pos) {
4423
3447
  const ch = text[i];
4424
3448
  if (ch === "\"" || ch === "'") {
4425
3449
  const str = ch === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
4426
- if (!str) return void 0;
3450
+ if (!str) return;
4427
3451
  keys.push(str.value);
4428
3452
  i = str.end;
4429
3453
  } else if (ch !== void 0 && BARE_KEY.test(ch)) {
@@ -4440,23 +3464,14 @@ function parseKeyPath(text, pos) {
4440
3464
  i++;
4441
3465
  }
4442
3466
  }
4443
- /**
4444
- * Scan an assignment's value (or the continuation line of a multi-line array),
4445
- * confirming where it ends. Returns the bracket depth carried onto the next
4446
- * line (0 = the value is complete) plus the string itself when the whole value
4447
- * was one plain one-line string. Undefined refuses the file: multi-line
4448
- * strings are where a line reader starts misreading string *content* as
4449
- * sections and entries — the exact mistake that could flip a real trust entry
4450
- * — so they are not parsed around, they end the attempt.
4451
- */
4452
3467
  function scanValueLine(text, pos, depth) {
4453
3468
  let i = skipWs(text, pos);
4454
3469
  if (depth === 0 && (text[i] === "\"" || text[i] === "'")) {
4455
- if (text.startsWith("\"\"\"", i) || text.startsWith("'''", i)) return void 0;
3470
+ if (text.startsWith("\"\"\"", i) || text.startsWith("'''", i)) return;
4456
3471
  const str = text[i] === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
4457
- if (!str) return void 0;
3472
+ if (!str) return;
4458
3473
  const rest = skipWs(text, str.end);
4459
- if (rest < text.length && text[rest] !== "#") return void 0;
3474
+ if (rest < text.length && text[rest] !== "#") return;
4460
3475
  return {
4461
3476
  depth: 0,
4462
3477
  value: str.value
@@ -4466,33 +3481,21 @@ function scanValueLine(text, pos, depth) {
4466
3481
  const ch = text[i];
4467
3482
  if (ch === "#") break;
4468
3483
  if (ch === "\"" || ch === "'") {
4469
- if (text.startsWith("\"\"\"", i) || text.startsWith("'''", i)) return void 0;
3484
+ if (text.startsWith("\"\"\"", i) || text.startsWith("'''", i)) return;
4470
3485
  const str = ch === "\"" ? parseBasicString(text, i) : parseLiteralString(text, i);
4471
- if (!str) return void 0;
3486
+ if (!str) return;
4472
3487
  i = str.end;
4473
3488
  continue;
4474
3489
  }
4475
3490
  if (ch === "[" || ch === "{") depth++;
4476
3491
  else if (ch === "]" || ch === "}") {
4477
3492
  depth--;
4478
- if (depth < 0) return void 0;
3493
+ if (depth < 0) return;
4479
3494
  }
4480
3495
  i++;
4481
3496
  }
4482
3497
  return { depth };
4483
3498
  }
4484
- /**
4485
- * The `[projects."<path>"] trust_level = "..."` entries of a codex
4486
- * `config.toml`, by a deliberately narrow reader (core takes no TOML
4487
- * dependency for this). Handles what codex itself writes plus the reasonable
4488
- * hand-edits — comments, CRLF, whitespace, quoted keys with escapes, literal
4489
- * and bare keys, `[projects]`-with-dotted-keys and top-level dotted forms,
4490
- * single-line inline tables, multi-line arrays — and returns **undefined for
4491
- * anything else it meets anywhere in the file** (multi-line strings,
4492
- * `projects` as an inline table, array-of-tables, junk): the caller treats
4493
- * undefined as "cannot know" and stays silent. Conflicting duplicate entries
4494
- * also refuse — invalid for TOML, and guessing wrong is a false notice.
4495
- */
4496
3499
  function parseProjectTrustEntries(source) {
4497
3500
  const entries = /* @__PURE__ */ new Map();
4498
3501
  let section = [];
@@ -4501,7 +3504,7 @@ function parseProjectTrustEntries(source) {
4501
3504
  const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
4502
3505
  if (carryDepth > 0) {
4503
3506
  const scanned = scanValueLine(line, 0, carryDepth);
4504
- if (!scanned) return void 0;
3507
+ if (!scanned) return;
4505
3508
  carryDepth = scanned.depth;
4506
3509
  continue;
4507
3510
  }
@@ -4510,43 +3513,35 @@ function parseProjectTrustEntries(source) {
4510
3513
  if (line[start] === "[") {
4511
3514
  const array = line.startsWith("[[", start);
4512
3515
  const path = parseKeyPath(line, start + (array ? 2 : 1));
4513
- if (!path) return void 0;
3516
+ if (!path) return;
4514
3517
  const close = array ? "]]" : "]";
4515
- if (!line.startsWith(close, path.end)) return void 0;
3518
+ if (!line.startsWith(close, path.end)) return;
4516
3519
  const rest = skipWs(line, path.end + close.length);
4517
- if (rest < line.length && line[rest] !== "#") return void 0;
4518
- if (array && path.value[0] === "projects") return void 0;
3520
+ if (rest < line.length && line[rest] !== "#") return;
3521
+ if (array && path.value[0] === "projects") return;
4519
3522
  section = path.value;
4520
3523
  continue;
4521
3524
  }
4522
3525
  const key = parseKeyPath(line, start);
4523
- if (!key) return void 0;
4524
- if (line[key.end] !== "=") return void 0;
3526
+ if (!key) return;
3527
+ if (line[key.end] !== "=") return;
4525
3528
  const scanned = scanValueLine(line, key.end + 1, 0);
4526
- if (!scanned) return void 0;
3529
+ if (!scanned) return;
4527
3530
  carryDepth = scanned.depth;
4528
3531
  const full = [...section, ...key.value];
4529
3532
  if (full[0] !== "projects") continue;
4530
- if (full.length < 3) return void 0;
3533
+ if (full.length < 3) return;
4531
3534
  if (full.length === 3 && full[2] === "trust_level") {
4532
- if (carryDepth !== 0 || scanned.value === void 0) return void 0;
3535
+ if (carryDepth !== 0 || scanned.value === void 0) return;
4533
3536
  const project = full[1];
4534
3537
  const existing = entries.get(project);
4535
- if (existing !== void 0 && existing !== scanned.value) return void 0;
3538
+ if (existing !== void 0 && existing !== scanned.value) return;
4536
3539
  entries.set(project, scanned.value);
4537
3540
  }
4538
3541
  }
4539
- if (carryDepth > 0) return void 0;
3542
+ if (carryDepth > 0) return;
4540
3543
  return entries;
4541
3544
  }
4542
- /**
4543
- * A linked worktree inherits trust from its main repository's entry (measured:
4544
- * trusting the main repo path loads the worktree's project config). The
4545
- * worktree's `.git` is a FILE whose `gitdir:` line names
4546
- * `<main>/.git/worktrees/<name>`; the directory owning that `.git` is the
4547
- * anchor to look up. Anything unreadable or shaped differently resolves false
4548
- * — this route can only ADD trust, i.e. silence, never a false notice.
4549
- */
4550
3545
  function mainRepositoryTrusted(gitRootDir, canonical) {
4551
3546
  const gitPath = join(gitRootDir, ".git");
4552
3547
  try {
@@ -4565,15 +3560,6 @@ function mainRepositoryTrusted(gitRootDir, canonical) {
4565
3560
  return false;
4566
3561
  }
4567
3562
  }
4568
- /**
4569
- * The notice for a codex session about to run on a cwd whose
4570
- * `.codex/config.toml` codex will ignore, or undefined when there is nothing
4571
- * to say — no project config anywhere codex would look, the project is
4572
- * trusted, or the situation cannot be established with certainty. Read-only
4573
- * throughout: WorkerDeck never writes trust entries (adjacent to the auth red
4574
- * lines — trusting a directory is the operator's decision, made in codex's
4575
- * own prompt or by their own hand).
4576
- */
4577
3563
  function untrustedProjectNotice(options) {
4578
3564
  let cwd;
4579
3565
  try {
@@ -4604,17 +3590,17 @@ function untrustedProjectNotice(options) {
4604
3590
  return false;
4605
3591
  }
4606
3592
  });
4607
- if (layers.length === 0) return void 0;
3593
+ if (layers.length === 0) return;
4608
3594
  const homeConfigPath = join(options.codexHome, "config.toml");
4609
3595
  let source = "";
4610
3596
  try {
4611
3597
  source = readFileSync(homeConfigPath, "utf8");
4612
3598
  } catch (error) {
4613
- if (error.code !== "ENOENT") return void 0;
3599
+ if (error.code !== "ENOENT") return;
4614
3600
  }
4615
3601
  const entries = parseProjectTrustEntries(source);
4616
- if (!entries) return void 0;
4617
- for (const value of entries.values()) if (value !== "trusted" && value !== "untrusted") return void 0;
3602
+ if (!entries) return;
3603
+ for (const value of entries.values()) if (value !== "trusted" && value !== "untrusted") return;
4618
3604
  const canonical = /* @__PURE__ */ new Map();
4619
3605
  for (const [key, value] of entries) {
4620
3606
  let path = key;
@@ -4630,59 +3616,25 @@ function untrustedProjectNotice(options) {
4630
3616
  if (entry !== void 0) return entry !== "trusted";
4631
3617
  return !rootTrusted;
4632
3618
  });
4633
- if (ignored.length === 0) return void 0;
3619
+ if (ignored.length === 0) return;
4634
3620
  const trustDir = gitRoot ?? cwd;
4635
3621
  const configs = ignored.map((layer) => join(layer, ".codex", "config.toml"));
4636
3622
  return `codex does not trust this directory, so ${configs.length === 1 ? `its project config (${configs[0]}) is` : `its project configs (${configs.join(", ")}) are`} being ignored — MCP servers and settings declared there will be missing from this session. To trust it, run codex once in ${trustDir} and accept the trust prompt, or add [projects."${trustDir}"] with trust_level = "trusted" to ${homeConfigPath}.`;
4637
3623
  }
4638
3624
  //#endregion
4639
3625
  //#region src/engines/codex/runner.ts
4640
- /**
4641
- * thread/start's sandbox axis (string form) — our permission modes as codex
4642
- * sandbox modes: `default` → read-only (reads run; any mutation is refused by
4643
- * the OS sandbox and — with the ask policy below — escalates to a real
4644
- * question), `acceptEdits` → workspace-write (in-workspace writes sail
4645
- * through, the acceptEdits grant), `bypassPermissions` → danger-full-access.
4646
- * `auto` rides the SAME sandbox as acceptEdits — it is not a wider grant, it
4647
- * only moves *who answers* the approvals (see {@link APPROVALS_REVIEWER_BY_MODE}).
4648
- */
4649
3626
  const THREAD_SANDBOX_BY_MODE = {
4650
3627
  default: "read-only",
4651
3628
  acceptEdits: "workspace-write",
4652
3629
  auto: "workspace-write",
4653
3630
  bypassPermissions: "danger-full-access"
4654
3631
  };
4655
- /**
4656
- * turn/start's sandboxPolicy axis (object form — same policy, second shape).
4657
- *
4658
- * The `workspaceWrite` entries here are a SHAPE, not the whole policy: every
4659
- * unstated field of that variant is serde-defaulted by the app-server, so
4660
- * sending it bare silently overrides the operator's `[sandbox_workspace_write]`
4661
- * — `network_access` back to false, `writable_roots` back to empty — on every
4662
- * turn. {@link CodexRunner.#turnSandboxPolicy} restates those fields from
4663
- * `config/read`; nothing else may send this map's `workspaceWrite` entries
4664
- * directly.
4665
- */
4666
3632
  const TURN_SANDBOX_BY_MODE = {
4667
3633
  default: { type: "readOnly" },
4668
3634
  acceptEdits: { type: "workspaceWrite" },
4669
3635
  auto: { type: "workspaceWrite" },
4670
3636
  bypassPermissions: { type: "dangerFullAccess" }
4671
3637
  };
4672
- /**
4673
- * The approval axis, stated as the GRANULAR object on both thread/start and
4674
- * turn/start — never the string vocabulary, deliberately and unconditionally:
4675
- * measured against 0.146.0, plain `'untrusted'` never asked anything (a
4676
- * sandbox-violating write was silently refused, a safe echo auto-approved),
4677
- * while the granular flags make a blocked action a real server→client
4678
- * question. Granular policies are gated on `capabilities.experimentalApi` at
4679
- * initialize; WorkerDeck declares it always and keeps NO non-experimental
4680
- * fallback — a future binary that rejects either gate fails loudly (see
4681
- * {@link CodexRunner.#ensureThread}) instead of quietly not asking.
4682
- *
4683
- * `default`/`acceptEdits` ask (all flags on — the sandbox axis above already
4684
- * decides *what needs asking*); `bypassPermissions` asks nothing, same shape.
4685
- */
4686
3638
  const GRANULAR_ASK = { granular: {
4687
3639
  sandbox_approval: true,
4688
3640
  rules: true,
@@ -4697,12 +3649,7 @@ const GRANULAR_NEVER = { granular: {
4697
3649
  request_permissions: false,
4698
3650
  skill_approval: false
4699
3651
  } };
4700
- /**
4701
- * Notifications whose meaning is scoped to ONE thread, and which are therefore
4702
- * only ever read off the session's own. Everything else (items, deltas) is
4703
- * accepted from any thread on the connection — see `#handleNotification`.
4704
- */
4705
- const THREAD_SCOPED_NOTIFICATIONS = new Set([
3652
+ const THREAD_SCOPED_NOTIFICATIONS = /* @__PURE__ */ new Set([
4706
3653
  "turn/started",
4707
3654
  "turn/completed",
4708
3655
  "thread/tokenUsage/updated"
@@ -4713,62 +3660,20 @@ const APPROVAL_POLICY_BY_MODE = {
4713
3660
  auto: GRANULAR_ASK,
4714
3661
  bypassPermissions: GRANULAR_NEVER
4715
3662
  };
4716
- /**
4717
- * The THIRD approval axis — *who reviews*, independent of the sandbox axis and
4718
- * the ask axis above. Codex's `approvalsReviewer` (thread/start and turn/start,
4719
- * present since 0.146.0) routes every approval request either to the user
4720
- * (`'user'`, codex's own default) or to `'auto_review'`: a prompted subagent
4721
- * that gathers context and applies a risk framework before allowing or denying.
4722
- * That is codex's "Approve for me" preset, and our `auto` mode is exactly it.
4723
- *
4724
- * Sent explicitly for EVERY mode rather than omitted for the default — a thread
4725
- * inherits `approvalsReviewer` across turns ("this turn and subsequent turns"),
4726
- * so leaving it unset would let a stale reviewer from an earlier turn survive a
4727
- * mode switch back to a user-reviewed mode. Stating it every time makes the
4728
- * mode the single source of truth.
4729
- *
4730
- * NOTE the asymmetry with the Claude engine's `auto`: that classifier is
4731
- * operator-configurable (`autoMode.environment`, allow/soft_deny/hard_deny);
4732
- * this reviewer has no configuration surface at all.
4733
- */
4734
3663
  const APPROVALS_REVIEWER_BY_MODE = {
4735
3664
  default: "user",
4736
3665
  acceptEdits: "user",
4737
3666
  auto: "auto_review",
4738
3667
  bypassPermissions: "user"
4739
3668
  };
4740
- /** Fallback timeout for a pending approval nobody answers — the SessionRunner
4741
- * default, so unattended codex sessions land the same way Claude ones do. */
4742
3669
  const DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
4743
- /**
4744
- * Tool name for codex's built-in `image_gen`. A stable string because it is a
4745
- * rendering contract: both clients key an icon (and, where they can reach the
4746
- * host filesystem, an inline preview) off it.
4747
- */
4748
3670
  const CODEX_IMAGE_TOOL = "CodexImageGeneration";
4749
- /**
4750
- * Tool name for a spawned agent's anchor `tool_use` — the claude engine's
4751
- * `Task` in this engine's vocabulary. Codex never sends such a call: the model's
4752
- * `spawn_agent` surfaces only as the `subAgentActivity` marker item, so the
4753
- * runner authors the call itself, because everything downstream is built on a
4754
- * top-level `tool_use` existing — `terminalBlocks` absorbs a sidechain into the
4755
- * call whose id its events carry as `parentToolUseId`, the takeover frames by
4756
- * it, and `taskIdentity` labels it from the input's `subagent_type`. Not a new
4757
- * wire idea, just a row: the same shape every other codex tool card uses.
4758
- */
4759
3671
  const CODEX_AGENT_TOOL = "CodexAgent";
4760
- /** Tool name for the model's collab-agent calls (`wait`, `sendInput`, …), the
4761
- * `tool` field carried in the input. One name for the whole open axis rather
4762
- * than a name per verb, so a future verb renders instead of vanishing. */
4763
3672
  const CODEX_COLLAB_TOOL = "CodexCollab";
4764
- /** An agent's name is its path's basename: '/root/date_one' → 'date_one'. */
4765
3673
  function agentName(agentPath) {
4766
- if (typeof agentPath !== "string") return void 0;
3674
+ if (typeof agentPath !== "string") return;
4767
3675
  return agentPath.split("/").filter(Boolean).at(-1) || void 0;
4768
3676
  }
4769
- /** The collab card's input: the verb always, the rich fields only when codex
4770
- * actually filled them (measured against 0.146.0 they arrive empty — the card
4771
- * must not render five null columns to say 'wait'). */
4772
3677
  function collabInput(item) {
4773
3678
  return {
4774
3679
  tool: item.tool,
@@ -4777,9 +3682,6 @@ function collabInput(item) {
4777
3682
  ...item.model ? { model: item.model } : {}
4778
3683
  };
4779
3684
  }
4780
- /** A completed turn's answer, from its summary `items` page — the last
4781
- * `agentMessage` text. For a sub-agent's thread this is the agent's report,
4782
- * which is exactly what belongs in the anchor's `tool_result`. */
4783
3685
  function turnReport(turn) {
4784
3686
  const items = Array.isArray(turn.items) ? turn.items : [];
4785
3687
  for (let index = items.length - 1; index >= 0; index--) {
@@ -4787,29 +3689,13 @@ function turnReport(turn) {
4787
3689
  if (item?.type === "agentMessage" && typeof item.text === "string" && item.text) return item.text;
4788
3690
  }
4789
3691
  }
4790
- /** Longest `result` worth putting in a tool card. The field is free-form and
4791
- * undocumented; anything past this is assumed to be an encoded image rather
4792
- * than a sentence, and encoded images do not go in the event log. */
4793
3692
  const MAX_IMAGE_RESULT_CHARS = 512;
4794
- const shortResult = (result) => result.length > 0 && result.length <= MAX_IMAGE_RESULT_CHARS && !result.startsWith("data:");
4795
- /**
4796
- * `file_produced.fileId` — derived from the path, not minted fresh.
4797
- *
4798
- * Two properties fall out of that and both are load-bearing: codex reports the
4799
- * same `savedPath` on the progress item and again on the completed one, so a
4800
- * derived id makes the second emission a no-op instead of a duplicate row; and
4801
- * a session rebuilt from a snapshot re-derives the same ids, so a client's
4802
- * cached URL still resolves after a park/restore.
4803
- */
3693
+ function shortResult(result) {
3694
+ return result.length > 0 && result.length <= MAX_IMAGE_RESULT_CHARS && !result.startsWith("data:");
3695
+ }
4804
3696
  function producedFileId(path) {
4805
3697
  return createHash("sha256").update(path).digest("hex").slice(0, 32);
4806
3698
  }
4807
- /** Media type from the extension, for the handful a client renders inline.
4808
- * Undefined for everything else — the route sniffs, and guessing here is how a
4809
- * text file ends up labelled `image/png`. */
4810
- function producedMediaType(path) {
4811
- return PRODUCED_MEDIA_TYPES[path.slice(path.lastIndexOf(".") + 1).toLowerCase()];
4812
- }
4813
3699
  const PRODUCED_MEDIA_TYPES = {
4814
3700
  png: "image/png",
4815
3701
  jpg: "image/jpeg",
@@ -4819,12 +3705,10 @@ const PRODUCED_MEDIA_TYPES = {
4819
3705
  svg: "image/svg+xml",
4820
3706
  pdf: "application/pdf"
4821
3707
  };
4822
- /**
4823
- * Codex's `SkillMetadata` as the protocol states it. `interface.shortDescription`
4824
- * beats the legacy top-level one (codex's own comment says to prefer it), and
4825
- * `enabled` defaults to true — an entry codex listed without the field is one it
4826
- * considers live, and defaulting to false would hide working skills.
4827
- */
3708
+ function producedMediaType(path) {
3709
+ const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
3710
+ return PRODUCED_MEDIA_TYPES[extension];
3711
+ }
4828
3712
  function skillInfo(skill) {
4829
3713
  return {
4830
3714
  name: skill.name,
@@ -4836,19 +3720,6 @@ function skillInfo(skill) {
4836
3720
  enabled: skill.enabled !== false
4837
3721
  };
4838
3722
  }
4839
- /**
4840
- * Codex's MCP status → the protocol's, which is Claude Code's vocabulary
4841
- * ('connected' | 'failed' | 'needs-auth' | 'pending' | 'disabled').
4842
- *
4843
- * Two inputs, and the auth one wins where it applies: a server that started
4844
- * fine but has no credential is *needs-auth*, not connected, because that is
4845
- * the thing the operator has to act on. `notLoggedIn` is the only auth value
4846
- * that means "unusable" — `unsupported` is the normal answer for a stdio server
4847
- * that has no auth concept at all.
4848
- *
4849
- * A server with no startup notification yet is 'pending', not 'connected':
4850
- * `mcpServerStatus/list` alone only proves it is *configured*.
4851
- */
4852
3723
  function mcpStatusOf(authStatus, update, hasTools) {
4853
3724
  if (update?.status === "failed") return update.failureReason === "reauthenticationRequired" ? "needs-auth" : "failed";
4854
3725
  if (update?.status === "cancelled") return "failed";
@@ -4857,7 +3728,6 @@ function mcpStatusOf(authStatus, update, hasTools) {
4857
3728
  if (hasTools) return "connected";
4858
3729
  return "pending";
4859
3730
  }
4860
- /** One `mcpServerStatus/list` entry as the protocol states it. */
4861
3731
  function mcpServerInfo(server, update) {
4862
3732
  const tools = Object.entries(server.tools ?? {}).flatMap(([key, tool]) => {
4863
3733
  if (!tool) return [];
@@ -4884,55 +3754,26 @@ function mcpServerInfo(server, update) {
4884
3754
  ...tools.length > 0 ? { tools } : {}
4885
3755
  };
4886
3756
  }
4887
- /** What the card shows while the picture is being made, and after. `savedPath`
4888
- * only exists once it lands — a client keys its preview off it, so it is a
4889
- * field rather than a sentence in the result text. */
4890
3757
  function imageGenerationInput(item) {
4891
3758
  return {
4892
3759
  ...item.revisedPrompt ? { prompt: item.revisedPrompt } : {},
4893
3760
  ...item.savedPath ? { savedPath: item.savedPath } : {}
4894
3761
  };
4895
3762
  }
4896
- /**
4897
- * The experimental per-request decision list, normalized to names: a string
4898
- * entry is its own name, a structured entry (`{acceptWithExecpolicyAmendment:
4899
- * …}`) is named by its key. Undefined = the request stated no list and the
4900
- * channel's schema enum applies. Present only under `experimentalApi: true` —
4901
- * which WorkerDeck always declares.
4902
- */
4903
3763
  function offeredDecisions(params) {
4904
3764
  const raw = params?.availableDecisions;
4905
- if (!Array.isArray(raw)) return void 0;
3765
+ if (!Array.isArray(raw)) return;
4906
3766
  const names = /* @__PURE__ */ new Set();
4907
3767
  for (const entry of raw) if (typeof entry === "string") names.add(entry);
4908
3768
  else if (entry && typeof entry === "object") for (const key of Object.keys(entry)) names.add(key);
4909
3769
  return names.size > 0 ? names : void 0;
4910
3770
  }
4911
- /**
4912
- * Decision picking for the `{decision: …}` channels (commandExecution,
4913
- * fileChange), honoring the request's own `availableDecisions`:
4914
- *
4915
- * - allow → 'accept' when offered (or when no list was stated). A request
4916
- * offering only the broader accepts ('acceptForSession',
4917
- * 'acceptWithExecpolicyAmendment') yields undefined: a one-shot allow must
4918
- * not be silently widened into a session-wide or persistent policy grant, so
4919
- * the caller answers with the denial and says why.
4920
- * - deny → 'decline', always: the response schema declares it unconditionally,
4921
- * and it was verified live against 0.146.0 answering a request whose
4922
- * availableDecisions omitted it — the turn completed cleanly. The list's job
4923
- * is to gate the accept variants, not to take "no, but keep going" away
4924
- * (its own alternative, 'cancel', would interrupt the whole turn).
4925
- * - deny+interrupt → 'cancel' (codex's deny-and-interrupt) when offered;
4926
- * otherwise 'decline', and the caller interrupts the turn itself.
4927
- */
4928
3771
  function pickDecision(behavior, interrupt, offered) {
4929
3772
  const has = (name) => !offered || offered.has(name);
4930
3773
  if (behavior === "allow") return has("accept") ? "accept" : void 0;
4931
3774
  if (interrupt && has("cancel")) return "cancel";
4932
3775
  return "decline";
4933
3776
  }
4934
- /** Codex `requestUserInput` questions in the AskUserQuestion wire shape both
4935
- * clients already render (QuestionPrompt / QuestionPromptView). */
4936
3777
  function userQuestionsFromCodex(questions) {
4937
3778
  return questions.map((question) => ({
4938
3779
  question: question.question,
@@ -4943,18 +3784,6 @@ function userQuestionsFromCodex(questions) {
4943
3784
  }))
4944
3785
  }));
4945
3786
  }
4946
- /**
4947
- * The text of a history `userMessage` item: its content entries' text parts
4948
- * joined.
4949
- *
4950
- * Image parts have no replayable representation — the bytes went to the model,
4951
- * not into the rollout we can render from — so they are named rather than
4952
- * dropped. A prompt that was *only* an image used to produce an empty string,
4953
- * which the caller read as "nothing to replay" and skipped: the turn lost its
4954
- * user row and, with it, the prompt mark the scrubber navigates by, so a resumed
4955
- * thread had answers with no visible question. A word in place of the picture is
4956
- * a smaller lie than a turn that never happened.
4957
- */
4958
3787
  function historyUserText(item) {
4959
3788
  if (!Array.isArray(item.content)) return "";
4960
3789
  let images = 0;
@@ -4967,9 +3796,6 @@ function historyUserText(item) {
4967
3796
  if (text) return text;
4968
3797
  return images > 0 ? `[${images === 1 ? "image" : `${images} images`}]` : "";
4969
3798
  }
4970
- /** The AskUserQuestion answer convention (question text → chosen label(s),
4971
- * comma-joined) mapped back to codex's id-keyed shape. Questions the client
4972
- * did not answer are absent, not empty. */
4973
3799
  function codexAnswers(questions, answers) {
4974
3800
  const out = {};
4975
3801
  for (const question of questions) {
@@ -4978,7 +3804,6 @@ function codexAnswers(questions, answers) {
4978
3804
  }
4979
3805
  return out;
4980
3806
  }
4981
- /** The two channels whose response is `{decision: …}` share their pick logic. */
4982
3807
  function decisionChannel(describe, itemId) {
4983
3808
  return {
4984
3809
  describe,
@@ -4999,11 +3824,6 @@ function decisionChannel(describe, itemId) {
4999
3824
  }
5000
3825
  };
5001
3826
  }
5002
- /**
5003
- * The ask channels, wired to the permission surface. Anything not listed here
5004
- * still gets a JSON-RPC -32601 — never a hang (an unanswered server request
5005
- * wedges the turn).
5006
- */
5007
3827
  const APPROVAL_CHANNELS = {
5008
3828
  "item/commandExecution/requestApproval": decisionChannel((raw) => {
5009
3829
  const params = raw;
@@ -5094,129 +3914,48 @@ const APPROVAL_CHANNELS = {
5094
3914
  deny: (_raw, interrupt) => ({ response: { action: interrupt ? "cancel" : "decline" } })
5095
3915
  }
5096
3916
  };
5097
- /**
5098
- * Name a subscription window by its measured length, so codex's positional
5099
- * windows land in the protocol's named vocabulary. The two names clients
5100
- * already understand are exact matches for codex's durations (300 min = 5h,
5101
- * 10080 min = 7d); anything else keeps a self-describing key rather than
5102
- * borrowing a name that would size it wrongly.
5103
- */
5104
3917
  function rateLimitWindowName(minutes) {
5105
- if (typeof minutes !== "number" || !Number.isFinite(minutes) || minutes <= 0) return void 0;
3918
+ if (typeof minutes !== "number" || !Number.isFinite(minutes) || minutes <= 0) return;
5106
3919
  if (minutes === 300) return "five_hour";
5107
3920
  if (minutes === 10080) return "seven_day";
5108
3921
  return `window_${minutes}m`;
5109
3922
  }
5110
- /**
5111
- * The Codex engine, over the binary's `app-server` JSON-RPC surface: ONE
5112
- * `codex app-server` child per *session* (spawned lazily, held across turns),
5113
- * streaming `item/agentMessage/delta` and the reasoning deltas token-by-token
5114
- * (`streaming: 'token'`). Follows `SessionRunner`'s event-log/seq/status
5115
- * discipline with `AiSdkRunner`'s turn-chain (one turn at a time; sendMessage
5116
- * queues). The first codex transport was `codex exec --experimental-json` (one
5117
- * child per turn) — retired because its JSONL carries no partial messages, so
5118
- * a turn could never stream.
5119
- *
5120
- * A dead child is a failed *turn*, not a failed session: the thread persists
5121
- * on disk, the connection is dropped, and the next message spawns a fresh
5122
- * child that `thread/resume`s the same thread id.
5123
- */
5124
3923
  var CodexRunner = class {
5125
3924
  id;
5126
3925
  createdAt;
5127
3926
  #config;
5128
- /** {@link CodexRunnerConfig.cwd}, checked once in the constructor. */
5129
3927
  #cwd;
5130
- #events = [];
3928
+ #log = new EventLog();
5131
3929
  #subscribers = new SubscriberSet();
5132
- #seq = 0;
5133
- /**
5134
- * Latest context-window reading, retained from the last `context_usage` this
5135
- * runner emitted so `GET /sessions` can answer it without an attach — see
5136
- * `SessionInfo.contextUsage`. Folded in the emit path, so it is by
5137
- * construction the same number the transcript last drew.
5138
- */
5139
- #contextUsage;
5140
- #activityCount = 0;
5141
- /**
5142
- * Seq of the latest `conversation_reset` event, 0 when none. The log itself is
5143
- * never truncated — it still carries the state-bearing events (`capabilities`,
5144
- * `system_init`, …) a fresh attacher depends on and which are not re-emitted —
5145
- * but `subscribe()` skips transcript *content* strictly below this mark, so a
5146
- * replay does not resurrect a cleared conversation. A later reset supersedes
5147
- * an earlier one by overwriting it.
5148
- */
5149
- #resetSeq = 0;
5150
3930
  #status = "starting";
3931
+ #statusDetail;
5151
3932
  #sdkSessionId;
5152
3933
  #model;
5153
3934
  #permissionMode;
5154
3935
  #reasoningEffort;
5155
- /** What the binary said the profile's defaults resolve to (thread/start
5156
- * response) — lets `setModel(undefined)` mean "back to the default" even
5157
- * though a turn/start override persists for subsequent turns. */
5158
3936
  #resolvedModel;
5159
- /** Last reported ChatGPT plan, so `plan_info` is emitted once per change. */
5160
3937
  #planType;
5161
3938
  #resolvedEffort;
5162
3939
  #queue = [];
5163
3940
  #turnChain = Promise.resolve();
5164
3941
  #activeTurn;
5165
3942
  #connection;
5166
- /** Per-child, from `config/read`; undefined = read failed, send the bare shape. */
5167
3943
  #workspaceWrite;
5168
3944
  #threadLoaded = false;
5169
3945
  #numTurns = 0;
5170
3946
  #totalCostUsd;
5171
- #lastActivityAt;
5172
3947
  #started = false;
5173
3948
  #closed = false;
5174
- /** Session temp dir for image attachments (`localImage` takes host paths). */
5175
3949
  #imageDir;
5176
- /** Pending server→client approvals, keyed by the surfaced request id. */
5177
3950
  #approvals = /* @__PURE__ */ new Map();
5178
- /** True from start() until the resume backfill (the turn chain's first link)
5179
- * settles — while set, sendMessage defers its user_message echo behind the
5180
- * chain so a new turn can never precede or interleave the replayed history. */
5181
3951
  #backfillPending = false;
5182
- /** The resumed thread's prior turns, stashed by {@link #ensureThread} from
5183
- * the ONE thread/resume the backfill consumes (`partial` = the response's
5184
- * turnsBackwardsCursor said older turns exist beyond this page). A mid-life
5185
- * reconnect also goes through thread/resume, but with no backfill pending
5186
- * nothing is stashed — history is never replayed twice. */
5187
3952
  #resumedHistory;
5188
- /** Set around history replay: {@link #emit} stamps `replay: true` onto the
5189
- * message events the live item mapping produces. */
5190
3953
  #replayingHistory = false;
5191
- /** Last `skills` payload emitted, serialized — the comparison that keeps a
5192
- * `skills/changed` storm (the watcher fires per touched file) from filling
5193
- * the event log with identical lists. */
5194
3954
  #skillsFingerprint;
5195
- /** In-flight `skills/list`, so a burst of `skills/changed` makes one call.
5196
- * The pending promise is reused rather than queued: the request has no
5197
- * arguments, so a second one would ask the same question. */
5198
3955
  #skillsRefresh;
5199
- /** Host paths already announced via `file_produced`, so the same picture
5200
- * reported on both the progress and the completed item registers once. */
5201
3956
  #producedPaths = /* @__PURE__ */ new Set();
5202
- /** Per-server liveness, accumulated from `mcpServer/startupStatus/updated`.
5203
- * `mcpServerStatus/list` does not carry a status field at all, so without
5204
- * this every server would read as "configured" and never as up or down. */
5205
3957
  #mcpStatus = /* @__PURE__ */ new Map();
5206
- /** The spawned agents, keyed by their thread ids — the attribution table
5207
- * behind `parentToolUseId` and the rollup behind `info().subagents`. Runner-
5208
- * level, not per-turn: an agent's thread outlives the root turn that spawned
5209
- * it, and only the child process dying (or the session closing) ends them
5210
- * all — see the module doc in `subagents.ts`. */
5211
3958
  #agents = new CodexAgentTracker();
5212
- /** Threads that belonged to a conversation this session has cleared — the
5213
- * agents that were still running when it happened. Their notifications keep
5214
- * arriving on the same connection (a clear does not interrupt them and does
5215
- * not drop the child), and without this {@link CodexRunner.#agentFor} would
5216
- * mint them a fresh anchor and stream the cleared conversation's agent work
5217
- * into the new one. Never pruned: it is a handful of uuids for the session's
5218
- * life, and a late report from a long-dead agent is exactly what it exists to
5219
- * catch. */
5220
3959
  #clearedThreads = /* @__PURE__ */ new Set();
5221
3960
  constructor(config, id = randomUUID()) {
5222
3961
  const mode = config.permissionMode ?? "default";
@@ -5232,15 +3971,8 @@ var CodexRunner = class {
5232
3971
  this.id = id;
5233
3972
  this.createdAt = Date.now();
5234
3973
  }
5235
- /** The complete child environment — spawn env replaces process.env wholesale,
5236
- * so this must carry everything a shell would, with the profile's CODEX_HOME
5237
- * pin winning over operator env. */
5238
3974
  #childEnv() {
5239
- const base = this.#config.env ?? process.env;
5240
- const env = {};
5241
- for (const [key, value] of Object.entries(base)) if (value !== void 0) env[key] = value;
5242
- if (this.#config.codexHome) env.CODEX_HOME = this.#config.codexHome;
5243
- return env;
3975
+ return codexChildEnv(this.#config.env ?? process.env, this.#config.codexHome);
5244
3976
  }
5245
3977
  get status() {
5246
3978
  return this.#status;
@@ -5249,7 +3981,7 @@ var CodexRunner = class {
5249
3981
  return this.#sdkSessionId;
5250
3982
  }
5251
3983
  get lastSeq() {
5252
- return this.#seq;
3984
+ return this.#log.seq;
5253
3985
  }
5254
3986
  get pendingApprovals() {
5255
3987
  return [...this.#approvals.values()].map((pending) => pending.request);
@@ -5267,36 +3999,22 @@ var CodexRunner = class {
5267
3999
  permissionMode: this.#permissionMode,
5268
4000
  canBypassPermissions: true,
5269
4001
  createdAt: this.createdAt,
5270
- lastSeq: this.#seq,
5271
- activityCount: this.#activityCount,
5272
- contextUsage: this.#contextUsage,
4002
+ lastSeq: this.#log.seq,
4003
+ activityCount: this.#log.activityCount,
4004
+ proseCount: this.#log.proseCount,
4005
+ contextUsage: this.#log.contextUsage,
5273
4006
  pendingPermissionCount: this.#approvals.size,
5274
4007
  meta: this.#config.meta,
5275
4008
  scope: this.#config.scope,
5276
- title: this.#title(),
4009
+ title: sessionTitle(this.#config),
5277
4010
  totalCostUsd: this.#totalCostUsd,
5278
4011
  numTurns: this.#numTurns || void 0,
5279
- lastActivityAt: this.#lastActivityAt,
4012
+ lastActivityAt: this.#log.lastActivityAt,
5280
4013
  subagents: this.#agents.list()
5281
4014
  };
5282
4015
  }
5283
- #title() {
5284
- const metaTitle = this.#config.meta?.title;
5285
- if (typeof metaTitle === "string" && metaTitle.length > 0) return metaTitle;
5286
- const prompt = this.#config.prompt;
5287
- if (!prompt) return void 0;
5288
- return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
5289
- }
5290
- /** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
5291
- * it (undefined) restores the derived title. The engine is never told. */
5292
4016
  setTitle(title) {
5293
- const meta = { ...this.#config.meta };
5294
- if (title) meta.title = title;
5295
- else delete meta.title;
5296
- this.#config = {
5297
- ...this.#config,
5298
- meta
5299
- };
4017
+ this.#config = withTitle(this.#config, title);
5300
4018
  }
5301
4019
  start() {
5302
4020
  if (this.#started) return this.#turnChain;
@@ -5310,20 +4028,6 @@ var CodexRunner = class {
5310
4028
  if (!this.#config.prompt && !this.#config.resume) this.#probeSkills();
5311
4029
  return this.#turnChain;
5312
4030
  }
5313
- /**
5314
- * One-time transcript notice for the codex trust gap: a `default`-mode
5315
- * session (read-only sandbox) on an untrusted cwd has its
5316
- * `.codex/config.toml` — MCP servers included — silently ignored, and the
5317
- * app-server surface has no trust prompt to say so (the TUI's prompt is
5318
- * where the entry normally gets written). `acceptEdits`/`bypassPermissions`
5319
- * sessions are exempt because their `thread/start` (workspace-write /
5320
- * danger-full-access sandbox) writes the trust entry itself and loads the
5321
- * config — measured against 0.146.0 and 0.149.0; a notice there would be
5322
- * false. Emitted as `session_error`, which both clients render as an inline
5323
- * notice while the session keeps running (the backfill-history precedent),
5324
- * so nothing new rides the wire. Every degrade path is silence: a false
5325
- * warning on a trusted project is worse than a missed one.
5326
- */
5327
4031
  #warnUntrustedProject() {
5328
4032
  if (this.#permissionMode !== "default") return;
5329
4033
  try {
@@ -5341,20 +4045,6 @@ var CodexRunner = class {
5341
4045
  });
5342
4046
  } catch {}
5343
4047
  }
5344
- /**
5345
- * List skills over a **throwaway** connection, for a session with nothing else
5346
- * to do yet.
5347
- *
5348
- * `skills/list` needs a live child but not a thread, so this spawns one, asks,
5349
- * and closes it — rather than bringing up the session's own child early and
5350
- * leaving a codex process parked behind every session someone created and
5351
- * never typed into. The session's real connection re-lists when it arrives;
5352
- * the fingerprint compare in {@link #refreshSkills} makes that a no-op.
5353
- *
5354
- * Entirely best-effort and never awaited: a missing binary, a failed spawn or
5355
- * a rejected handshake here must not turn a session that has not started into
5356
- * a session that failed.
5357
- */
5358
4048
  async #probeSkills() {
5359
4049
  let connection;
5360
4050
  try {
@@ -5365,28 +4055,10 @@ var CodexRunner = class {
5365
4055
  connection?.close();
5366
4056
  }
5367
4057
  }
5368
- /**
5369
- * A handshaken child that is **not** the session's — for the questions a
5370
- * client can ask before the session has anything to run (its skills, its MCP
5371
- * servers). The caller owns it and must close it.
5372
- *
5373
- * No onNotification/onRequest/onClose wiring on purpose: this child answers
5374
- * one question and goes away, so its notifications are noise and its death is
5375
- * not the session's problem. The alternative — bringing the session's real
5376
- * child up early — would park a codex process behind every session someone
5377
- * created and never typed into.
5378
- */
5379
4058
  async #openScratchConnection() {
5380
4059
  const connection = this.#config.connectFn({ env: this.#childEnv() });
5381
4060
  try {
5382
- await connection.request("initialize", {
5383
- clientInfo: {
5384
- name: "workerdeck",
5385
- title: "WorkerDeck",
5386
- version: `protocol-${PROTOCOL_VERSION}`
5387
- },
5388
- capabilities: { experimentalApi: true }
5389
- });
4061
+ await connection.request("initialize", INITIALIZE_PARAMS);
5390
4062
  connection.notify("initialized");
5391
4063
  return connection;
5392
4064
  } catch (error) {
@@ -5421,12 +4093,6 @@ var CodexRunner = class {
5421
4093
  this.#queue.push({ input });
5422
4094
  this.#scheduleTurn();
5423
4095
  }
5424
- /**
5425
- * App-server input for a message with attachments: images land in a session
5426
- * temp dir and travel as `localImage` host paths, text files inline into the
5427
- * prompt in the shared named envelope, PDF has no representation (the
5428
- * gateway's 415 normally refuses it first).
5429
- */
5430
4096
  #buildInput(text, attachments) {
5431
4097
  const parts = [];
5432
4098
  for (const attachment of attachments) {
@@ -5459,8 +4125,6 @@ var CodexRunner = class {
5459
4125
  });
5460
4126
  return parts;
5461
4127
  }
5462
- /** Resolve a pending approval. Returns false if the id is unknown (e.g.
5463
- * timed out, or already settled by codex itself). */
5464
4128
  resolvePermission(requestId, decision) {
5465
4129
  const pending = this.#approvals.get(requestId);
5466
4130
  if (!pending) return false;
@@ -5476,42 +4140,12 @@ var CodexRunner = class {
5476
4140
  await this.#interruptTurn();
5477
4141
  await this.#turnChain;
5478
4142
  }
5479
- /**
5480
- * Reset the conversation: a **fresh thread on the same session**.
5481
- *
5482
- * Codex has no clear/reset RPC — `thread/compact/start` summarises and
5483
- * continues, `thread/fork` makes a second thread, and neither is "same
5484
- * session, empty context". So the analog is to stop resuming the old thread
5485
- * and start a new one, which is the path a dead child already takes minus the
5486
- * resume. The old thread is NOT deleted: it stays in CODEX_HOME and stays
5487
- * resumable from `GET /sdk-sessions`.
5488
- *
5489
- * Two things it does on the way through, both mirroring the Claude engine's
5490
- * SDK-driven reset (`engines/claude/runner.ts`):
5491
- *
5492
- * 1. **The new thread id is adopted before `conversation_reset` is emitted**,
5493
- * whenever a child is already up — the eager `thread/start` costs no
5494
- * tokens and no model call, and it is what keeps the dormant record from
5495
- * ever naming the conversation that was just cleared. With no child there
5496
- * is nothing to start against and the id is simply dropped; the parking
5497
- * service treats a resumable session with no engine session id as one with
5498
- * nothing to come back to, and forgets the stale record.
5499
- * 2. **The context reading is retired**, in `#emit`'s `conversation_reset`
5500
- * arm. Codex cannot re-poll it the way Claude does — the only source is
5501
- * `thread/tokenUsage/updated`, which arrives *during* a turn — so there is
5502
- * no reading at all until the next turn runs, and the protocol's rule
5503
- * applies: render nothing rather than a stale ring or a 0%.
5504
- *
5505
- * The turn counter stays monotonic across this, on purpose (it is an unread
5506
- * cursor, not an item count), and so does `activityCount` — `#emit` owns both.
5507
- */
5508
4143
  async clearContext() {
5509
4144
  if (this.#closed) throw new Error("session is closed");
5510
4145
  const run = this.#turnChain.then(() => this.#clearNow());
5511
4146
  this.#turnChain = run.then(() => void 0, () => void 0);
5512
4147
  await run;
5513
4148
  }
5514
- /** The clear itself, only ever called as a turn-chain link. */
5515
4149
  async #clearNow() {
5516
4150
  if (this.#closed) throw new Error("session is closed");
5517
4151
  const previousThread = this.#sdkSessionId;
@@ -5536,8 +4170,6 @@ var CodexRunner = class {
5536
4170
  sdkSessionId: this.#sdkSessionId
5537
4171
  });
5538
4172
  }
5539
- /** Address the in-flight turn only (no approval sweep) — also the follow-up
5540
- * for a deny+interrupt whose wire decision couldn't carry the interrupt. */
5541
4173
  async #interruptTurn() {
5542
4174
  const active = this.#activeTurn;
5543
4175
  const connection = this.#connection;
@@ -5606,37 +4238,15 @@ var CodexRunner = class {
5606
4238
  });
5607
4239
  this.#setStatus("closed");
5608
4240
  }
5609
- /** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
5610
- * "show everything" on one row, so a per-runner seq index would be a map
5611
- * maintained on every emit to save a walk nobody makes twice a minute. */
5612
4241
  eventAt(seq) {
5613
- return this.#events.find((event) => event.seq === seq);
4242
+ return this.#log.at(seq);
5614
4243
  }
5615
4244
  subscribe(listener, afterSeq = 0, options) {
5616
- return this.#subscribers.subscribe(this.#events, listener, afterSeq, options, this.#resetSeq);
4245
+ return this.#subscribers.subscribe(this.#log.events, listener, afterSeq, options, this.#log.resetSeq);
5617
4246
  }
5618
4247
  #scheduleTurn() {
5619
4248
  this.#turnChain = this.#turnChain.then(() => this.#runTurn());
5620
4249
  }
5621
- /**
5622
- * Read `[sandbox_workspace_write]` as codex resolves it for this session's
5623
- * cwd, once per child, so {@link CodexRunner.#turnSandboxPolicy} can restate
5624
- * it verbatim.
5625
- *
5626
- * Why this exists at all: `turn/start`'s object-form sandbox policy is
5627
- * serde-defaulted field by field, so `{type: 'workspaceWrite'}` bare means
5628
- * `networkAccess: false, writableRoots: []` NO MATTER what the operator
5629
- * configured — and we must keep sending the object every turn, because
5630
- * restating it is what makes a between-turns permission-mode switch take
5631
- * effect. Measured against 0.149.0 with `network_access = true` set: the
5632
- * bare object produced `curl: (6) Could not resolve host`, the fully-stated
5633
- * object and an omitted policy both produced `200`. `read-only` is not
5634
- * affected — the setting is scoped to workspace-write, as its name says, and
5635
- * a read-only sandbox has no network either way.
5636
- *
5637
- * A failure here is not fatal: `#workspaceWrite` stays undefined and we send
5638
- * the bare shape, which is exactly the behaviour that shipped before.
5639
- */
5640
4250
  async #readWorkspaceWrite(connection) {
5641
4251
  this.#workspaceWrite = void 0;
5642
4252
  try {
@@ -5651,7 +4261,6 @@ var CodexRunner = class {
5651
4261
  };
5652
4262
  } catch {}
5653
4263
  }
5654
- /** The mode's turn-level sandbox policy, with the operator's workspace-write settings intact. */
5655
4264
  #turnSandboxPolicy() {
5656
4265
  const policy = TURN_SANDBOX_BY_MODE[this.#permissionMode];
5657
4266
  if (policy?.type !== "workspaceWrite" || !this.#workspaceWrite) return policy;
@@ -5660,13 +4269,6 @@ var CodexRunner = class {
5660
4269
  ...this.#workspaceWrite
5661
4270
  };
5662
4271
  }
5663
- /**
5664
- * The session's live connection with its thread loaded, (re)building both as
5665
- * needed: spawn + `initialize`/`initialized` on a fresh child, then
5666
- * `thread/start` (new) or `thread/resume` (a create-request `resume`, or a
5667
- * thread orphaned by a dead child). The response's resolved model/effort are
5668
- * kept so per-turn overrides can name "the profile default" explicitly.
5669
- */
5670
4272
  async #ensureThread() {
5671
4273
  if (this.#closed) throw new Error("session is closed");
5672
4274
  let connection = this.#connection;
@@ -5689,18 +4291,11 @@ var CodexRunner = class {
5689
4291
  this.#activeTurn?.reject(new Error(message));
5690
4292
  });
5691
4293
  try {
5692
- await connection.request("initialize", {
5693
- clientInfo: {
5694
- name: "workerdeck",
5695
- title: "WorkerDeck",
5696
- version: `protocol-${PROTOCOL_VERSION}`
5697
- },
5698
- capabilities: { experimentalApi: true }
5699
- });
4294
+ await connection.request("initialize", INITIALIZE_PARAMS);
5700
4295
  } catch (error) {
5701
4296
  connection.close();
5702
4297
  if (this.#connection === connection) this.#connection = void 0;
5703
- if (error instanceof JsonRpcError) throw new Error("codex app-server rejected initialize (capabilities.experimentalApi: true — required for the granular approval policy, and WorkerDeck has no non-experimental fallback): " + error.message);
4298
+ if (error instanceof JsonRpcError) throw new Error("codex app-server rejected initialize (capabilities.experimentalApi: true — required for the granular approval policy, and WorkerDeck has no non-experimental fallback): " + error.message, { cause: error });
5704
4299
  throw error;
5705
4300
  }
5706
4301
  connection.notify("initialized");
@@ -5731,21 +4326,6 @@ var CodexRunner = class {
5731
4326
  this.#refreshSkills(connection);
5732
4327
  return connection;
5733
4328
  }
5734
- /**
5735
- * Re-read `skills/list` and publish it, if it changed.
5736
- *
5737
- * **`cwds` is passed explicitly, and must be.** The schema documents the empty
5738
- * case as "the current session working directory", which reads like the
5739
- * thread's — it is not. Measured against 0.146.0: with no `cwds`, and *after*
5740
- * a `thread/start` carrying this session's cwd, the response comes back keyed
5741
- * to the app-server child's own process directory (for WorkerDeck, wherever
5742
- * the gateway was launched) and reports no repo-scoped skills at all. So a
5743
- * project's own `.codex/skills/**` were invisible until this argument existed.
5744
- *
5745
- * Best-effort throughout. A binary too old to know the method, a broken
5746
- * manifest, a child that died mid-call — none of that is worth failing a
5747
- * session over, and the panel simply stays absent.
5748
- */
5749
4329
  async #refreshSkills(connection) {
5750
4330
  if (this.#skillsRefresh) return this.#skillsRefresh;
5751
4331
  const run = (async () => {
@@ -5775,33 +4355,8 @@ var CodexRunner = class {
5775
4355
  this.#skillsRefresh = run;
5776
4356
  return run;
5777
4357
  }
5778
- /**
5779
- * The session's MCP servers, live from the binary.
5780
- *
5781
- * Two sources merged, because codex splits them: `mcpServerStatus/list` says
5782
- * what is configured and what each server exposes (including every tool's
5783
- * full JSON Schema, which the Agent SDK does not give us), and the
5784
- * `mcpServer/startupStatus/updated` notifications say which of them are
5785
- * actually up.
5786
- *
5787
- * Answers **before the session has connected**, over a throwaway child, for
5788
- * the same reason the skill list does: a codex session spawns nothing until
5789
- * it has work, and a panel that said "no MCP servers configured" until the
5790
- * first turn would be stating something false about the operator's config.
5791
- * The request blocks until the servers are enumerated (measured: complete on
5792
- * the very first call), so there is no half-populated answer to race.
5793
- *
5794
- * Resolves undefined only when there is genuinely nothing to say — the
5795
- * session is closed, or the child could not be spoken to. The route turns
5796
- * that into a 501.
5797
- *
5798
- * **Listing only.** There is no per-server reconnect or toggle on this
5799
- * transport — hence no `reconnectMcpServer`/`setMcpServerEnabled` here, and
5800
- * `ENGINE_CAPABILITIES.codex.mcpServerActions: false` so clients render the
5801
- * panel read-only instead of offering buttons that cannot work.
5802
- */
5803
4358
  async mcpServers() {
5804
- if (this.#closed) return void 0;
4359
+ if (this.#closed) return;
5805
4360
  const live = this.#connection;
5806
4361
  let scratch;
5807
4362
  try {
@@ -5812,15 +4367,6 @@ var CodexRunner = class {
5812
4367
  scratch?.close();
5813
4368
  }
5814
4369
  }
5815
- /**
5816
- * Announce a file the ENGINE wrote on the host, so a client can fetch it
5817
- * without the operator having declared its directory as a host-file root.
5818
- *
5819
- * Deliberately narrow: only paths codex reports as *written by its own tool*
5820
- * belong here. A path the model merely read (`imageView`) is an agent-chosen
5821
- * claim, and those keep going through `/fs/*` and its root allowlist — see
5822
- * the note on `file_produced` in the protocol.
5823
- */
5824
4370
  #emitFileProduced(path, toolUseId) {
5825
4371
  if (this.#producedPaths.has(path)) return;
5826
4372
  this.#producedPaths.add(path);
@@ -5838,16 +4384,6 @@ var CodexRunner = class {
5838
4384
  toolUseId
5839
4385
  });
5840
4386
  }
5841
- /**
5842
- * On resume, replay the thread's prior turns as `replay: true` events,
5843
- * seq'd before any live turn — the SessionRunner backfill contract, fed
5844
- * from `thread/resume`'s own `thread.turns`. When the resume response says
5845
- * that page is partial (`turnsBackwardsCursor`), the FULL rollout history
5846
- * is fetched via `thread/read {includeTurns: true}` instead — and if even
5847
- * that fails, the partial page is replayed under a visible notice rather
5848
- * than silently posing as the whole thread. Best-effort like the Claude
5849
- * backfill: an unreadable history never blocks the resume itself.
5850
- */
5851
4387
  async #backfillHistory() {
5852
4388
  try {
5853
4389
  if (this.#closed) return;
@@ -5876,7 +4412,6 @@ var CodexRunner = class {
5876
4412
  this.#setStatus("idle");
5877
4413
  }
5878
4414
  }
5879
- /** Replay historical turns through the SAME item mapping the live path uses. */
5880
4415
  #replayTurns(turns) {
5881
4416
  for (const turn of turns) {
5882
4417
  if (this.#closed) return;
@@ -5905,8 +4440,6 @@ var CodexRunner = class {
5905
4440
  }
5906
4441
  }
5907
4442
  }
5908
- /** Fresh per-turn state — one per live turn, and one per REPLAYED turn (the
5909
- * nonce is the item-id namespace, and its per-turn-ness is the invariant). */
5910
4443
  #newTurnState() {
5911
4444
  return {
5912
4445
  nonce: randomUUID(),
@@ -5995,10 +4528,6 @@ var CodexRunner = class {
5995
4528
  }
5996
4529
  this.#notifications[method]?.(params);
5997
4530
  }
5998
- /** Whether a notification is about the session's own thread. A notification
5999
- * with no `threadId` counts as the root's: every thread-scoped method the
6000
- * schema defines carries one, so an absent id means an older or narrower
6001
- * shape, not a sub-agent. */
6002
4531
  #isRootThread(params) {
6003
4532
  const threadId = this.#threadIdOf(params);
6004
4533
  if (threadId === void 0) return true;
@@ -6008,42 +4537,18 @@ var CodexRunner = class {
6008
4537
  const threadId = params?.threadId;
6009
4538
  return typeof threadId === "string" ? threadId : void 0;
6010
4539
  }
6011
- /**
6012
- * The agent behind a notification's `threadId` — the attribution every item
6013
- * and delta handler asks before emitting, so two agents streaming
6014
- * concurrently into this one connection come apart again by the id each
6015
- * frame carries, never by any mutable "current agent".
6016
- *
6017
- * A non-root thread with no record still gets one: a thread emitting items on
6018
- * this connection *is* an agent, whatever announced it (codex runs threads of
6019
- * its own for review/compact, and a `subAgentActivity` could in principle be
6020
- * missed) — the claude tracker's nested-event fallback, on a stronger signal.
6021
- * The minted record is label-less and its anchor is authored here, because an
6022
- * attributed event whose parent id matches no top-level `tool_use` would
6023
- * render inline rather than as a frame; a late `started` edge fills the name
6024
- * in. Root-thread traffic — and, defensively, the pre-thread shapes with no
6025
- * id at all — stays unattributed (`undefined`).
6026
- */
6027
4540
  #agentFor(params) {
6028
4541
  const threadId = this.#threadIdOf(params);
6029
- if (threadId === void 0 || threadId === this.#sdkSessionId) return void 0;
4542
+ if (threadId === void 0 || threadId === this.#sdkSessionId) return;
6030
4543
  const known = this.#agents.get(threadId);
6031
4544
  if (known) return known;
6032
- if (this.#clearedThreads.has(threadId)) return void 0;
4545
+ if (this.#clearedThreads.has(threadId)) return;
6033
4546
  const nonce = this.#activeTurn?.nonce ?? "codex";
6034
4547
  const record = this.#agents.open(threadId, `${nonce}:agent:${threadId}`, void 0, Date.now());
6035
4548
  record.anchored = true;
6036
4549
  this.#emitToolUse(record.toolUseId, CODEX_AGENT_TOOL, { agentThreadId: threadId });
6037
4550
  return record;
6038
4551
  }
6039
- /**
6040
- * A child thread's `turn/completed` is that AGENT's completion — the one
6041
- * codex sends (`subAgentActivity` has no 'completed' kind, verified live).
6042
- * The verdict is the turn's own status, and the report is the completed
6043
- * turn's final message, delivered as the anchor's `tool_result` so the row
6044
- * settles exactly the way a claude `Task`'s does. Deliberately not gated on
6045
- * `#activeTurn`: an agent finishing between root turns still finished.
6046
- */
6047
4552
  #settleAgentTurn(params) {
6048
4553
  const threadId = this.#threadIdOf(params);
6049
4554
  const record = threadId ? this.#agents.get(threadId) : void 0;
@@ -6054,11 +4559,6 @@ var CodexRunner = class {
6054
4559
  const report = (turn ? turnReport(turn) : void 0) ?? turn?.error?.message ?? (status === "done" ? "" : turn?.status ?? "failed");
6055
4560
  this.#emitToolResult(record.toolUseId, report, status === "failed");
6056
4561
  }
6057
- /** Reasoning deltas arrive on two methods that differ only in which section
6058
- * counter they advance; the section key carries the method so the two streams
6059
- * never share a boundary (and item ids are per-thread, so two agents' streams
6060
- * never share one either). Section boundaries (a new summary/content entry)
6061
- * render as paragraph breaks — the completed item joins sections with '\n\n'. */
6062
4562
  #reasoningDelta(method) {
6063
4563
  return (params) => {
6064
4564
  const active = this.#activeTurn;
@@ -6066,26 +4566,23 @@ var CodexRunner = class {
6066
4566
  const payload = params;
6067
4567
  if (typeof payload?.delta !== "string" || !payload.delta) return;
6068
4568
  const index = payload.contentIndex ?? payload.summaryIndex ?? 0;
6069
- const key = `${payload.itemId ?? ""}:${method}`;
4569
+ const agent = this.#agentFor(params);
4570
+ const key = `${agent?.toolUseId ?? ""}:${payload.itemId ?? ""}:${method}`;
6070
4571
  const previous = active.sectionIndex.get(key);
6071
4572
  active.sectionIndex.set(key, index);
6072
4573
  const separator = previous !== void 0 && index > previous ? "\n\n" : "";
6073
4574
  this.#emitDelta({
6074
4575
  type: "thinking_delta",
6075
4576
  thinking: separator + payload.delta
6076
- }, this.#agentFor(params)?.toolUseId ?? null);
4577
+ }, agent?.toolUseId ?? null);
6077
4578
  };
6078
4579
  }
6079
- /** One item-progress handler serves `item/started` and `item/updated`. */
6080
4580
  #itemProgress = (params) => {
6081
4581
  const active = this.#activeTurn;
6082
4582
  if (!active) return;
6083
4583
  const item = params?.item;
6084
4584
  if (item) this.#handleItemProgress(item, active, this.#agentFor(params));
6085
4585
  };
6086
- /** The notification dispatch table — every method the child emits that this
6087
- * runner maps, in one place. Handlers read `this.#activeTurn` themselves:
6088
- * dispatch is synchronous, so the read is the same one the old switch made. */
6089
4586
  #notifications = {
6090
4587
  "thread/started": (params) => {
6091
4588
  const thread = params?.thread;
@@ -6180,26 +4677,17 @@ var CodexRunner = class {
6180
4677
  return;
6181
4678
  }
6182
4679
  },
6183
- "error": (params) => {
4680
+ error: (params) => {
6184
4681
  const active = this.#activeTurn;
6185
4682
  const error = params?.error;
6186
4683
  if (active && typeof error?.message === "string") active.lastError = error.message;
6187
4684
  }
6188
4685
  };
6189
- /** Answer a server→client request: the ask channels become pending
6190
- * permission requests; anything else gets a JSON-RPC -32601 rather than a
6191
- * hang (an unanswered server request wedges the turn). */
6192
4686
  async #answerServerRequest(method, params, wireId) {
6193
4687
  const channel = APPROVAL_CHANNELS[method];
6194
4688
  if (channel) return this.#requestApproval(channel, method, params, wireId);
6195
4689
  throw new JsonRpcError(-32601, `workerdeck does not handle server request '${method}'`);
6196
4690
  }
6197
- /**
6198
- * Surface one ask-channel request as a pending {@link PermissionRequest};
6199
- * the returned promise is the JSON-RPC response, resolved when a
6200
- * `permission_decision` lands — or by the timeout, an interrupt, turn end,
6201
- * session close, or codex resolving it itself. Never left hanging.
6202
- */
6203
4691
  #requestApproval(channel, method, params, wireId) {
6204
4692
  if (method === "item/tool/requestUserInput") {
6205
4693
  const behavior = this.#config.questionBehavior ?? "ask";
@@ -6238,9 +4726,6 @@ var CodexRunner = class {
6238
4726
  if (this.#activeTurn) this.#setStatus("awaiting_approval");
6239
4727
  });
6240
4728
  }
6241
- /** 'auto'/'deny' sessions settle codex questions synchronously instead of
6242
- * pending. Request/resolved events still fire so transcripts and job
6243
- * webhooks show what was chosen. */
6244
4729
  #resolveQuestionByPolicy(channel, params, mode) {
6245
4730
  const itemId = channel.itemId(params);
6246
4731
  const request = {
@@ -6275,13 +4760,6 @@ var CodexRunner = class {
6275
4760
  });
6276
4761
  return { answers };
6277
4762
  }
6278
- /**
6279
- * Settle one pending approval: pick the channel's wire response for the
6280
- * decision, answer the JSON-RPC request, and emit `permission_resolved`.
6281
- * An allow the request offered no plain accept for becomes the channel's
6282
- * denial, said out loud — never a silently widened grant, and never a
6283
- * decision the request didn't offer.
6284
- */
6285
4763
  #settleApproval(id, pending, decision, resolvedBy) {
6286
4764
  clearTimeout(pending.timer);
6287
4765
  this.#approvals.delete(id);
@@ -6309,9 +4787,6 @@ var CodexRunner = class {
6309
4787
  if (behavior === "deny" && decision.behavior === "deny" && decision.interrupt && sent.decision !== "cancel") this.#interruptTurn();
6310
4788
  if (!this.#closed && this.#approvals.size === 0 && this.#status === "awaiting_approval") this.#setStatus("running");
6311
4789
  }
6312
- /** Tool calls surface as tool_use when they start; text and reasoning stream
6313
- * natively via the delta notifications. `agent` is the sub-agent whose thread
6314
- * the item arrived on — undefined for the session's own. */
6315
4790
  #handleItemProgress(item, active, agent) {
6316
4791
  const id = `${active.nonce}:${item.id}`;
6317
4792
  if (item.type === "subAgentActivity") {
@@ -6355,14 +4830,6 @@ var CodexRunner = class {
6355
4830
  }
6356
4831
  });
6357
4832
  }
6358
- /**
6359
- * The completed-item mapping, one handler per member of the {@link AppServerItem}
6360
- * union. The mapped type is the invariant made checkable: model a new item
6361
- * type in `types.ts` and this table fails to compile until it says what the
6362
- * item becomes on the wire — the old switch silently fell through to the
6363
- * unknown-item passthrough instead. (The runtime still receives types the
6364
- * union has never heard of; those take the passthrough above.)
6365
- */
6366
4833
  #itemCompleted = {
6367
4834
  userMessage: (item, active, _id, agent) => {
6368
4835
  if (!agent) return;
@@ -6502,10 +4969,6 @@ var CodexRunner = class {
6502
4969
  uuid
6503
4970
  });
6504
4971
  }
6505
- /** `agent` (rather than a bare parent id) because a nested call is also the
6506
- * agent's progress reading: `SubagentInfo.toolCount` ticks here, once per
6507
- * card — the `counted` set is what keeps an upserted re-emission (the
6508
- * finished imageGeneration input) from counting one picture twice. */
6509
4972
  #emitToolUse(id, name, input, agent) {
6510
4973
  if (agent && !agent.counted.has(id)) {
6511
4974
  agent.counted.add(id);
@@ -6545,14 +5008,6 @@ var CodexRunner = class {
6545
5008
  uuid: `${toolUseId}-result`
6546
5009
  });
6547
5010
  }
6548
- /**
6549
- * Per-turn usage re-mapped to the Anthropic accounting convention the whole
6550
- * stack assumes (GOTCHAS §Codex engine): OpenAI's `inputTokens` includes the
6551
- * cached share, so input excludes it (else queue token budgets double-count
6552
- * cache-heavy runs); reasoning tokens are billed output; `totalCostUsd: 0` =
6553
- * unknown, the AiSdkRunner precedent. Usage is summed from the turn's
6554
- * `thread/tokenUsage/updated` notifications — `turn/completed` carries none.
6555
- */
6556
5011
  #finishTurn(kind, startedAt, active, errors) {
6557
5012
  for (const [id, pending] of this.#approvals) this.#settleApproval(id, pending, {
6558
5013
  behavior: "deny",
@@ -6580,23 +5035,6 @@ var CodexRunner = class {
6580
5035
  this.#emitContextUsage(active);
6581
5036
  this.#setStatus("idle");
6582
5037
  }
6583
- /**
6584
- * Subscription windows, mapped onto the protocol's named vocabulary.
6585
- *
6586
- * The shapes disagree: codex reports windows *positionally* (`primary` /
6587
- * `secondary`) with a length in minutes, while `RateLimitInfo.rateLimitType`
6588
- * is a name whose meaning clients already know — iOS labels `seven_day` as
6589
- * "Weekly" and derives the pace marker's denominator from it. Naming the
6590
- * window by its measured duration is therefore the honest mapping rather
6591
- * than a borrowed one: codex's primary window is 10080 minutes, which *is*
6592
- * seven days. A duration we have no name for keeps an explicit
6593
- * `window_<n>m` key — clients render it verbatim and simply draw no pace
6594
- * marker, which beats mislabeling it as a week.
6595
- *
6596
- * `status` is 'allowed' by construction (the session is running), matching
6597
- * `rateLimitEventsFromUsage`; codex's `rateLimitReachedType` is the one
6598
- * signal that a limit is actually biting, so it becomes 'rejected'.
6599
- */
6600
5038
  #emitRateLimits(limits) {
6601
5039
  if (!limits) return;
6602
5040
  const status = limits.rateLimitReachedType ? "rejected" : "allowed";
@@ -6620,16 +5058,6 @@ var CodexRunner = class {
6620
5058
  });
6621
5059
  }
6622
5060
  }
6623
- /**
6624
- * Context occupancy, after the turn — the same cadence the Claude runner
6625
- * polls `getContextUsage()` on, so clients need nothing new.
6626
- *
6627
- * Emitted only when the binary gave BOTH numbers: the protocol is explicit
6628
- * that a client renders nothing rather than a 0% ring, and a window of
6629
- * `null` (which app-server does send) would otherwise divide into a
6630
- * meaningless percentage. `categories` is empty because codex publishes no
6631
- * breakdown — clients must not render an empty "Breakdown" section for it.
6632
- */
6633
5061
  #emitContextUsage(active) {
6634
5062
  const totalTokens = active.contextTokens;
6635
5063
  const maxTokens = active.contextWindow;
@@ -6646,9 +5074,10 @@ var CodexRunner = class {
6646
5074
  });
6647
5075
  }
6648
5076
  #setStatus(status, detail) {
6649
- if (this.#status === status) return;
5077
+ if (this.#status === status && this.#statusDetail === detail) return;
6650
5078
  if (this.#status === "closed" || this.#status === "failed") return;
6651
5079
  this.#status = status;
5080
+ this.#statusDetail = detail;
6652
5081
  this.#emit({
6653
5082
  type: "status_changed",
6654
5083
  status,
@@ -6660,32 +5089,26 @@ var CodexRunner = class {
6660
5089
  ...body,
6661
5090
  replay: true
6662
5091
  };
6663
- const event = {
6664
- ...body,
6665
- seq: ++this.#seq,
6666
- ts: Date.now()
6667
- };
6668
- this.#lastActivityAt = event.ts;
6669
- this.#activityCount += transcriptActivity(body);
6670
- this.#contextUsage = contextReading(body) ?? this.#contextUsage;
6671
- if (body.type === "conversation_reset") {
6672
- this.#resetSeq = event.seq;
6673
- this.#contextUsage = void 0;
6674
- }
6675
- this.#events.push(event);
6676
- this.#subscribers.emit(event);
5092
+ this.#subscribers.emit(this.#log.append(body));
6677
5093
  }
6678
5094
  };
6679
5095
  //#endregion
6680
5096
  //#region src/engines/codex/catalog.ts
6681
5097
  /**
6682
- * The Codex engine's model catalog, seeded from the binary's own embedded
6683
- * presets `@openai/codex@0.146.0` ships its model table inside the
6684
- * executable, and that table (not the SDK's stale `ModelReasoningEffort`
6685
- * union) is the truth about which reasoning efforts each model takes.
5098
+ * The Codex engine's model catalog, seeded from the binary's own embedded model
5099
+ * table (see `provenance` for the version) — that table, not the SDK's stale
5100
+ * `ModelReasoningEffort` union, is the truth about which reasoning efforts each
5101
+ * model takes. Mapping decisions: the internal `codex-auto-review` row is dropped
5102
+ * (the codex analogue of the CLI's `default` sentinel), `primary` mirrors the
5103
+ * binary's own `visibility` field so both UIs group the way codex's picker does,
5104
+ * and `reasoningEfforts` carries `supported_reasoning_levels` verbatim — `max`
5105
+ * and `ultra` go beyond the SDK union, so trust the binary and keep strings open.
6686
5106
  *
6687
- * **Refresh procedure** (release checklist): extract the embedded JSON from
6688
- * the platform binary and diff —
5107
+ * **Refresh procedure** (release checklist): extract the embedded JSON from the
5108
+ * platform binary and diff. The two-hop resolve is NOT optional under pnpm's
5109
+ * strict layout the platform package is a dependency of `@openai/codex` and
5110
+ * resolves only from that wrapper's location (MODULE_NOT_FOUND otherwise), the
5111
+ * same two hops `resolveBundledCodexExecutable` makes.
6689
5112
  *
6690
5113
  * node -e 'const d=require("fs").readFileSync(process.argv[1]);
6691
5114
  * const s=d.indexOf(`{\n "models": [`);
@@ -6697,23 +5120,9 @@ var CodexRunner = class {
6697
5120
  * const w=require.resolve("@openai/codex/package.json");
6698
5121
  * createRequire(w).resolve("@openai/codex-darwin-arm64/package.json")
6699
5122
  * .replace("package.json","vendor/aarch64-apple-darwin/bin/codex")')"
6700
- *
6701
- * The two-hop resolve is NOT optional: under pnpm's strict layout the platform
6702
- * package is a dependency of `@openai/codex`, so it resolves only from that
6703
- * wrapper's location, never from the repo root. Resolving it directly throws
6704
- * MODULE_NOT_FOUND — the same two hops `resolveBundledCodexExecutable` makes.
6705
- *
6706
- * Mapping decisions:
6707
- * - the internal `codex-auto-review` row is dropped (the codex analogue of
6708
- * dropping the CLI's `default` sentinel);
6709
- * - `primary` mirrors the binary's own `visibility` field ('list' = shown in
6710
- * its picker, 'hide' = its "older models"), so both UIs group the way
6711
- * codex's own picker does;
6712
- * - `reasoningEfforts` carries `supported_reasoning_levels` verbatim — note
6713
- * `max`/`ultra` beyond the SDK union; trust the binary, keep strings open.
6714
5123
  */
6715
5124
  const CODEX_CATALOG = {
6716
- provenance: "embedded model presets of @openai/codex@0.149.0 (darwin-arm64 binary), extracted 2026-08-22",
5125
+ provenance: "embedded model presets of @openai/codex@0.151.0 (darwin-arm64 binary), re-extracted 2026-09-02 and unchanged since 0.149.0",
6717
5126
  models: [
6718
5127
  {
6719
5128
  value: "gpt-5.6-sol",
@@ -6813,19 +5222,6 @@ const CODEX_CATALOG = {
6813
5222
  };
6814
5223
  //#endregion
6815
5224
  //#region src/engines/codex/process.ts
6816
- /** How much stderr to keep for the exit diagnostic. The binary logs startup
6817
- * noise there; only the tail explains a death. */
6818
- const STDERR_TAIL_BYTES = 4096;
6819
- /**
6820
- * Spawn one `codex app-server` child and frame JSON-RPC over its stdio — the
6821
- * real {@link AppServerConnectFn}. The child's env is passed **complete**
6822
- * (a provided spawn env replaces process.env, never merges with it), with the
6823
- * profile's CODEX_HOME pin already applied by the runner.
6824
- *
6825
- * No spawn cwd: the working directory is a thread/turn parameter, and a cwd
6826
- * that doesn't exist should fail the *turn* with codex's own error, not the
6827
- * spawn.
6828
- */
6829
5225
  function connectAppServer(options) {
6830
5226
  const child = spawn(options.executable, ["app-server"], {
6831
5227
  env: options.env,
@@ -6841,7 +5237,7 @@ function connectAppServer(options) {
6841
5237
  });
6842
5238
  let stderrTail = "";
6843
5239
  child.stderr.on("data", (chunk) => {
6844
- stderrTail = (stderrTail + String(chunk)).slice(-STDERR_TAIL_BYTES);
5240
+ stderrTail = (stderrTail + String(chunk)).slice(-4096);
6845
5241
  });
6846
5242
  let closeHandler;
6847
5243
  let done = false;
@@ -6874,19 +5270,12 @@ function connectAppServer(options) {
6874
5270
  //#endregion
6875
5271
  //#region src/engines/codex/adapter.ts
6876
5272
  const NOT_INSTALLED = "@openai/codex is not installed — add it (an optional peer of @workerdeck/core) to run codex profiles";
6877
- /**
6878
- * The codex binary sessions will run: the per-platform package installed next
6879
- * to `@openai/codex`, `vendor/<target-triple>/bin/codex` — the same file the
6880
- * npm wrapper's own `bin/codex.js` launcher execs. Probing this binary rather
6881
- * than whatever `codex` is on PATH means the availability answer is about the
6882
- * executable sessions will actually run. Undefined when it can't be found;
6883
- * callers degrade to 'unknown'.
6884
- */
6885
5273
  function resolveBundledCodexExecutable() {
6886
5274
  const triple = targetTriple();
6887
- if (!triple) return void 0;
5275
+ if (!triple) return;
6888
5276
  try {
6889
- const path = createRequire(createRequire(import.meta.url).resolve("@openai/codex/package.json")).resolve(`@openai/codex-${platformPackageSuffix()}/package.json`).replace(/package\.json$/, `vendor/${triple}/bin/codex`);
5277
+ const wrapper = createRequire(import.meta.url).resolve("@openai/codex/package.json");
5278
+ const path = createRequire(wrapper).resolve(`@openai/codex-${platformPackageSuffix()}/package.json`).replace(/package\.json$/, `vendor/${triple}/bin/codex`);
6890
5279
  if (existsSync(path)) return path;
6891
5280
  } catch {}
6892
5281
  }
@@ -6899,34 +5288,13 @@ function targetTriple() {
6899
5288
  function platformPackageSuffix() {
6900
5289
  return `${process.platform}-${process.arch}`;
6901
5290
  }
6902
- /**
6903
- * Availability, mirroring **the app-server surface's actual credential chain**
6904
- * (verified 2026-08-05 against 0.146.0 by driving the raw binary): auth comes
6905
- * solely from the CODEX_HOME auth store (`codex login`, file or keyring). The
6906
- * env-key routes are dead ends here — `CODEX_API_KEY` is read only by
6907
- * `codex exec` (a turn goes out with no credential at all: "Missing bearer"),
6908
- * and `OPENAI_API_KEY` was never read by either surface. So, in order:
6909
- *
6910
- * 1. Binary resolvable, else unavailable with the install reason;
6911
- * 2. `codex login status` under the profile's complete session env:
6912
- * exit 0 → available; the "Not logged in" verdict → unavailable, with an
6913
- * exact remedy when a stranded env key explains the misconfiguration;
6914
- * anything else (a stray CODEX_ACCESS_TOKEN JWT error, a crashed spawn) →
6915
- * 'unknown' — the checkClaudeAuth never-overclaim discipline.
6916
- *
6917
- * Only the exit code and the fixed verdict line are consulted — never
6918
- * surfaced: `login status` output includes a masked key fragment. The
6919
- * `smoke:codex --canary` run is the drift alarm for all of this.
6920
- */
6921
5291
  async function checkCodexAvailability(profile, env, options = {}) {
6922
5292
  const executable = resolveBundledCodexExecutable();
6923
5293
  if (!executable) return {
6924
5294
  available: false,
6925
5295
  reason: NOT_INSTALLED
6926
5296
  };
6927
- const childEnv = {};
6928
- for (const [key, value] of Object.entries(env)) if (value !== void 0) childEnv[key] = value;
6929
- if (profile.codexHome) childEnv.CODEX_HOME = profile.codexHome;
5297
+ const childEnv = codexChildEnv(env, profile.codexHome);
6930
5298
  return new Promise((resolve) => {
6931
5299
  execFile(executable, ["login", "status"], {
6932
5300
  env: childEnv,
@@ -6948,24 +5316,18 @@ async function checkCodexAvailability(profile, env, options = {}) {
6948
5316
  });
6949
5317
  });
6950
5318
  }
6951
- /** `thread/list` page size (its own default is 25) and a hard page bound so a
6952
- * misbehaving cursor can never spin the listing forever. */
6953
5319
  const LIST_PAGE_SIZE = 100;
6954
5320
  const MAX_LIST_PAGES = 40;
6955
- /** thread/list's `cwd` filter is an EXACT path match (measured, 0.146.0), so
6956
- * offer both the spelled and canonical forms — macOS listings would otherwise
6957
- * miss `/tmp/...` threads recorded under `/private/tmp/...`. */
6958
5321
  function cwdFilter(dir) {
6959
- const forms = new Set([dir]);
5322
+ const forms = /* @__PURE__ */ new Set([dir]);
6960
5323
  try {
6961
5324
  forms.add(realpathSync(dir));
6962
5325
  } catch {}
6963
5326
  return [...forms];
6964
5327
  }
6965
- const secondsToMs = (value) => typeof value === "number" && Number.isFinite(value) ? value * 1e3 : void 0;
6966
- /** One thread row in the protocol's browser-safe summary shape. `id` is what
6967
- * `CreateSessionRequest.resume` feeds `thread/resume` — the row's separate
6968
- * `sessionId` field is not it. */
5328
+ function secondsToMs(value) {
5329
+ return typeof value === "number" && Number.isFinite(value) ? value * 1e3 : void 0;
5330
+ }
6969
5331
  function summarizeThread(row) {
6970
5332
  const name = typeof row.name === "string" && row.name.length > 0 ? row.name : void 0;
6971
5333
  const preview = typeof row.preview === "string" && row.preview.length > 0 ? row.preview : void 0;
@@ -6980,30 +5342,12 @@ function summarizeThread(row) {
6980
5342
  cwd: typeof row.cwd === "string" ? row.cwd : void 0
6981
5343
  };
6982
5344
  }
6983
- /**
6984
- * CODEX_HOME's threads over ONE short-lived `codex app-server` child: the
6985
- * runner's own handshake (`experimentalApi` and all — one code path, no
6986
- * second vocabulary to drift), `thread/list` pages walked by cursor, child
6987
- * closed before returning. Requires no live session and costs no tokens —
6988
- * it is how "resume" is offered before anything is running. The `connectFn`
6989
- * seam exists for the scripted-peer tests; the adapter passes the real
6990
- * spawn.
6991
- */
6992
5345
  async function listCodexSessions(options) {
6993
- const childEnv = {};
6994
- for (const [key, value] of Object.entries(options.env)) if (value !== void 0) childEnv[key] = value;
6995
- if (options.profile?.codexHome) childEnv.CODEX_HOME = options.profile.codexHome;
5346
+ const childEnv = codexChildEnv(options.env, options.profile?.codexHome);
6996
5347
  const connection = options.connectFn({ env: childEnv });
6997
5348
  const rows = [];
6998
5349
  try {
6999
- await connection.request("initialize", {
7000
- clientInfo: {
7001
- name: "workerdeck",
7002
- title: "WorkerDeck",
7003
- version: `protocol-${PROTOCOL_VERSION}`
7004
- },
7005
- capabilities: { experimentalApi: true }
7006
- });
5350
+ await connection.request("initialize", INITIALIZE_PARAMS);
7007
5351
  connection.notify("initialized");
7008
5352
  const base = {
7009
5353
  limit: LIST_PAGE_SIZE,
@@ -7030,15 +5374,6 @@ async function listCodexSessions(options) {
7030
5374
  const start = options.offset ?? 0;
7031
5375
  return options.limit === void 0 ? summaries.slice(start) : summaries.slice(start, start + options.limit);
7032
5376
  }
7033
- /**
7034
- * OpenAI Codex as an engine: the codex CLI binary driven over its `app-server`
7035
- * JSON-RPC surface — structurally the Claude engine's sibling (a local agent
7036
- * binary with sessions, sandboxing and resume, resolving its own credentials
7037
- * from the operator's environment). `@openai/codex` — the npm package that
7038
- * carries the binary — is an **optional peer**: absent, every codex profile
7039
- * reports unavailable and createRunner throws the same message, and no
7040
- * consumer downloads a ~40 MB per-platform binary it never uses.
7041
- */
7042
5377
  const codexAdapter = {
7043
5378
  engine: "codex",
7044
5379
  capabilities: ENGINE_CAPABILITIES.codex,
@@ -7071,17 +5406,6 @@ const codexAdapter = {
7071
5406
  };
7072
5407
  //#endregion
7073
5408
  //#region src/engines/provider/adapter.ts
7074
- /**
7075
- * The model-agnostic provider engine as a pseudo-adapter: capabilities and an
7076
- * env-var probe live here, but its runners are assembled by the host's
7077
- * `createEngineRunner` hook (which is where provider credentials are resolved
7078
- * and model SDKs are imported — neither belongs in this repo's import graph).
7079
- * The server routes provider creates to the hook; `createRunner` here throws
7080
- * so a mis-routed call fails loudly instead of quietly building nothing.
7081
- *
7082
- * The catalog is empty by the same token: provider model ids are operator-
7083
- * declared per profile (`provider.models`), not shipped with releases.
7084
- */
7085
5409
  const providerAdapter = {
7086
5410
  engine: "provider",
7087
5411
  capabilities: ENGINE_CAPABILITIES.provider,
@@ -7110,7 +5434,6 @@ const ADAPTERS = {
7110
5434
  codex: codexAdapter,
7111
5435
  provider: providerAdapter
7112
5436
  };
7113
- /** The in-repo adapter for an engine. An absent `engine` means 'claude'. */
7114
5437
  function getEngineAdapter(engine) {
7115
5438
  return ADAPTERS[engine ?? "claude"];
7116
5439
  }