@workerdeck/core 0.13.0 → 0.16.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
- import { getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
4
- import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, transcriptActivity } from "@workerdeck/protocol";
3
+ import { getSessionInfo, getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
4
+ import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, replayCoalesceKey, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
5
5
  import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
6
6
  import { execFile, spawn } from "node:child_process";
7
7
  import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -10,7 +10,7 @@ import { z } from "zod";
10
10
  import { lookup } from "node:dns/promises";
11
11
  import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
13
- //#region src/attachments.ts
13
+ //#region src/lib/attachments.ts
14
14
  /** The four the Anthropic API accepts. Notably absent: image/heic — an iPhone's
15
15
  * native photo format, which clients must transcode before upload. */
16
16
  const IMAGE_TYPES = new Set([
@@ -103,7 +103,7 @@ function decodeText(base64) {
103
103
  return Buffer.from(base64, "base64").toString("utf8");
104
104
  }
105
105
  //#endregion
106
- //#region src/input-queue.ts
106
+ //#region src/lib/input-queue.ts
107
107
  /**
108
108
  * Push-based single-consumer AsyncIterable bridging imperative sendMessage() calls
109
109
  * into the streaming `prompt` the Agent SDK consumes.
@@ -162,7 +162,160 @@ var InputQueue = class {
162
162
  }
163
163
  };
164
164
  //#endregion
165
- //#region src/normalize.ts
165
+ //#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
+ const MAX_PATCH_LINES = 400;
190
+ function capHunks(hunks) {
191
+ const kept = [];
192
+ let lines = 0;
193
+ for (const hunk of hunks) {
194
+ if (lines + hunk.lines.length > MAX_PATCH_LINES && kept.length > 0) return {
195
+ hunks: kept,
196
+ truncated: true
197
+ };
198
+ kept.push(hunk);
199
+ lines += hunk.lines.length;
200
+ }
201
+ return { hunks: kept };
202
+ }
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
+ function isHunk(value) {
206
+ const hunk = value;
207
+ 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
+ }
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
+ function filePatchFromToolResult(result) {
218
+ const output = result;
219
+ if (!output || !Array.isArray(output.structuredPatch)) return void 0;
220
+ const hunks = output.structuredPatch.filter(isHunk);
221
+ if (hunks.length === 0) return void 0;
222
+ const { hunks: kept, truncated } = capHunks(hunks);
223
+ return {
224
+ ...typeof output.filePath === "string" && { path: output.filePath },
225
+ ...output.type === "create" || output.originalFile === null ? { kind: "create" } : output.type === "update" || typeof output.originalFile === "string" ? { kind: "update" } : {},
226
+ hunks: kept,
227
+ ...truncated && { truncated }
228
+ };
229
+ }
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
+ function parseUnifiedDiff(diff, path) {
245
+ const hunks = [];
246
+ let current;
247
+ for (const line of diff.split("\n")) {
248
+ const header = HUNK_HEADER.exec(line);
249
+ if (header) {
250
+ current = {
251
+ oldStart: Number(header[1]),
252
+ oldLines: header[2] === void 0 ? 1 : Number(header[2]),
253
+ newStart: Number(header[3]),
254
+ newLines: header[4] === void 0 ? 1 : Number(header[4]),
255
+ lines: []
256
+ };
257
+ hunks.push(current);
258
+ continue;
259
+ }
260
+ if (!current) continue;
261
+ if (line.startsWith(" ") || line.startsWith("-") || line.startsWith("+")) current.lines.push(line);
262
+ else if (line === "") current.lines.push(" ");
263
+ else current = void 0;
264
+ }
265
+ if (hunks.length === 0) return void 0;
266
+ const { hunks: kept, truncated } = capHunks(hunks);
267
+ return {
268
+ ...path && { path },
269
+ hunks: kept,
270
+ ...truncated && { truncated }
271
+ };
272
+ }
273
+ //#endregion
274
+ //#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. */
278
+ function singleToolResult(message) {
279
+ const content = message.content;
280
+ if (!Array.isArray(content)) return false;
281
+ return content.filter((block) => block.type === "tool_result").length === 1;
282
+ }
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
+ 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
+ function isSyntheticUserText(message) {
313
+ const content = message.content;
314
+ const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((block) => block.type === "text")?.text : void 0;
315
+ if (typeof text !== "string") return false;
316
+ const head = text.trimStart();
317
+ return SYNTHETIC_USER_PREFIXES.some((prefix) => head.startsWith(prefix));
318
+ }
166
319
  function toApiMessage(message) {
167
320
  const m = message;
168
321
  return {
@@ -351,14 +504,18 @@ function normalizeSdkMessage(msg) {
351
504
  parentToolUseId: msg.parent_tool_use_id,
352
505
  uuid: msg.uuid
353
506
  };
354
- case "user": return {
355
- type: "user_message",
356
- message: toApiMessage(msg.message),
357
- parentToolUseId: msg.parent_tool_use_id,
358
- replay: "isReplay" in msg && msg.isReplay === true ? true : void 0,
359
- synthetic: msg.isSynthetic === true ? true : void 0,
360
- uuid: msg.uuid
361
- };
507
+ case "user": {
508
+ const message = toApiMessage(msg.message);
509
+ return {
510
+ type: "user_message",
511
+ message,
512
+ parentToolUseId: msg.parent_tool_use_id,
513
+ replay: "isReplay" in msg && msg.isReplay === true ? true : void 0,
514
+ synthetic: msg.isSynthetic === true || msg.origin?.kind === "task-notification" || isSyntheticUserText(message) ? true : void 0,
515
+ patch: singleToolResult(message) ? filePatchFromToolResult(msg.tool_use_result) : void 0,
516
+ uuid: msg.uuid
517
+ };
518
+ }
362
519
  case "stream_event": return {
363
520
  type: "stream_delta",
364
521
  event: msg.event,
@@ -376,6 +533,10 @@ function normalizeSdkMessage(msg) {
376
533
  errors: msg.subtype === "success" ? void 0 : msg.errors,
377
534
  usage: msg.usage
378
535
  };
536
+ case "conversation_reset": return {
537
+ type: "conversation_reset",
538
+ sdkSessionId: msg.new_conversation_id
539
+ };
379
540
  case "rate_limit_event": return {
380
541
  type: "rate_limit",
381
542
  info: {
@@ -399,7 +560,41 @@ function normalizeSdkMessage(msg) {
399
560
  }
400
561
  }
401
562
  //#endregion
402
- //#region src/runner.ts
563
+ //#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
+ function staleReplaySeqs(events, afterSeq) {
584
+ const stale = /* @__PURE__ */ new Set();
585
+ const seen = /* @__PURE__ */ new Set();
586
+ for (let index = events.length - 1; index >= 0; index--) {
587
+ const event = events[index];
588
+ if (event.seq <= afterSeq) break;
589
+ const key = replayCoalesceKey(event);
590
+ if (key === void 0) continue;
591
+ if (seen.has(key)) stale.add(event.seq);
592
+ else seen.add(key);
593
+ }
594
+ return stale;
595
+ }
596
+ //#endregion
597
+ //#region src/engines/claude/runner.ts
403
598
  const DEFAULT_APPROVAL_TIMEOUT_MS$1 = 3e5;
404
599
  /**
405
600
  * One live Agent SDK session: owns the query() call, the streaming input queue, the
@@ -410,10 +605,21 @@ var SessionRunner = class {
410
605
  id;
411
606
  createdAt;
412
607
  #config;
608
+ /** {@link SessionRunnerConfig.cwd}, checked once in the constructor. */
609
+ #cwd;
413
610
  #events = [];
414
611
  #listeners = /* @__PURE__ */ new Set();
415
612
  #seq = 0;
416
613
  #activityCount = 0;
614
+ /**
615
+ * Seq of the latest `conversation_reset` event, 0 when none. The log itself is
616
+ * never truncated — it still carries the state-bearing events (`capabilities`,
617
+ * `system_init`, …) a fresh attacher depends on and which are not re-emitted —
618
+ * but `subscribe()` skips transcript *content* strictly below this mark, so a
619
+ * replay does not resurrect a cleared conversation. A later reset supersedes
620
+ * an earlier one by overwriting it.
621
+ */
622
+ #resetSeq = 0;
417
623
  #status = "starting";
418
624
  #statusDetail;
419
625
  #sdkSessionId;
@@ -430,10 +636,15 @@ var SessionRunner = class {
430
636
  /** Last plan reported by the usage poll, so `plan_info` is emitted on change
431
637
  * rather than once per turn. */
432
638
  #subscriptionType;
639
+ /** The title the CLI gave this thread (see `#fetchEngineTitle`). Undefined
640
+ * until it has one — a session gets its summary a turn or two in. */
641
+ #engineTitle;
433
642
  #started = false;
434
643
  #closed = false;
435
644
  #runPromise;
436
645
  constructor(config, id = randomUUID()) {
646
+ if (!config.cwd) throw new Error("the claude engine requires a cwd");
647
+ this.#cwd = config.cwd;
437
648
  this.#config = config;
438
649
  this.#permissionMode = config.permissionMode;
439
650
  this.id = id;
@@ -460,7 +671,7 @@ var SessionRunner = class {
460
671
  id: this.id,
461
672
  sdkSessionId: this.#sdkSessionId,
462
673
  status: this.#status,
463
- cwd: this.#config.cwd,
674
+ cwd: this.#cwd,
464
675
  profile: this.#config.profile,
465
676
  engine: "claude",
466
677
  capabilities: ENGINE_CAPABILITIES.claude,
@@ -473,15 +684,27 @@ var SessionRunner = class {
473
684
  activityCount: this.#activityCount,
474
685
  pendingPermissionCount: this.#pending.size,
475
686
  meta: this.#config.meta,
687
+ scope: this.#config.scope,
476
688
  title: this.#title(),
477
689
  totalCostUsd: this.#totalCostUsd,
478
690
  numTurns: this.#numTurns,
479
691
  lastActivityAt: this.#lastActivityAt
480
692
  };
481
693
  }
694
+ /**
695
+ * Three sources, most-deliberate first: the host's own rename (`meta.title`),
696
+ * the title the CLI gave this thread (`#engineTitle`), then the first prompt
697
+ * truncated.
698
+ *
699
+ * The rename outranks everything by design — a person naming a session must
700
+ * not have it renamed under them by a model — which is also why the engine
701
+ * title is *only ever read* while `meta.title` is unset (see
702
+ * `#fetchEngineTitle`), rather than read and then discarded here.
703
+ */
482
704
  #title() {
483
705
  const metaTitle = this.#config.meta?.title;
484
706
  if (typeof metaTitle === "string" && metaTitle.length > 0) return metaTitle;
707
+ if (this.#engineTitle) return this.#engineTitle;
485
708
  const prompt = this.#config.prompt;
486
709
  if (!prompt) return void 0;
487
710
  return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
@@ -611,9 +834,24 @@ var SessionRunner = class {
611
834
  /**
612
835
  * Replay buffered events with seq > afterSeq, then deliver live events.
613
836
  * Returns an unsubscribe function.
837
+ *
838
+ * Replay honours the reset watermark: transcript content below the latest
839
+ * `conversation_reset` is skipped (the reducer would clear it again anyway,
840
+ * and a pre-reset client that never learned the reducer's case would render
841
+ * a conversation the engine has discarded), while state-bearing events —
842
+ * which are emitted once and never again — always replay. The reset event
843
+ * itself replays (the skip is strictly-below), which is what clears a
844
+ * reconnecting client still holding pre-reset rows; superseded resets are
845
+ * content below the newer one and are skipped with what they cleared.
614
846
  */
615
- subscribe(listener, afterSeq = 0) {
616
- for (const event of this.#events) if (event.seq > afterSeq) listener(event);
847
+ subscribe(listener, afterSeq = 0, options) {
848
+ const stale = options?.coalesceReplay ? staleReplaySeqs(this.#events, afterSeq) : void 0;
849
+ for (const event of this.#events) {
850
+ if (event.seq <= afterSeq) continue;
851
+ if (event.seq < this.#resetSeq && transcriptContent(event)) continue;
852
+ if (stale?.has(event.seq)) continue;
853
+ listener(event);
854
+ }
617
855
  this.#listeners.add(listener);
618
856
  return () => this.#listeners.delete(listener);
619
857
  }
@@ -665,20 +903,23 @@ var SessionRunner = class {
665
903
  const historyFn = c.historyFn ?? ((sessionId, options) => getSessionMessages(sessionId, options));
666
904
  let messages;
667
905
  try {
668
- messages = await historyFn(c.resume, { dir: c.cwd });
906
+ messages = await historyFn(c.resume, { dir: this.#cwd });
669
907
  } catch {
670
908
  return;
671
909
  }
672
910
  for (const m of messages) {
673
911
  if (this.#closed) return;
674
- if (m.type === "user") this.#emit({
675
- type: "user_message",
676
- message: toApiMessage(m.message),
677
- parentToolUseId: m.parent_tool_use_id,
678
- replay: true,
679
- uuid: m.uuid
680
- });
681
- else if (m.type === "assistant") this.#emit({
912
+ if (m.type === "user") {
913
+ const message = toApiMessage(m.message);
914
+ this.#emit({
915
+ type: "user_message",
916
+ message,
917
+ parentToolUseId: m.parent_tool_use_id,
918
+ replay: true,
919
+ synthetic: isSyntheticUserText(message) ? true : void 0,
920
+ uuid: m.uuid
921
+ });
922
+ } else if (m.type === "assistant") this.#emit({
682
923
  type: "assistant_message",
683
924
  message: toApiMessage(m.message),
684
925
  parentToolUseId: m.parent_tool_use_id,
@@ -690,7 +931,7 @@ var SessionRunner = class {
690
931
  #buildOptions() {
691
932
  const c = this.#config;
692
933
  return {
693
- cwd: c.cwd,
934
+ cwd: this.#cwd,
694
935
  permissionMode: c.permissionMode,
695
936
  allowedTools: c.allowedTools,
696
937
  disallowedTools: c.disallowedTools,
@@ -733,6 +974,7 @@ var SessionRunner = class {
733
974
  this.#fetchCapabilities();
734
975
  this.#fetchContextUsage();
735
976
  this.#fetchRateLimits();
977
+ this.#fetchEngineTitle();
736
978
  return;
737
979
  }
738
980
  if (msg.type === "system" && msg.subtype === "session_state_changed") {
@@ -744,12 +986,17 @@ var SessionRunner = class {
744
986
  const body = normalizeSdkMessage(msg);
745
987
  if (body) {
746
988
  this.#emit(body);
989
+ if (body.type === "conversation_reset") {
990
+ if (body.sdkSessionId) this.#sdkSessionId = body.sdkSessionId;
991
+ this.#fetchContextUsage();
992
+ }
747
993
  if (body.type === "turn_result") {
748
994
  this.#totalCostUsd = body.totalCostUsd;
749
995
  this.#numTurns = body.numTurns;
750
996
  if (this.#pending.size === 0) this.#setStatus("idle");
751
997
  this.#fetchContextUsage();
752
998
  this.#fetchRateLimits();
999
+ this.#fetchEngineTitle();
753
1000
  }
754
1001
  }
755
1002
  }
@@ -779,6 +1026,44 @@ var SessionRunner = class {
779
1026
  });
780
1027
  } catch {}
781
1028
  }
1029
+ /**
1030
+ * Adopt the title the CLI gave this thread — the "friendly title" it writes a
1031
+ * turn or two into a session, and the name a resumed thread already carries.
1032
+ *
1033
+ * A **poll, not an observation**, and unavoidably so: no member of the SDK's
1034
+ * `SDKMessage` union carries it (the whole union was checked). It lives on
1035
+ * `SDKSessionInfo`, which only `getSessionInfo` / `listSessions` return — the
1036
+ * same record `GET /sdk-sessions` already serves as `SdkSessionSummary`. So it
1037
+ * is read at init and after each turn, which is also roughly the rate at which
1038
+ * it changes.
1039
+ *
1040
+ * Two rules:
1041
+ * - **Never while `meta.title` is set.** A rename is a person's decision and a
1042
+ * generated summary must not overwrite it. Not read at all in that case, so
1043
+ * there is no stored value waiting to resurface if the rename is cleared —
1044
+ * the next turn simply fetches it again.
1045
+ * - `summary` falls back to the first prompt when the session has no real
1046
+ * title yet, so it is taken only when it *differs* from `firstPrompt`.
1047
+ * Otherwise `#title()`'s own prompt fallback covers it, and the two would
1048
+ * disagree only in how they truncate.
1049
+ *
1050
+ * Best-effort throughout: an unreadable transcript, a session file that is not
1051
+ * there yet, an SDK without the function — all leave the title as it was.
1052
+ */
1053
+ async #fetchEngineTitle() {
1054
+ const metaTitle = this.#config.meta?.title;
1055
+ if (typeof metaTitle === "string" && metaTitle.length > 0) return;
1056
+ const sdkSessionId = this.#sdkSessionId;
1057
+ if (!sdkSessionId) return;
1058
+ const read = this.#config.sessionInfoFn ?? getSessionInfo;
1059
+ try {
1060
+ const info = await read(sdkSessionId, { dir: this.#cwd });
1061
+ if (this.#closed || !info) return;
1062
+ const summary = info.summary && info.summary !== info.firstPrompt ? info.summary : void 0;
1063
+ const title = info.customTitle || summary;
1064
+ if (title) this.#engineTitle = title;
1065
+ } catch {}
1066
+ }
782
1067
  /** Snapshot the context window after a turn and surface it as an event. Optional-chained
783
1068
  * and best-effort for the same reasons as #fetchCapabilities. */
784
1069
  async #fetchContextUsage() {
@@ -960,6 +1245,7 @@ var SessionRunner = class {
960
1245
  };
961
1246
  this.#lastActivityAt = event.ts;
962
1247
  this.#activityCount += transcriptActivity(body);
1248
+ if (body.type === "conversation_reset") this.#resetSeq = event.seq;
963
1249
  this.#events.push(event);
964
1250
  for (const listener of this.#listeners) try {
965
1251
  listener(event);
@@ -981,7 +1267,7 @@ function recommendedAnswers(input) {
981
1267
  return answers;
982
1268
  }
983
1269
  //#endregion
984
- //#region src/ai-sdk-runner.ts
1270
+ //#region src/engines/provider/runner.ts
985
1271
  /** Permission modes this engine can honor. The rest of the protocol vocabulary
986
1272
  * (acceptEdits/plan/auto) is Claude Code CLI semantics with no meaning here —
987
1273
  * setPermissionMode rejects them, which the server surfaces as protocol_error. */
@@ -1099,7 +1385,7 @@ var AiSdkRunner = class {
1099
1385
  return {
1100
1386
  id: this.id,
1101
1387
  status: this.#status,
1102
- cwd: this.#config.cwd ?? process.cwd(),
1388
+ cwd: this.#config.cwd ?? "",
1103
1389
  profile: this.#config.profile,
1104
1390
  engine: "provider",
1105
1391
  capabilities: ENGINE_CAPABILITIES.provider,
@@ -1110,6 +1396,7 @@ var AiSdkRunner = class {
1110
1396
  activityCount: this.#activityCount,
1111
1397
  pendingPermissionCount: 0,
1112
1398
  meta: this.#config.meta,
1399
+ scope: this.#config.scope,
1113
1400
  title: this.#title(),
1114
1401
  numTurns: this.#numTurns || void 0,
1115
1402
  lastActivityAt: this.#lastActivityAt
@@ -1351,8 +1638,13 @@ var AiSdkRunner = class {
1351
1638
  Promise.resolve(this.#config.onClose?.()).catch(() => {});
1352
1639
  } catch {}
1353
1640
  }
1354
- subscribe(listener, afterSeq = 0) {
1355
- for (const event of this.#events) if (event.seq > afterSeq) listener(event);
1641
+ subscribe(listener, afterSeq = 0, options) {
1642
+ const stale = options?.coalesceReplay ? staleReplaySeqs(this.#events, afterSeq) : void 0;
1643
+ for (const event of this.#events) {
1644
+ if (event.seq <= afterSeq) continue;
1645
+ if (stale?.has(event.seq)) continue;
1646
+ listener(event);
1647
+ }
1356
1648
  this.#listeners.add(listener);
1357
1649
  return () => this.#listeners.delete(listener);
1358
1650
  }
@@ -1483,29 +1775,29 @@ var AiSdkRunner = class {
1483
1775
  cacheWrite: 0,
1484
1776
  cacheRead: 0
1485
1777
  };
1778
+ let blocks = [];
1779
+ const textBuf = /* @__PURE__ */ new Map();
1780
+ const reasoningBuf = /* @__PURE__ */ new Map();
1781
+ const flush = () => {
1782
+ if (blocks.length === 0) return;
1783
+ this.#emit({
1784
+ type: "assistant_message",
1785
+ message: {
1786
+ role: "assistant",
1787
+ content: blocks,
1788
+ model: this.#modelId()
1789
+ },
1790
+ parentToolUseId: null,
1791
+ uuid: randomUUID()
1792
+ });
1793
+ blocks = [];
1794
+ };
1486
1795
  try {
1487
1796
  const result = await agent.stream({
1488
1797
  messages: [...this.#messages],
1489
1798
  abortSignal: abort.signal
1490
1799
  });
1491
1800
  const partials = this.#config.includePartialMessages !== false;
1492
- let blocks = [];
1493
- const textBuf = /* @__PURE__ */ new Map();
1494
- const reasoningBuf = /* @__PURE__ */ new Map();
1495
- const flush = () => {
1496
- if (blocks.length === 0) return;
1497
- this.#emit({
1498
- type: "assistant_message",
1499
- message: {
1500
- role: "assistant",
1501
- content: blocks,
1502
- model: this.#modelId()
1503
- },
1504
- parentToolUseId: null,
1505
- uuid: randomUUID()
1506
- });
1507
- blocks = [];
1508
- };
1509
1801
  const emitToolResult = (toolCallId, content, isError) => {
1510
1802
  flush();
1511
1803
  this.#emit({
@@ -1635,6 +1927,15 @@ var AiSdkRunner = class {
1635
1927
  this.#finishTurn(text);
1636
1928
  } catch (error) {
1637
1929
  if (this.#closed) return;
1930
+ for (const [, thinking] of reasoningBuf) if (thinking) blocks.push({
1931
+ type: "thinking",
1932
+ thinking
1933
+ });
1934
+ for (const [, text] of textBuf) if (text) blocks.push({
1935
+ type: "text",
1936
+ text
1937
+ });
1938
+ flush();
1638
1939
  const message = error instanceof Error ? error.message : String(error);
1639
1940
  this.#numTurns += 1;
1640
1941
  this.#emit({
@@ -1694,6 +1995,17 @@ var AiSdkRunner = class {
1694
1995
  if (!prompt) return void 0;
1695
1996
  return prompt.length > 80 ? prompt.slice(0, 77) + "…" : prompt;
1696
1997
  }
1998
+ /**
1999
+ * This session's MCP servers, as the host assembled them.
2000
+ *
2001
+ * Always answers — an empty list when no MCP was wired — because the
2002
+ * alternative (undefined, which the server turns into a 501) says "this
2003
+ * engine cannot tell you", and this engine can: the host that built the
2004
+ * session is the only party who knows, and it has been asked.
2005
+ */
2006
+ async mcpServers() {
2007
+ return await this.#config.reportMcpServers?.() ?? [];
2008
+ }
1697
2009
  /** Host-facing rename: writes `meta.title`, which `#title()` prefers. Clearing
1698
2010
  * it (undefined) restores the derived title. The engine is never told. */
1699
2011
  setTitle(title) {
@@ -1744,7 +2056,7 @@ function errorText(error) {
1744
2056
  return error instanceof Error ? error.message : String(error);
1745
2057
  }
1746
2058
  //#endregion
1747
- //#region src/claude-auth.ts
2059
+ //#region src/engines/claude/auth.ts
1748
2060
  /**
1749
2061
  * The native Claude Code binary the Agent SDK itself spawns, resolved the way
1750
2062
  * the SDK resolves it: the platform-specific optional dependency installed next
@@ -1798,7 +2110,7 @@ function checkClaudeAuth(env, options = {}) {
1798
2110
  });
1799
2111
  }
1800
2112
  //#endregion
1801
- //#region src/quickjs-executor.ts
2113
+ //#region src/executors/quickjs-executor.ts
1802
2114
  /**
1803
2115
  * In-process execution backend: runs a tool's untrusted script in the QuickJS
1804
2116
  * WASM guest. Always settles inline — nothing downstream assumes that, which is
@@ -1896,7 +2208,7 @@ function isHostAllowed(url, allowedHosts) {
1896
2208
  });
1897
2209
  }
1898
2210
  //#endregion
1899
- //#region src/pending-registry.ts
2211
+ //#region src/lib/pending-registry.ts
1900
2212
  var PendingRequestRegistry = class {
1901
2213
  #slots = /* @__PURE__ */ new Map();
1902
2214
  get size() {
@@ -2003,7 +2315,7 @@ function toEntry(slot) {
2003
2315
  };
2004
2316
  }
2005
2317
  //#endregion
2006
- //#region src/browser-bridge-executor.ts
2318
+ //#region src/executors/browser-bridge-executor.ts
2007
2319
  /**
2008
2320
  * Executes tool calls in the attached client's own sandbox. The first backend
2009
2321
  * that genuinely returns `pending`: dispatch puts a request on the wire and
@@ -2114,7 +2426,7 @@ function toExecutionResult(outcome) {
2114
2426
  };
2115
2427
  }
2116
2428
  //#endregion
2117
- //#region src/deferred-executor.ts
2429
+ //#region src/executors/deferred-executor.ts
2118
2430
  /**
2119
2431
  * The executor for work that outlives the session's process residency: dispatch
2120
2432
  * hands the call off and returns `pending` **without holding a promise**, because
@@ -2161,7 +2473,7 @@ var DeferredExecutor = class {
2161
2473
  }
2162
2474
  };
2163
2475
  //#endregion
2164
- //#region src/tools.ts
2476
+ //#region src/engines/provider/tools.ts
2165
2477
  const MAX_FILE_BYTES = 1024 * 1024;
2166
2478
  /**
2167
2479
  * Build the capability-scoped tool set for a session.
@@ -2327,28 +2639,57 @@ function createToolContext(options) {
2327
2639
  /** Add host-side MCP tools to a context. They are ALWAYS authoritative: they run
2328
2640
  * server-side with server credentials, and must never be handed to a browser. */
2329
2641
  function withMcpTools(context, mcpTools) {
2642
+ return withHostTools(context, Object.fromEntries(Object.entries(mcpTools).map(([name, mcpTool]) => [name, {
2643
+ tool: mcpTool,
2644
+ trust: "authoritative"
2645
+ }])), "MCP tool");
2646
+ }
2647
+ /**
2648
+ * Add host-supplied tools to a context at an explicit trust level.
2649
+ *
2650
+ * The trust level is the whole point of the seam: {@link withMcpTools} can only
2651
+ * produce authoritative tools, so a host tool that *should* be sandboxed — and
2652
+ * therefore executable in the browser tab that asked for it — had no way to be
2653
+ * expressed at all. Here the host says which it is, and the contradictions are
2654
+ * refused rather than silently resolved:
2655
+ *
2656
+ * - a `sandboxed` tool carrying `execute` would run inline in this process with
2657
+ * the gateway's ambient authority, which is exactly what sandboxing it was
2658
+ * meant to prevent;
2659
+ * - an `authoritative` tool *without* `execute` would park the turn on a call no
2660
+ * executor claims, and the session would simply stop.
2661
+ */
2662
+ function withHostTools(context, hostTools, kind = "host tool") {
2663
+ const entries = Object.entries(hostTools);
2664
+ if (entries.length === 0) return context;
2330
2665
  const definitions = [...context.definitions];
2331
2666
  const tools = { ...context.tools };
2332
- for (const [name, mcpTool] of Object.entries(mcpTools)) {
2333
- if (context.sandboxedToolNames.includes(name)) throw new Error(`MCP tool '${name}' collides with a sandboxed tool of the same name`);
2667
+ const sandboxedToolNames = [...context.sandboxedToolNames];
2668
+ for (const [name, { tool: hostTool, trust }] of entries) {
2669
+ if (name in tools) throw new Error(`${kind} '${name}' collides with an existing tool of the same name`);
2670
+ const executes = typeof hostTool.execute === "function";
2671
+ if (trust === "sandboxed" && executes) throw new Error(`${kind} '${name}' is declared sandboxed but has an \`execute\` — it would run in this process with full authority. Drop \`execute\` so it rides the ToolExecutor seam.`);
2672
+ if (trust === "authoritative" && !executes) throw new Error(`${kind} '${name}' is declared authoritative but has no \`execute\` — nothing would ever answer its calls and the turn would stall.`);
2334
2673
  definitions.push({
2335
2674
  name,
2336
- trust: "authoritative",
2337
- tool: mcpTool
2675
+ trust,
2676
+ tool: hostTool
2338
2677
  });
2339
- tools[name] = mcpTool;
2678
+ tools[name] = hostTool;
2679
+ if (trust === "sandboxed") sandboxedToolNames.push(name);
2340
2680
  }
2341
2681
  return {
2342
2682
  ...context,
2343
2683
  tools,
2344
- definitions
2684
+ definitions,
2685
+ sandboxedToolNames
2345
2686
  };
2346
2687
  }
2347
2688
  function truncate(text) {
2348
2689
  return text.length > MAX_FILE_BYTES ? text.slice(0, MAX_FILE_BYTES) : text;
2349
2690
  }
2350
2691
  //#endregion
2351
- //#region src/web-fetch.ts
2692
+ //#region src/engines/provider/web-fetch.ts
2352
2693
  const MAX_CACHE_ENTRIES = 64;
2353
2694
  const MAX_REDIRECTS = 5;
2354
2695
  function createWebFetch(options = {}) {
@@ -2560,7 +2901,7 @@ function decodeEntities(text) {
2560
2901
  return text.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))).replace(/&#x([\da-f]+);/gi, (_, code) => String.fromCodePoint(parseInt(code, 16))).replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;|&apos;/g, "'").replace(/&amp;/g, "&");
2561
2902
  }
2562
2903
  //#endregion
2563
- //#region src/engine.ts
2904
+ //#region src/engines/provider/session.ts
2564
2905
  /** Which capability a wired backend yields, for grant filtering. */
2565
2906
  const CAPABILITY_TOOLS = {
2566
2907
  search: "web_search",
@@ -2581,7 +2922,7 @@ const CAPABILITY_TOOLS = {
2581
2922
  * the host wired, which is what a host that ignores profiles gets.
2582
2923
  */
2583
2924
  function createEngineSession(options) {
2584
- const vfs = options.config.vfs ?? createVfs(options.config.restore?.vfs);
2925
+ const vfs = options.config.vfs ?? createVfs(options.config.restore ? options.config.restore.vfs : options.seedVfs);
2585
2926
  const executor = options.selectExecutor();
2586
2927
  const granted = options.config.capabilities ?? options.profile?.session?.capabilities;
2587
2928
  const isGranted = (key) => granted === void 0 || granted.includes(CAPABILITY_TOOLS[key]);
@@ -2602,8 +2943,12 @@ function createEngineSession(options) {
2602
2943
  webFetch,
2603
2944
  onFileDelivered: options.capabilities?.deliverFiles === false || !isGranted("deliverFiles") ? void 0 : (file) => runner?.emitFileDelivered(file)
2604
2945
  });
2605
- const mcpTools = selectMcpTools(options.mcpTools, options.profile?.session?.mcpServers);
2606
- const context = mcpTools ? withMcpTools(base, mcpTools) : base;
2946
+ const declaredServers = options.profile?.session?.mcpServers;
2947
+ const connected = options.mcp?.tools ?? options.mcpTools;
2948
+ requireDeclaredServers(options.profile?.name ?? "(unnamed)", declaredServers, options.mcp, connected);
2949
+ const mcpTools = selectMcpTools(connected, declaredServers);
2950
+ const withMcp = mcpTools ? withMcpTools(base, mcpTools) : base;
2951
+ const context = options.tools ? withHostTools(withMcp, options.tools) : withMcp;
2607
2952
  runner = new AiSdkRunner({
2608
2953
  ...options.config,
2609
2954
  languageModel: options.resolveModel(options.profile, options.config),
@@ -2613,11 +2958,42 @@ function createEngineSession(options) {
2613
2958
  executor,
2614
2959
  executableTools: context.sandboxedToolNames,
2615
2960
  executionBackend: options.backend ?? "server",
2616
- executionLimits: options.executionLimits
2617
- });
2961
+ executionLimits: options.executionLimits,
2962
+ reportMcpServers: options.mcp ? () => Promise.resolve(declaredServers === void 0 ? options.mcp.servers : options.mcp.servers.filter((s) => declaredServers.includes(s.name))) : void 0
2963
+ }, options.id);
2618
2964
  return runner;
2619
2965
  }
2620
2966
  /**
2967
+ * Refuse to build a session whose profile names an MCP server that isn't there.
2968
+ *
2969
+ * A profile's `mcpServers` list is a **declaration**, not a filter: an embedder
2970
+ * who wrote it meant the agent to have those tools. Honouring it partially is
2971
+ * the worst failure mode this engine has — the session starts, reports healthy,
2972
+ * and the agent apologises its way through every request that needed the server,
2973
+ * with one warning line in a log nobody is reading.
2974
+ *
2975
+ * With a {@link McpConnection} the check is exact (did this server connect?).
2976
+ * With a bare tool set all we can see is whether any tool carries the server's
2977
+ * namespace, so a genuinely tool-less server would trip it — the fix there is to
2978
+ * pass `mcp` rather than to weaken this.
2979
+ */
2980
+ function requireDeclaredServers(profileName, declared, mcp, tools) {
2981
+ if (!declared || declared.length === 0) return;
2982
+ const missing = declared.filter((name) => {
2983
+ if (mcp) {
2984
+ const server = mcp.servers.find((s) => s.name === name);
2985
+ return !server || server.status !== "connected";
2986
+ }
2987
+ return !Object.keys(tools ?? {}).some((tool) => tool.split("__")[0] === name);
2988
+ });
2989
+ if (missing.length === 0) return;
2990
+ const reasons = missing.map((name) => {
2991
+ const error = mcp?.servers.find((s) => s.name === name)?.error;
2992
+ return error ? `${name} (${error})` : name;
2993
+ }).join(", ");
2994
+ 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.`);
2995
+ }
2996
+ /**
2621
2997
  * Restrict a connected tool set to the MCP servers a profile grants, by the
2622
2998
  * `<server>__<tool>` namespace {@link connectMcpTools} assigns. Undefined `servers`
2623
2999
  * = no declaration, so every connected server passes through.
@@ -2637,31 +3013,88 @@ function selectMcpTools(tools, servers) {
2637
3013
  * Server-side only, with server credentials: these tools are authoritative and
2638
3014
  * must never be bridged to a browser. `@ai-sdk/mcp` is imported lazily and is an
2639
3015
  * optional dependency — an operator who wires no MCP servers never needs it.
3016
+ *
3017
+ * **A stateless MCP server must answer `GET` with 405.** The client opens the
3018
+ * SSE stream with a `GET` before it sends anything, and a POST-only server
3019
+ * mounted under a framework's default 404 makes the whole connect fail with an
3020
+ * error that names neither the method nor the route. This is the single most
3021
+ * common way an otherwise-correct MCP mount fails.
2640
3022
  */
2641
3023
  async function connectMcpTools(servers, options = {}) {
2642
3024
  const entries = Object.entries(servers);
2643
3025
  if (entries.length === 0) return {
2644
3026
  tools: {},
3027
+ servers: [],
2645
3028
  close: async () => {}
2646
3029
  };
2647
3030
  const { createMCPClient } = await import("@ai-sdk/mcp");
2648
3031
  const clients = [];
2649
3032
  const tools = {};
2650
- for (const [name, server] of entries) try {
2651
- const client = await createMCPClient({
2652
- transport: toTransport(server),
2653
- onUncaughtError: (error) => options.onError?.(name, error)
2654
- });
2655
- clients.push(client);
2656
- for (const [toolName, mcpTool] of Object.entries(await client.tools())) tools[`${name}__${toolName}`] = mcpTool;
2657
- } catch (error) {
2658
- options.onError?.(name, error);
3033
+ const statuses = [];
3034
+ const closeAll = async () => {
3035
+ await Promise.allSettled(clients.map((c) => c.close()));
3036
+ };
3037
+ for (const [name, server] of entries) {
3038
+ const identity = describeServer(server);
3039
+ try {
3040
+ const client = await createMCPClient({
3041
+ transport: toTransport(server),
3042
+ onUncaughtError: (error) => options.onError?.(name, error)
3043
+ });
3044
+ clients.push(client);
3045
+ const connected = await client.tools();
3046
+ for (const [toolName, mcpTool] of Object.entries(connected)) tools[`${name}__${toolName}`] = mcpTool;
3047
+ statuses.push({
3048
+ name,
3049
+ status: "connected",
3050
+ ...identity,
3051
+ tools: Object.entries(connected).map(([toolName, mcpTool]) => toToolInfo(toolName, mcpTool))
3052
+ });
3053
+ } catch (error) {
3054
+ const message = error instanceof Error ? error.message : String(error);
3055
+ statuses.push({
3056
+ name,
3057
+ status: "failed",
3058
+ error: message,
3059
+ ...identity
3060
+ });
3061
+ options.onError?.(name, error);
3062
+ if (options.required) {
3063
+ await closeAll();
3064
+ throw new Error(`MCP server '${name}' failed to connect: ${message}`);
3065
+ }
3066
+ }
2659
3067
  }
2660
3068
  return {
2661
3069
  tools,
2662
- close: async () => {
2663
- await Promise.allSettled(clients.map((c) => c.close()));
2664
- }
3070
+ servers: statuses,
3071
+ close: closeAll
3072
+ };
3073
+ }
3074
+ /** The connection's identity, minus its secrets — `headers` never travel. */
3075
+ function describeServer(server) {
3076
+ if ("url" in server) return {
3077
+ transport: server.type === "sse" ? "sse" : "http",
3078
+ url: server.url
3079
+ };
3080
+ return {
3081
+ transport: "stdio",
3082
+ command: server.command,
3083
+ args: server.args
3084
+ };
3085
+ }
3086
+ /**
3087
+ * The AI SDK hands back its own `Tool`, whose `inputSchema` may be a zod schema
3088
+ * or a `jsonSchema()` wrapper. Only the latter carries a JSON Schema document,
3089
+ * so that is the only case where parameters are reported — `McpServerToolInfo`
3090
+ * models the absence deliberately, and inventing one here would be worse.
3091
+ */
3092
+ function toToolInfo(name, mcpTool) {
3093
+ const { description, inputSchema } = mcpTool ?? {};
3094
+ return {
3095
+ name,
3096
+ description: typeof description === "string" ? description : void 0,
3097
+ inputSchema: inputSchema?.jsonSchema
2665
3098
  };
2666
3099
  }
2667
3100
  /**
@@ -2797,9 +3230,9 @@ const claudeAdapter = {
2797
3230
  };
2798
3231
  return { available: "unknown" };
2799
3232
  },
2800
- createRunner({ config, restore }) {
3233
+ createRunner({ config, restore, id }) {
2801
3234
  if (restore) throw new Error("the Claude engine cannot rebuild a parked session");
2802
- return new SessionRunner(config);
3235
+ return new SessionRunner(config, id);
2803
3236
  },
2804
3237
  /**
2805
3238
  * The Agent SDK's on-disk session store, mapped browser-safe. The SDK reads
@@ -3177,15 +3610,29 @@ function userQuestionsFromCodex(questions) {
3177
3610
  }))
3178
3611
  }));
3179
3612
  }
3180
- /** The text of a history `userMessage` item: its content entries' text parts
3181
- * joined. Image parts have no replayable representation (the bytes went to the
3182
- * model, not into the rollout we can render from) and are skipped. */
3613
+ /**
3614
+ * The text of a history `userMessage` item: its content entries' text parts
3615
+ * joined.
3616
+ *
3617
+ * Image parts have no replayable representation — the bytes went to the model,
3618
+ * not into the rollout we can render from — so they are named rather than
3619
+ * dropped. A prompt that was *only* an image used to produce an empty string,
3620
+ * which the caller read as "nothing to replay" and skipped: the turn lost its
3621
+ * user row and, with it, the prompt mark the scrubber navigates by, so a resumed
3622
+ * thread had answers with no visible question. A word in place of the picture is
3623
+ * a smaller lie than a turn that never happened.
3624
+ */
3183
3625
  function historyUserText(item) {
3184
3626
  if (!Array.isArray(item.content)) return "";
3185
- return item.content.map((part) => {
3627
+ let images = 0;
3628
+ const text = item.content.map((part) => {
3186
3629
  const candidate = part;
3187
- return candidate?.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
3630
+ if (candidate?.type === "text" && typeof candidate.text === "string") return candidate.text;
3631
+ if (typeof candidate?.type === "string" && candidate.type.toLowerCase().includes("image")) images += 1;
3632
+ return "";
3188
3633
  }).filter(Boolean).join("\n");
3634
+ if (text) return text;
3635
+ return images > 0 ? `[${images === 1 ? "image" : `${images} images`}]` : "";
3189
3636
  }
3190
3637
  /** The AskUserQuestion answer convention (question text → chosen label(s),
3191
3638
  * comma-joined) mapped back to codex's id-keyed shape. Questions the client
@@ -3345,6 +3792,8 @@ var CodexRunner = class {
3345
3792
  id;
3346
3793
  createdAt;
3347
3794
  #config;
3795
+ /** {@link CodexRunnerConfig.cwd}, checked once in the constructor. */
3796
+ #cwd;
3348
3797
  #events = [];
3349
3798
  #listeners = /* @__PURE__ */ new Set();
3350
3799
  #seq = 0;
@@ -3407,6 +3856,8 @@ var CodexRunner = class {
3407
3856
  const mode = config.permissionMode ?? "default";
3408
3857
  if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
3409
3858
  if (config.forkSession) throw new Error("the codex engine cannot fork a resumed thread");
3859
+ if (!config.cwd) throw new Error("the codex engine requires a cwd");
3860
+ this.#cwd = config.cwd;
3410
3861
  this.#config = config;
3411
3862
  this.#permissionMode = mode;
3412
3863
  this.#model = config.model;
@@ -3442,7 +3893,7 @@ var CodexRunner = class {
3442
3893
  id: this.id,
3443
3894
  sdkSessionId: this.#sdkSessionId,
3444
3895
  status: this.#status,
3445
- cwd: this.#config.cwd,
3896
+ cwd: this.#cwd,
3446
3897
  profile: this.#config.profile,
3447
3898
  engine: "codex",
3448
3899
  capabilities: ENGINE_CAPABILITIES.codex,
@@ -3454,6 +3905,7 @@ var CodexRunner = class {
3454
3905
  activityCount: this.#activityCount,
3455
3906
  pendingPermissionCount: this.#approvals.size,
3456
3907
  meta: this.#config.meta,
3908
+ scope: this.#config.scope,
3457
3909
  title: this.#title(),
3458
3910
  totalCostUsd: this.#totalCostUsd,
3459
3911
  numTurns: this.#numTurns || void 0,
@@ -3684,8 +4136,13 @@ var CodexRunner = class {
3684
4136
  });
3685
4137
  this.#setStatus("closed");
3686
4138
  }
3687
- subscribe(listener, afterSeq = 0) {
3688
- for (const event of this.#events) if (event.seq > afterSeq) listener(event);
4139
+ subscribe(listener, afterSeq = 0, options) {
4140
+ const stale = options?.coalesceReplay ? staleReplaySeqs(this.#events, afterSeq) : void 0;
4141
+ for (const event of this.#events) {
4142
+ if (event.seq <= afterSeq) continue;
4143
+ if (stale?.has(event.seq)) continue;
4144
+ listener(event);
4145
+ }
3689
4146
  this.#listeners.add(listener);
3690
4147
  return () => this.#listeners.delete(listener);
3691
4148
  }
@@ -3738,7 +4195,7 @@ var CodexRunner = class {
3738
4195
  }
3739
4196
  if (!this.#threadLoaded) {
3740
4197
  const options = {
3741
- cwd: this.#config.cwd,
4198
+ cwd: this.#cwd,
3742
4199
  approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
3743
4200
  sandbox: THREAD_SANDBOX_BY_MODE[this.#permissionMode]
3744
4201
  };
@@ -3779,7 +4236,7 @@ var CodexRunner = class {
3779
4236
  if (this.#skillsRefresh) return this.#skillsRefresh;
3780
4237
  const run = (async () => {
3781
4238
  try {
3782
- const result = await connection.request("skills/list", { cwds: [this.#config.cwd] });
4239
+ const result = await connection.request("skills/list", { cwds: [this.#cwd] });
3783
4240
  if (this.#closed) return;
3784
4241
  const entries = Array.isArray(result?.data) ? result.data : [];
3785
4242
  const seen = /* @__PURE__ */ new Set();
@@ -3981,7 +4438,7 @@ var CodexRunner = class {
3981
4438
  const params = {
3982
4439
  threadId: this.#sdkSessionId,
3983
4440
  input: turn.input,
3984
- cwd: this.#config.cwd,
4441
+ cwd: this.#cwd,
3985
4442
  approvalPolicy: APPROVAL_POLICY_BY_MODE[this.#permissionMode],
3986
4443
  sandboxPolicy: TURN_SANDBOX_BY_MODE[this.#permissionMode]
3987
4444
  };
@@ -4012,131 +4469,137 @@ var CodexRunner = class {
4012
4469
  }
4013
4470
  #handleNotification(method, params) {
4014
4471
  if (this.#closed) return;
4472
+ this.#notifications[method]?.(params);
4473
+ }
4474
+ /** Reasoning deltas arrive on two methods that differ only in which section
4475
+ * counter they advance; the section key carries the method so the two streams
4476
+ * never share a boundary. Section boundaries (a new summary/content entry)
4477
+ * render as paragraph breaks — the completed item joins sections with '\n\n'. */
4478
+ #reasoningDelta(method) {
4479
+ return (params) => {
4480
+ const active = this.#activeTurn;
4481
+ if (!active) return;
4482
+ const payload = params;
4483
+ if (typeof payload?.delta !== "string" || !payload.delta) return;
4484
+ const index = payload.contentIndex ?? payload.summaryIndex ?? 0;
4485
+ const key = `${payload.itemId ?? ""}:${method}`;
4486
+ const previous = active.sectionIndex.get(key);
4487
+ active.sectionIndex.set(key, index);
4488
+ const separator = previous !== void 0 && index > previous ? "\n\n" : "";
4489
+ this.#emitDelta({
4490
+ type: "thinking_delta",
4491
+ thinking: separator + payload.delta
4492
+ });
4493
+ };
4494
+ }
4495
+ /** One item-progress handler serves `item/started` and `item/updated`. */
4496
+ #itemProgress = (params) => {
4015
4497
  const active = this.#activeTurn;
4016
- switch (method) {
4017
- case "thread/started": {
4018
- const thread = params?.thread;
4019
- if (typeof thread?.id === "string") this.#sdkSessionId = thread.id;
4020
- return;
4021
- }
4022
- case "turn/started": {
4023
- const turn = params?.turn;
4024
- if (active && turn && !active.turnId) active.turnId = turn.id;
4025
- return;
4026
- }
4027
- case "turn/completed": {
4028
- const turn = params?.turn;
4029
- if (active && turn) active.resolve(turn);
4030
- return;
4031
- }
4032
- case "item/started":
4033
- case "item/updated": {
4034
- if (!active) return;
4035
- const item = params?.item;
4036
- if (item) this.#handleItemProgress(item, active);
4037
- return;
4038
- }
4039
- case "item/completed": {
4040
- if (!active) return;
4041
- const item = params?.item;
4042
- if (item) this.#handleItemCompleted(item, active);
4043
- return;
4044
- }
4045
- case "item/agentMessage/delta": {
4046
- if (!active) return;
4047
- const delta = params?.delta;
4048
- if (typeof delta === "string" && delta) this.#emitDelta({
4049
- type: "text_delta",
4050
- text: delta
4051
- });
4052
- return;
4053
- }
4054
- case "item/reasoning/textDelta":
4055
- case "item/reasoning/summaryTextDelta": {
4056
- if (!active) return;
4057
- const payload = params;
4058
- if (typeof payload?.delta !== "string" || !payload.delta) return;
4059
- const index = payload.contentIndex ?? payload.summaryIndex ?? 0;
4060
- const key = `${payload.itemId ?? ""}:${method}`;
4061
- const previous = active.sectionIndex.get(key);
4062
- active.sectionIndex.set(key, index);
4063
- const separator = previous !== void 0 && index > previous ? "\n\n" : "";
4064
- this.#emitDelta({
4065
- type: "thinking_delta",
4066
- thinking: separator + payload.delta
4067
- });
4068
- return;
4069
- }
4070
- case "thread/tokenUsage/updated": {
4071
- if (!active) return;
4072
- const last = params?.tokenUsage?.last;
4073
- if (!last) return;
4074
- active.sawUsage = true;
4075
- active.usage.inputTokens += last.inputTokens ?? 0;
4076
- active.usage.cachedInputTokens += last.cachedInputTokens ?? 0;
4077
- active.usage.cacheWriteInputTokens = (active.usage.cacheWriteInputTokens ?? 0) + (last.cacheWriteInputTokens ?? 0);
4078
- active.usage.outputTokens += last.outputTokens ?? 0;
4079
- active.usage.reasoningOutputTokens += last.reasoningOutputTokens ?? 0;
4080
- const update = params;
4081
- active.contextTokens = last.totalTokens ?? void 0;
4082
- active.contextWindow = update.tokenUsage?.modelContextWindow ?? void 0;
4083
- return;
4084
- }
4085
- case "mcpServer/startupStatus/updated": {
4086
- const update = params;
4087
- if (typeof update?.name !== "string") return;
4088
- this.#mcpStatus.set(update.name, {
4089
- status: typeof update.status === "string" ? update.status : "starting",
4090
- ...update.error ? { error: update.error } : {},
4091
- ...update.failureReason ? { failureReason: update.failureReason } : {}
4092
- });
4093
- return;
4094
- }
4095
- case "skills/changed": {
4096
- const connection = this.#connection;
4097
- if (connection) this.#refreshSkills(connection);
4098
- return;
4099
- }
4100
- case "account/rateLimits/updated":
4101
- this.#emitRateLimits(params?.rateLimits);
4102
- return;
4103
- case "turn/plan/updated": {
4104
- if (!active) return;
4105
- const plan = params?.plan;
4106
- if (!Array.isArray(plan)) return;
4107
- this.#emit({
4108
- type: "sdk_event",
4109
- payload: {
4110
- type: "codex.todo_list",
4111
- id: `${active.nonce}:plan`,
4112
- items: plan.map((step) => ({
4113
- text: step.step,
4114
- completed: step.status === "completed"
4115
- }))
4116
- }
4117
- });
4118
- return;
4119
- }
4120
- case "serverRequest/resolved": {
4121
- const requestId = params?.requestId;
4122
- if (requestId === void 0) return;
4123
- for (const [id, pending] of this.#approvals) if (pending.wireId === requestId) {
4124
- this.#settleApproval(id, pending, {
4125
- behavior: "deny",
4126
- message: "resolved by codex"
4127
- }, "policy");
4128
- return;
4498
+ if (!active) return;
4499
+ const item = params?.item;
4500
+ if (item) this.#handleItemProgress(item, active);
4501
+ };
4502
+ /** The notification dispatch table — every method the child emits that this
4503
+ * runner maps, in one place. Handlers read `this.#activeTurn` themselves:
4504
+ * dispatch is synchronous, so the read is the same one the old switch made. */
4505
+ #notifications = {
4506
+ "thread/started": (params) => {
4507
+ const thread = params?.thread;
4508
+ if (typeof thread?.id === "string") this.#sdkSessionId = thread.id;
4509
+ },
4510
+ "turn/started": (params) => {
4511
+ const active = this.#activeTurn;
4512
+ const turn = params?.turn;
4513
+ if (active && turn && !active.turnId) active.turnId = turn.id;
4514
+ },
4515
+ "turn/completed": (params) => {
4516
+ const active = this.#activeTurn;
4517
+ const turn = params?.turn;
4518
+ if (active && turn) active.resolve(turn);
4519
+ },
4520
+ "item/started": this.#itemProgress,
4521
+ "item/updated": this.#itemProgress,
4522
+ "item/completed": (params) => {
4523
+ const active = this.#activeTurn;
4524
+ if (!active) return;
4525
+ const item = params?.item;
4526
+ if (item) this.#handleItemCompleted(item, active);
4527
+ },
4528
+ "item/agentMessage/delta": (params) => {
4529
+ if (!this.#activeTurn) return;
4530
+ const delta = params?.delta;
4531
+ if (typeof delta === "string" && delta) this.#emitDelta({
4532
+ type: "text_delta",
4533
+ text: delta
4534
+ });
4535
+ },
4536
+ "item/reasoning/textDelta": this.#reasoningDelta("item/reasoning/textDelta"),
4537
+ "item/reasoning/summaryTextDelta": this.#reasoningDelta("item/reasoning/summaryTextDelta"),
4538
+ "thread/tokenUsage/updated": (params) => {
4539
+ const active = this.#activeTurn;
4540
+ if (!active) return;
4541
+ const last = params?.tokenUsage?.last;
4542
+ if (!last) return;
4543
+ active.sawUsage = true;
4544
+ active.usage.inputTokens += last.inputTokens ?? 0;
4545
+ active.usage.cachedInputTokens += last.cachedInputTokens ?? 0;
4546
+ active.usage.cacheWriteInputTokens = (active.usage.cacheWriteInputTokens ?? 0) + (last.cacheWriteInputTokens ?? 0);
4547
+ active.usage.outputTokens += last.outputTokens ?? 0;
4548
+ active.usage.reasoningOutputTokens += last.reasoningOutputTokens ?? 0;
4549
+ const update = params;
4550
+ active.contextTokens = last.totalTokens ?? void 0;
4551
+ active.contextWindow = update.tokenUsage?.modelContextWindow ?? void 0;
4552
+ },
4553
+ "mcpServer/startupStatus/updated": (params) => {
4554
+ const update = params;
4555
+ if (typeof update?.name !== "string") return;
4556
+ this.#mcpStatus.set(update.name, {
4557
+ status: typeof update.status === "string" ? update.status : "starting",
4558
+ ...update.error ? { error: update.error } : {},
4559
+ ...update.failureReason ? { failureReason: update.failureReason } : {}
4560
+ });
4561
+ },
4562
+ "skills/changed": () => {
4563
+ const connection = this.#connection;
4564
+ if (connection) this.#refreshSkills(connection);
4565
+ },
4566
+ "account/rateLimits/updated": (params) => {
4567
+ this.#emitRateLimits(params?.rateLimits);
4568
+ },
4569
+ "turn/plan/updated": (params) => {
4570
+ const active = this.#activeTurn;
4571
+ if (!active) return;
4572
+ const plan = params?.plan;
4573
+ if (!Array.isArray(plan)) return;
4574
+ this.#emit({
4575
+ type: "sdk_event",
4576
+ payload: {
4577
+ type: "codex.todo_list",
4578
+ id: `${active.nonce}:plan`,
4579
+ items: plan.map((step) => ({
4580
+ text: step.step,
4581
+ completed: step.status === "completed"
4582
+ }))
4129
4583
  }
4584
+ });
4585
+ },
4586
+ "serverRequest/resolved": (params) => {
4587
+ const requestId = params?.requestId;
4588
+ if (requestId === void 0) return;
4589
+ for (const [id, pending] of this.#approvals) if (pending.wireId === requestId) {
4590
+ this.#settleApproval(id, pending, {
4591
+ behavior: "deny",
4592
+ message: "resolved by codex"
4593
+ }, "policy");
4130
4594
  return;
4131
4595
  }
4132
- case "error": {
4133
- const error = params?.error;
4134
- if (active && typeof error?.message === "string") active.lastError = error.message;
4135
- return;
4136
- }
4137
- default: return;
4596
+ },
4597
+ "error": (params) => {
4598
+ const active = this.#activeTurn;
4599
+ const error = params?.error;
4600
+ if (active && typeof error?.message === "string") active.lastError = error.message;
4138
4601
  }
4139
- }
4602
+ };
4140
4603
  /** Answer a server→client request: the ask channels become pending
4141
4604
  * permission requests; anything else gets a JSON-RPC -32601 rather than a
4142
4605
  * hang (an unanswered server request wedges the turn). */
@@ -4282,83 +4745,89 @@ var CodexRunner = class {
4282
4745
  }
4283
4746
  #handleItemCompleted(item, active) {
4284
4747
  const id = `${active.nonce}:${item.id}`;
4285
- switch (item.type) {
4286
- case "userMessage": return;
4287
- case "agentMessage": {
4288
- const text = typeof item.text === "string" ? item.text : "";
4289
- this.#emitAssistant(id, [{
4290
- type: "text",
4291
- text
4292
- }]);
4293
- active.finalText = text;
4294
- return;
4295
- }
4296
- case "reasoning": {
4297
- const summary = Array.isArray(item.summary) ? item.summary.filter(Boolean) : [];
4298
- const content = Array.isArray(item.content) ? item.content.filter(Boolean) : [];
4299
- const thinking = (summary.length > 0 ? summary : content).join("\n\n");
4300
- if (thinking) this.#emitAssistant(id, [{
4301
- type: "thinking",
4302
- thinking
4303
- }]);
4304
- return;
4305
- }
4306
- case "commandExecution": {
4307
- if (!active.toolUseEmitted.has(id)) {
4308
- active.toolUseEmitted.add(id);
4309
- this.#emitToolUse(id, "CodexCommand", { command: item.command });
4310
- }
4311
- const exitCode = item.exitCode ?? void 0;
4312
- const failed = item.status === "failed" || item.status === "declined" || exitCode !== void 0 && exitCode !== 0;
4313
- const output = (item.aggregatedOutput ?? "") + (exitCode !== void 0 && exitCode !== 0 ? `\n(exit code ${exitCode})` : "");
4314
- this.#emitToolResult(id, output, failed);
4315
- return;
4316
- }
4317
- case "fileChange": {
4318
- this.#emitToolUse(id, "CodexFileChange", { changes: item.changes });
4319
- const lines = item.changes.map((change) => {
4320
- return `${(typeof change.kind === "string" ? change.kind : change.kind?.type) ?? "change"}: ${change.path}`;
4321
- });
4322
- this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined");
4323
- return;
4324
- }
4325
- case "mcpToolCall": {
4326
- if (!active.toolUseEmitted.has(id)) {
4327
- active.toolUseEmitted.add(id);
4328
- this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
4329
- }
4330
- const isError = item.error !== void 0 && item.error !== null || item.status === "failed";
4331
- this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError);
4332
- return;
4748
+ const handler = this.#itemCompleted[item.type];
4749
+ if (handler) {
4750
+ handler(item, active, id);
4751
+ return;
4752
+ }
4753
+ const unknown = item;
4754
+ this.#emit({
4755
+ type: "sdk_event",
4756
+ payload: {
4757
+ type: `codex.${unknown.type}`,
4758
+ item: unknown
4333
4759
  }
4334
- case "webSearch":
4335
- this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
4336
- this.#emitToolResult(id, "", false);
4337
- return;
4338
- case "imageGeneration": {
4760
+ });
4761
+ }
4762
+ /**
4763
+ * The completed-item mapping, one handler per member of the {@link AppServerItem}
4764
+ * union. The mapped type is the invariant made checkable: model a new item
4765
+ * type in `types.ts` and this table fails to compile until it says what the
4766
+ * item becomes on the wire — the old switch silently fell through to the
4767
+ * unknown-item passthrough instead. (The runtime still receives types the
4768
+ * union has never heard of; those take the passthrough above.)
4769
+ */
4770
+ #itemCompleted = {
4771
+ userMessage: () => {},
4772
+ agentMessage: (item, active, id) => {
4773
+ const text = typeof item.text === "string" ? item.text : "";
4774
+ this.#emitAssistant(id, [{
4775
+ type: "text",
4776
+ text
4777
+ }]);
4778
+ active.finalText = text;
4779
+ },
4780
+ reasoning: (item, _active, id) => {
4781
+ const summary = Array.isArray(item.summary) ? item.summary.filter(Boolean) : [];
4782
+ const content = Array.isArray(item.content) ? item.content.filter(Boolean) : [];
4783
+ const thinking = (summary.length > 0 ? summary : content).join("\n\n");
4784
+ if (thinking) this.#emitAssistant(id, [{
4785
+ type: "thinking",
4786
+ thinking
4787
+ }]);
4788
+ },
4789
+ commandExecution: (item, active, id) => {
4790
+ if (!active.toolUseEmitted.has(id)) {
4339
4791
  active.toolUseEmitted.add(id);
4340
- this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
4341
- if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
4342
- const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
4343
- this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
4344
- return;
4792
+ this.#emitToolUse(id, "CodexCommand", { command: item.command });
4345
4793
  }
4346
- case "imageView":
4347
- this.#emitToolUse(id, "CodexImageView", { path: item.path });
4348
- this.#emitToolResult(id, item.path, false);
4349
- return;
4350
- default: {
4351
- const unknown = item;
4352
- this.#emit({
4353
- type: "sdk_event",
4354
- payload: {
4355
- type: `codex.${unknown.type}`,
4356
- item: unknown
4357
- }
4358
- });
4794
+ const exitCode = item.exitCode ?? void 0;
4795
+ const failed = item.status === "failed" || item.status === "declined" || exitCode !== void 0 && exitCode !== 0;
4796
+ const output = (item.aggregatedOutput ?? "") + (exitCode !== void 0 && exitCode !== 0 ? `\n(exit code ${exitCode})` : "");
4797
+ this.#emitToolResult(id, output, failed);
4798
+ },
4799
+ fileChange: (item, _active, id) => {
4800
+ this.#emitToolUse(id, "CodexFileChange", { changes: item.changes });
4801
+ const lines = item.changes.map((change) => {
4802
+ return `${(typeof change.kind === "string" ? change.kind : change.kind?.type) ?? "change"}: ${change.path}`;
4803
+ });
4804
+ const only = item.changes.length === 1 ? item.changes[0] : void 0;
4805
+ this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined", only?.diff ? parseUnifiedDiff(only.diff, only.path) : void 0);
4806
+ },
4807
+ mcpToolCall: (item, active, id) => {
4808
+ if (!active.toolUseEmitted.has(id)) {
4809
+ active.toolUseEmitted.add(id);
4810
+ this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
4359
4811
  }
4812
+ const isError = item.error !== void 0 && item.error !== null || item.status === "failed";
4813
+ this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError);
4814
+ },
4815
+ webSearch: (item, _active, id) => {
4816
+ this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
4817
+ this.#emitToolResult(id, "", false);
4818
+ },
4819
+ imageGeneration: (item, active, id) => {
4820
+ active.toolUseEmitted.add(id);
4821
+ this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
4822
+ if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
4823
+ const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
4824
+ this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
4825
+ },
4826
+ imageView: (item, _active, id) => {
4827
+ this.#emitToolUse(id, "CodexImageView", { path: item.path });
4828
+ this.#emitToolResult(id, item.path, false);
4360
4829
  }
4361
- }
4830
+ };
4362
4831
  #emitDelta(delta) {
4363
4832
  if (this.#config.includePartialMessages === false) return;
4364
4833
  this.#emit({
@@ -4400,7 +4869,7 @@ var CodexRunner = class {
4400
4869
  uuid: `${id}-use`
4401
4870
  });
4402
4871
  }
4403
- #emitToolResult(toolUseId, content, isError) {
4872
+ #emitToolResult(toolUseId, content, isError, patch) {
4404
4873
  this.#emit({
4405
4874
  type: "user_message",
4406
4875
  message: {
@@ -4414,6 +4883,7 @@ var CodexRunner = class {
4414
4883
  },
4415
4884
  parentToolUseId: null,
4416
4885
  synthetic: true,
4886
+ patch,
4417
4887
  uuid: `${toolUseId}-result`
4418
4888
  });
4419
4889
  }
@@ -4905,7 +5375,7 @@ const codexAdapter = {
4905
5375
  capabilities: ENGINE_CAPABILITIES.codex,
4906
5376
  catalog: CODEX_CATALOG,
4907
5377
  checkAvailability: (profile, env) => checkCodexAvailability(profile, env),
4908
- createRunner({ config, profile, restore }) {
5378
+ createRunner({ config, profile, restore, id }) {
4909
5379
  if (restore) throw new Error("the codex engine cannot rebuild a parked session");
4910
5380
  const executable = config.codexPathOverride ?? resolveBundledCodexExecutable();
4911
5381
  if (!executable) throw new Error(NOT_INSTALLED);
@@ -4916,7 +5386,7 @@ const codexAdapter = {
4916
5386
  executable,
4917
5387
  ...options
4918
5388
  })
4919
- });
5389
+ }, id);
4920
5390
  },
4921
5391
  async listSessions(options) {
4922
5392
  const executable = resolveBundledCodexExecutable();
@@ -4976,6 +5446,6 @@ function getEngineAdapter(engine) {
4976
5446
  return ADAPTERS[engine ?? "claude"];
4977
5447
  }
4978
5448
  //#endregion
4979
- export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, withMcpTools };
5449
+ export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, withHostTools, withMcpTools };
4980
5450
 
4981
5451
  //# sourceMappingURL=index.mjs.map