@cabane/companion 0.6.18 → 0.6.20

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.
Files changed (3) hide show
  1. package/dist/cli.js +238 -649
  2. package/dist/runtime.js +220 -631
  3. package/package.json +3 -2
package/dist/runtime.js CHANGED
@@ -309,14 +309,14 @@ function localAgentConfig(cfg, agent) {
309
309
  return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
310
310
  }
311
311
  function loadConfig() {
312
- const path3 = configPath();
313
- if (!existsSync(path3)) return null;
312
+ const path = configPath();
313
+ if (!existsSync(path)) return null;
314
314
  let raw;
315
315
  try {
316
- raw = readFileSync(path3, "utf8");
316
+ raw = readFileSync(path, "utf8");
317
317
  } catch (err) {
318
318
  throw new ConfigError(
319
- `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
319
+ `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
320
320
  );
321
321
  }
322
322
  if (raw.trim().length === 0) return null;
@@ -325,7 +325,7 @@ function loadConfig() {
325
325
  parsed = JSON.parse(raw);
326
326
  } catch (err) {
327
327
  throw new ConfigError(
328
- `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
328
+ `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
329
329
  );
330
330
  }
331
331
  const result = companionConfigSchema.safeParse(parsed);
@@ -333,30 +333,30 @@ function loadConfig() {
333
333
  const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
334
334
  if (agentIssue) {
335
335
  throw new ConfigError(
336
- `${path3}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
336
+ `${path}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
337
337
  );
338
338
  }
339
339
  throw new ConfigError(
340
- `${path3} is from an incompatible or older version of the companion, or was hand-edited. Run \`cabane-companion pair\` to re-pair, or \`cabane-companion logout --purge\` to reset.`
340
+ `${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`cabane-companion pair\` to re-pair, or \`cabane-companion logout --purge\` to reset.`
341
341
  );
342
342
  }
343
343
  return result.data;
344
344
  }
345
345
  function saveConfig(cfg) {
346
- const path3 = configPath();
347
- mkdirSync(dirname(path3), { recursive: true });
346
+ const path = configPath();
347
+ mkdirSync(dirname(path), { recursive: true });
348
348
  try {
349
349
  chmodSync(cabaneDir(), 448);
350
350
  } catch {
351
351
  }
352
- const tmp = `${path3}.${process.pid}.tmp`;
352
+ const tmp = `${path}.${process.pid}.tmp`;
353
353
  try {
354
354
  writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
355
355
  try {
356
356
  chmodSync(tmp, 384);
357
357
  } catch {
358
358
  }
359
- renameSync(tmp, path3);
359
+ renameSync(tmp, path);
360
360
  } catch (err) {
361
361
  try {
362
362
  rmSync(tmp, { force: true });
@@ -417,8 +417,8 @@ function consoleMessageFormat(log, messageKey) {
417
417
  var cached = null;
418
418
  function getLogger() {
419
419
  if (cached) return cached;
420
- const path3 = companionLogPath();
421
- mkdirSync2(dirname2(path3), { recursive: true });
420
+ const path = companionLogPath();
421
+ mkdirSync2(dirname2(path), { recursive: true });
422
422
  const streams = [];
423
423
  if (process.env.CABANE_COMPANION_DAEMON !== "1") {
424
424
  const consoleStream = pretty({
@@ -428,7 +428,7 @@ function getLogger() {
428
428
  });
429
429
  streams.push({ level: "info", stream: consoleStream });
430
430
  }
431
- streams.push({ level: "debug", stream: createWriteStream(path3, { flags: "a" }) });
431
+ streams.push({ level: "debug", stream: createWriteStream(path, { flags: "a" }) });
432
432
  cached = pino({ level: "debug" }, pino.multistream(streams));
433
433
  return cached;
434
434
  }
@@ -807,12 +807,12 @@ function clampLimit(raw, fallback, max = 200) {
807
807
  if (!Number.isFinite(n) || n <= 0) return fallback;
808
808
  return Math.min(Math.floor(n), max);
809
809
  }
810
- function tailFile(path3, lines) {
811
- if (!existsSync2(path3)) return [];
810
+ function tailFile(path, lines) {
811
+ if (!existsSync2(path)) return [];
812
812
  const MAX_BYTES = 256 * 1024;
813
813
  let fd;
814
814
  try {
815
- fd = openSync(path3, "r");
815
+ fd = openSync(path, "r");
816
816
  const size = fstatSync(fd).size;
817
817
  const start = Math.max(0, size - MAX_BYTES);
818
818
  const len = size - start;
@@ -1054,9 +1054,9 @@ function serialize(state) {
1054
1054
  return JSON.stringify(state, null, 2) + "\n";
1055
1055
  }
1056
1056
  function writeRuntimeState(state) {
1057
- const path3 = runtimePath();
1057
+ const path = runtimePath();
1058
1058
  mkdirSync3(cabaneDir(), { recursive: true });
1059
- writeFileSync2(path3, serialize(state), "utf8");
1059
+ writeFileSync2(path, serialize(state), "utf8");
1060
1060
  }
1061
1061
  function acquireRuntimeState(state) {
1062
1062
  const live = readLiveRuntimeState();
@@ -1076,15 +1076,15 @@ function acquireRuntimeState(state) {
1076
1076
  return { acquired: true };
1077
1077
  }
1078
1078
  function clearRuntimeState() {
1079
- const path3 = runtimePath();
1080
- if (existsSync3(path3)) rmSync2(path3, { force: true });
1079
+ const path = runtimePath();
1080
+ if (existsSync3(path)) rmSync2(path, { force: true });
1081
1081
  }
1082
1082
  function readLiveRuntimeState() {
1083
- const path3 = runtimePath();
1084
- if (!existsSync3(path3)) return null;
1083
+ const path = runtimePath();
1084
+ if (!existsSync3(path)) return null;
1085
1085
  let parsed;
1086
1086
  try {
1087
- parsed = JSON.parse(readFileSync2(path3, "utf8"));
1087
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
1088
1088
  } catch {
1089
1089
  return null;
1090
1090
  }
@@ -1148,8 +1148,8 @@ var CabaneApi = class {
1148
1148
  // One HTTP attempt — no retry. Throws `ApiError` on a 4xx/5xx response and
1149
1149
  // rethrows transport errors (fetch reject) unchanged so the caller's retry
1150
1150
  // logic can classify them.
1151
- async attempt(method, path3, body, signal) {
1152
- const res = await fetch(`${this.base}${path3}`, {
1151
+ async attempt(method, path, body, signal) {
1152
+ const res = await fetch(`${this.base}${path}`, {
1153
1153
  method,
1154
1154
  headers: {
1155
1155
  Authorization: `Bearer ${this.opts.token}`,
@@ -1175,12 +1175,12 @@ var CabaneApi = class {
1175
1175
  }
1176
1176
  return parsed;
1177
1177
  }
1178
- async request(method, path3, body, opts = {}) {
1178
+ async request(method, path, body, opts = {}) {
1179
1179
  const { signal, retry = false } = opts;
1180
1180
  const maxAttempts = retry ? RETRY_BACKOFF_MS.length + 1 : 1;
1181
1181
  for (let attempt = 1; ; attempt++) {
1182
1182
  try {
1183
- return await this.attempt(method, path3, body, signal);
1183
+ return await this.attempt(method, path, body, signal);
1184
1184
  } catch (err) {
1185
1185
  if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
1186
1186
  await sleep(RETRY_BACKOFF_MS[attempt - 1], signal);
@@ -1206,9 +1206,9 @@ var CabaneApi = class {
1206
1206
  // delivers it once the API returns. `(turnId, seq)` is the server's
1207
1207
  // idempotency key, so a replay whose original POST's fate is unknown
1208
1208
  // converges instead of duplicating.
1209
- async durableCommit(kind, path3, body, turnId, seq, signal) {
1209
+ async durableCommit(kind, path, body, turnId, seq, signal) {
1210
1210
  try {
1211
- await this.request("POST", path3, body, {
1211
+ await this.request("POST", path, body, {
1212
1212
  retry: true,
1213
1213
  ...signal ? { signal } : {}
1214
1214
  });
@@ -1217,7 +1217,7 @@ var CabaneApi = class {
1217
1217
  if (!outbox) throw err;
1218
1218
  if (signal?.aborted || isAbortError(err)) throw err;
1219
1219
  if (!isRetryable(err)) throw err;
1220
- outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
1220
+ outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path, body, kind });
1221
1221
  this.opts.log?.warn(
1222
1222
  { kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
1223
1223
  "companion: commit queued to outbox after transient failure (will drain when the API returns)"
@@ -1369,12 +1369,12 @@ var CabaneApi = class {
1369
1369
  // left best-effort: it's lower-stakes and self-heals on the next turn, so it
1370
1370
  // stays a single-shot PATCH and is deliberately out of CT93's scope.
1371
1371
  setActiveRun(workspaceId, conversationId, agentId, body) {
1372
- const path3 = `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/active-run`;
1372
+ const path = `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/active-run`;
1373
1373
  const touchesFlag = Object.prototype.hasOwnProperty.call(body, "activeRunStartedAt");
1374
1374
  if (touchesFlag && this.opts.outbox) {
1375
- return this.durableActiveRunWrite(path3, conversationId, agentId, body);
1375
+ return this.durableActiveRunWrite(path, conversationId, agentId, body);
1376
1376
  }
1377
- return this.request("PATCH", path3, body);
1377
+ return this.request("PATCH", path, body);
1378
1378
  }
1379
1379
  // CT93: send-or-enqueue for an active-run flag write, with last-writer-wins
1380
1380
  // across the (conversation, agent) pair. Mirrors `durableCommit`, with two
@@ -1388,11 +1388,11 @@ var CabaneApi = class {
1388
1388
  // later and clobber the state we just wrote (the cross-turn race: turn N's
1389
1389
  // queued clear vs. turn N+1's live set). Combined with persist-overwrites-
1390
1390
  // by-key, this is the full last-writer-wins guarantee.
1391
- async durableActiveRunWrite(path3, conversationId, agentId, body) {
1391
+ async durableActiveRunWrite(path, conversationId, agentId, body) {
1392
1392
  const outbox = this.opts.outbox;
1393
1393
  const key = activeRunOutboxKey(conversationId, agentId);
1394
1394
  try {
1395
- await this.request("PATCH", path3, body, { retry: true });
1395
+ await this.request("PATCH", path, body, { retry: true });
1396
1396
  outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1397
1397
  } catch (err) {
1398
1398
  if (!outbox) throw err;
@@ -1405,7 +1405,7 @@ var CabaneApi = class {
1405
1405
  turnId: key,
1406
1406
  seq: ACTIVE_RUN_OUTBOX_SEQ,
1407
1407
  method: "PATCH",
1408
- path: path3,
1408
+ path,
1409
1409
  body,
1410
1410
  kind: "active-run"
1411
1411
  });
@@ -1493,8 +1493,8 @@ var CabaneApi = class {
1493
1493
  // shared resolver the in-app path uses. Omitting it (older call sites) returns
1494
1494
  // the agent default — graceful degradation, no version coupling.
1495
1495
  getAgentSelf(conversationId) {
1496
- const path3 = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
1497
- return this.request("GET", path3);
1496
+ const path = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
1497
+ return this.request("GET", path);
1498
1498
  }
1499
1499
  // The companion fetches the triggering message body by listing the
1500
1500
  // conversation's messages and finding the one with `id === messageId`.
@@ -1549,8 +1549,8 @@ var DeviceApi = class {
1549
1549
  get base() {
1550
1550
  return this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
1551
1551
  }
1552
- async request(method, path3, body) {
1553
- const res = await fetch(`${this.base}${path3}`, {
1552
+ async request(method, path, body) {
1553
+ const res = await fetch(`${this.base}${path}`, {
1554
1554
  method,
1555
1555
  headers: {
1556
1556
  Authorization: `Bearer ${this.opts.deviceToken}`,
@@ -1614,11 +1614,11 @@ function credentialsPath() {
1614
1614
  }
1615
1615
  var credentialStoreSchema = z3.record(z3.string(), z3.string());
1616
1616
  function load() {
1617
- const path3 = credentialsPath();
1618
- if (!existsSync4(path3)) return {};
1617
+ const path = credentialsPath();
1618
+ if (!existsSync4(path)) return {};
1619
1619
  let raw;
1620
1620
  try {
1621
- raw = readFileSync3(path3, "utf8");
1621
+ raw = readFileSync3(path, "utf8");
1622
1622
  } catch {
1623
1623
  return {};
1624
1624
  }
@@ -1631,20 +1631,20 @@ function load() {
1631
1631
  }
1632
1632
  }
1633
1633
  function save(map) {
1634
- const path3 = credentialsPath();
1635
- mkdirSync4(dirname4(path3), { recursive: true });
1634
+ const path = credentialsPath();
1635
+ mkdirSync4(dirname4(path), { recursive: true });
1636
1636
  try {
1637
1637
  chmodSync2(cabaneDir(), 448);
1638
1638
  } catch {
1639
1639
  }
1640
- const tmp = `${path3}.${process.pid}.tmp`;
1640
+ const tmp = `${path}.${process.pid}.tmp`;
1641
1641
  try {
1642
1642
  writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
1643
1643
  try {
1644
1644
  chmodSync2(tmp, 384);
1645
1645
  } catch {
1646
1646
  }
1647
- renameSync2(tmp, path3);
1647
+ renameSync2(tmp, path);
1648
1648
  } catch (err) {
1649
1649
  try {
1650
1650
  rmSync3(tmp, { force: true });
@@ -1683,15 +1683,15 @@ function pathFor(workspaceId) {
1683
1683
  return join7(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
1684
1684
  }
1685
1685
  function readCursor(workspaceId) {
1686
- const path3 = pathFor(workspaceId);
1687
- if (!existsSync5(path3)) return null;
1688
- const raw = readFileSync4(path3, "utf8").trim();
1686
+ const path = pathFor(workspaceId);
1687
+ if (!existsSync5(path)) return null;
1688
+ const raw = readFileSync4(path, "utf8").trim();
1689
1689
  return raw.length > 0 ? raw : null;
1690
1690
  }
1691
1691
  function writeCursor(workspaceId, eventId) {
1692
- const path3 = pathFor(workspaceId);
1692
+ const path = pathFor(workspaceId);
1693
1693
  mkdirSync5(join7(cabaneDir(), "cursors"), { recursive: true });
1694
- writeFileSync4(path3, eventId + "\n", "utf8");
1694
+ writeFileSync4(path, eventId + "\n", "utf8");
1695
1695
  }
1696
1696
 
1697
1697
  // src/cursor-tracker.ts
@@ -1744,10 +1744,10 @@ function pathFor2(log, workspaceId) {
1744
1744
  return join8(dir(log), encodeURIComponent(workspaceId));
1745
1745
  }
1746
1746
  function readIds(log, workspaceId) {
1747
- const path3 = pathFor2(log, workspaceId);
1748
- if (!existsSync6(path3)) return [];
1747
+ const path = pathFor2(log, workspaceId);
1748
+ if (!existsSync6(path)) return [];
1749
1749
  try {
1750
- return readFileSync5(path3, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
1750
+ return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
1751
1751
  } catch {
1752
1752
  return [];
1753
1753
  }
@@ -1784,10 +1784,10 @@ function resumePathFor(workspaceId) {
1784
1784
  }
1785
1785
  function readResumeCounts(workspaceId) {
1786
1786
  const out = /* @__PURE__ */ new Map();
1787
- const path3 = resumePathFor(workspaceId);
1788
- if (!existsSync6(path3)) return out;
1787
+ const path = resumePathFor(workspaceId);
1788
+ if (!existsSync6(path)) return out;
1789
1789
  try {
1790
- for (const line of readFileSync5(path3, "utf8").split("\n")) {
1790
+ for (const line of readFileSync5(path, "utf8").split("\n")) {
1791
1791
  const trimmed = line.trim();
1792
1792
  if (!trimmed) continue;
1793
1793
  const tab = trimmed.lastIndexOf(" ");
@@ -4379,6 +4379,16 @@ function readMcpError(error) {
4379
4379
  }
4380
4380
 
4381
4381
  // packages/agent-runtime/src/codex/session.ts
4382
+ function fingerprintPrompt(prompt) {
4383
+ let fnv = 2166136261;
4384
+ let djb = 5381;
4385
+ for (let i = 0; i < prompt.length; i += 1) {
4386
+ const code = prompt.charCodeAt(i);
4387
+ fnv = Math.imul(fnv ^ code, 16777619);
4388
+ djb = Math.imul(djb, 33) + code | 0;
4389
+ }
4390
+ return `${prompt.length}.${(fnv >>> 0).toString(36)}.${(djb >>> 0).toString(36)}`;
4391
+ }
4382
4392
  function encodeSession3(state) {
4383
4393
  return JSON.stringify(state);
4384
4394
  }
@@ -4398,7 +4408,10 @@ function decideResume3(stored, currentCwd) {
4398
4408
  if (storedCwd !== (currentCwd ?? "")) {
4399
4409
  return { fresh: true, reason: "cwd_mismatch" };
4400
4410
  }
4401
- return { resume: state.threadId };
4411
+ return {
4412
+ resume: state.threadId,
4413
+ promptFingerprint: typeof state.promptFingerprint === "string" ? state.promptFingerprint : null
4414
+ };
4402
4415
  }
4403
4416
 
4404
4417
  // packages/agent-runtime/src/codex/loop.ts
@@ -4421,13 +4434,20 @@ async function* decodeCodexStream(events, ctx) {
4421
4434
  if (ctx.signal.aborted) return;
4422
4435
  if (ev.type === "thread.started") {
4423
4436
  const threadId = readThreadId(ev);
4424
- if (!sessionEmitted && threadId && threadId !== ctx.resumedThreadId) {
4425
- sessionEmitted = true;
4426
- yield {
4427
- type: "session",
4428
- state: encodeSession3({ threadId, cwd: ctx.cwd ?? "" }),
4429
- ...ctx.degraded ? { degraded: true } : {}
4430
- };
4437
+ if (threadId && !sessionEmitted) {
4438
+ const state = encodeSession3({
4439
+ threadId,
4440
+ cwd: ctx.cwd ?? "",
4441
+ promptFingerprint: ctx.promptFingerprint ?? null
4442
+ });
4443
+ if (threadId !== ctx.resumedThreadId || state !== ctx.storedState) {
4444
+ sessionEmitted = true;
4445
+ yield {
4446
+ type: "session",
4447
+ state,
4448
+ ...ctx.degraded ? { degraded: true } : {}
4449
+ };
4450
+ }
4431
4451
  }
4432
4452
  continue;
4433
4453
  }
@@ -4550,7 +4570,7 @@ function codexToolPolicy(policy) {
4550
4570
  networkAccessEnabled: policy.web
4551
4571
  };
4552
4572
  }
4553
- var CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
4573
+ var CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
4554
4574
  var codexDialectSchema = z11.object({
4555
4575
  modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
4556
4576
  }).loose();
@@ -4572,7 +4592,7 @@ var CABANE_MCP_SERVER3 = "cabane";
4572
4592
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
4573
4593
  var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
4574
4594
  var ENV_ENVELOPE_KEYS = ["CABANE_ENV_TIER", "CABANE_ENV_KEY", "CABANE_ENV_BINDING"];
4575
- function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4595
+ function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null) {
4576
4596
  const { policy, config } = req;
4577
4597
  const directory = req.local.cwd ?? "";
4578
4598
  const dialect = readCodexDialect(config.runtimeOptions);
@@ -4583,6 +4603,9 @@ function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4583
4603
  `[agent-runtime/codex] CT823: dispatching a subscription turn with NO concrete model (config.model=${JSON.stringify(config.model)}) \u2014 Codex will fall back to its own default. This should be unreachable post-CT823; the model resolver should have supplied a concrete house default.`
4584
4604
  );
4585
4605
  }
4606
+ const promptFingerprint = fingerprintPrompt(req.systemPrompt);
4607
+ const threadHasThisPrompt = resumeThreadId !== null && threadPromptFingerprint !== null && threadPromptFingerprint === promptFingerprint;
4608
+ const promptRidesInput = baseInstructionsFile === null && !threadHasThisPrompt;
4586
4609
  return {
4587
4610
  resumeThreadId,
4588
4611
  directory,
@@ -4591,14 +4614,16 @@ function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4591
4614
  skipGitRepoCheck: true,
4592
4615
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
4593
4616
  baseInstructionsFile,
4594
- input: buildInput(req, baseInstructionsFile !== null),
4617
+ input: buildInput(req, promptRidesInput),
4618
+ promptRidesInput,
4619
+ promptFingerprint,
4595
4620
  config: buildConfig(req, baseInstructionsFile)
4596
4621
  };
4597
4622
  }
4598
- function buildInput(req, promptRidesInstructionsFile) {
4623
+ function buildInput(req, promptRidesInput) {
4599
4624
  const userText = req.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
4600
4625
  const body = userText.trim().length > 0 ? userText : req.prompt;
4601
- if (promptRidesInstructionsFile) return body;
4626
+ if (!promptRidesInput) return body;
4602
4627
  const system = req.systemPrompt.trim();
4603
4628
  return system.length > 0 ? `${system}
4604
4629
 
@@ -4700,536 +4725,10 @@ function isStringRecord2(v) {
4700
4725
  return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
4701
4726
  }
4702
4727
 
4703
- // node_modules/.pnpm/@openai+codex-sdk@0.146.0/node_modules/@openai/codex-sdk/dist/index.js
4704
- import { promises as fs } from "fs";
4705
- import os from "os";
4706
- import path from "path";
4707
- import { spawn as spawn4 } from "child_process";
4708
- import { statSync } from "fs";
4709
- import path2 from "path";
4710
- import readline from "readline";
4711
- import { createRequire } from "module";
4712
- async function createOutputSchemaFile(schema) {
4713
- if (schema === void 0) {
4714
- return { cleanup: async () => {
4715
- } };
4716
- }
4717
- if (!isJsonObject(schema)) {
4718
- throw new Error("outputSchema must be a plain JSON object");
4719
- }
4720
- const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
4721
- const schemaPath = path.join(schemaDir, "schema.json");
4722
- const cleanup = async () => {
4723
- try {
4724
- await fs.rm(schemaDir, { recursive: true, force: true });
4725
- } catch {
4726
- }
4727
- };
4728
- try {
4729
- await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
4730
- return { schemaPath, cleanup };
4731
- } catch (error) {
4732
- await cleanup();
4733
- throw error;
4734
- }
4735
- }
4736
- function isJsonObject(value) {
4737
- return typeof value === "object" && value !== null && !Array.isArray(value);
4738
- }
4739
- var Thread = class {
4740
- _exec;
4741
- _options;
4742
- _id;
4743
- _threadOptions;
4744
- /** Returns the ID of the thread. Populated after the first turn starts. */
4745
- get id() {
4746
- return this._id;
4747
- }
4748
- /* @internal */
4749
- constructor(exec, options, threadOptions, id = null) {
4750
- this._exec = exec;
4751
- this._options = options;
4752
- this._id = id;
4753
- this._threadOptions = threadOptions;
4754
- }
4755
- /** Provides the input to the agent and streams events as they are produced during the turn. */
4756
- async runStreamed(input, turnOptions = {}) {
4757
- return { events: this.runStreamedInternal(input, turnOptions) };
4758
- }
4759
- async *runStreamedInternal(input, turnOptions = {}) {
4760
- const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema);
4761
- const options = this._threadOptions;
4762
- const { prompt, images } = normalizeInput(input);
4763
- const generator = this._exec.run({
4764
- input: prompt,
4765
- baseUrl: this._options.baseUrl,
4766
- apiKey: this._options.apiKey,
4767
- threadId: this._id,
4768
- images,
4769
- model: options?.model,
4770
- sandboxMode: options?.sandboxMode,
4771
- workingDirectory: options?.workingDirectory,
4772
- skipGitRepoCheck: options?.skipGitRepoCheck,
4773
- outputSchemaFile: schemaPath,
4774
- modelReasoningEffort: options?.modelReasoningEffort,
4775
- signal: turnOptions.signal,
4776
- networkAccessEnabled: options?.networkAccessEnabled,
4777
- webSearchMode: options?.webSearchMode,
4778
- webSearchEnabled: options?.webSearchEnabled,
4779
- approvalPolicy: options?.approvalPolicy,
4780
- additionalDirectories: options?.additionalDirectories
4781
- });
4782
- try {
4783
- for await (const item of generator) {
4784
- let parsed;
4785
- try {
4786
- parsed = JSON.parse(item);
4787
- } catch (error) {
4788
- throw new Error(`Failed to parse item: ${item}`, { cause: error });
4789
- }
4790
- if (parsed.type === "thread.started") {
4791
- this._id = parsed.thread_id;
4792
- } else if (parsed.type === "turn.completed") {
4793
- parsed.usage.cache_write_input_tokens ??= 0;
4794
- }
4795
- yield parsed;
4796
- }
4797
- } finally {
4798
- await cleanup();
4799
- }
4800
- }
4801
- /** Provides the input to the agent and returns the completed turn. */
4802
- async run(input, turnOptions = {}) {
4803
- const generator = this.runStreamedInternal(input, turnOptions);
4804
- const items = [];
4805
- let finalResponse = "";
4806
- let usage = null;
4807
- let turnFailure = null;
4808
- for await (const event of generator) {
4809
- if (event.type === "item.completed") {
4810
- if (event.item.type === "agent_message") {
4811
- finalResponse = event.item.text;
4812
- }
4813
- items.push(event.item);
4814
- } else if (event.type === "turn.completed") {
4815
- usage = event.usage;
4816
- } else if (event.type === "turn.failed") {
4817
- turnFailure = event.error;
4818
- break;
4819
- }
4820
- }
4821
- if (turnFailure) {
4822
- throw new Error(turnFailure.message);
4823
- }
4824
- return { items, finalResponse, usage };
4825
- }
4826
- };
4827
- function normalizeInput(input) {
4828
- if (typeof input === "string") {
4829
- return { prompt: input, images: [] };
4830
- }
4831
- const promptParts = [];
4832
- const images = [];
4833
- for (const item of input) {
4834
- if (item.type === "text") {
4835
- promptParts.push(item.text);
4836
- } else if (item.type === "local_image") {
4837
- images.push(item.path);
4838
- }
4839
- }
4840
- return { prompt: promptParts.join("\n\n"), images };
4841
- }
4842
- var INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
4843
- var TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
4844
- var CODEX_NPM_NAME = "@openai/codex";
4845
- var PLATFORM_PACKAGE_BY_TARGET = {
4846
- "x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
4847
- "aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
4848
- "x86_64-apple-darwin": "@openai/codex-darwin-x64",
4849
- "aarch64-apple-darwin": "@openai/codex-darwin-arm64",
4850
- "x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
4851
- "aarch64-pc-windows-msvc": "@openai/codex-win32-arm64"
4852
- };
4853
- var moduleRequire = createRequire(import.meta.url);
4854
- var CodexExec = class {
4855
- executablePath;
4856
- pathDirs;
4857
- envOverride;
4858
- configOverrides;
4859
- constructor(executablePath = null, env, configOverrides) {
4860
- if (executablePath) {
4861
- this.executablePath = executablePath;
4862
- this.pathDirs = [];
4863
- } else {
4864
- const resolved = findCodexPath();
4865
- this.executablePath = resolved.executablePath;
4866
- this.pathDirs = resolved.pathDirs;
4867
- }
4868
- this.envOverride = env;
4869
- this.configOverrides = configOverrides;
4870
- }
4871
- async *run(args) {
4872
- const commandArgs = ["exec", "--experimental-json"];
4873
- if (this.configOverrides) {
4874
- for (const override of serializeConfigOverrides(this.configOverrides)) {
4875
- commandArgs.push("--config", override);
4876
- }
4877
- }
4878
- if (args.baseUrl) {
4879
- commandArgs.push(
4880
- "--config",
4881
- `openai_base_url=${toTomlValue(args.baseUrl, "openai_base_url")}`
4882
- );
4883
- }
4884
- if (args.model) {
4885
- commandArgs.push("--model", args.model);
4886
- }
4887
- if (args.sandboxMode) {
4888
- commandArgs.push("--sandbox", args.sandboxMode);
4889
- }
4890
- if (args.workingDirectory) {
4891
- commandArgs.push("--cd", args.workingDirectory);
4892
- }
4893
- if (args.additionalDirectories?.length) {
4894
- for (const dir2 of args.additionalDirectories) {
4895
- commandArgs.push("--add-dir", dir2);
4896
- }
4897
- }
4898
- if (args.skipGitRepoCheck) {
4899
- commandArgs.push("--skip-git-repo-check");
4900
- }
4901
- if (args.outputSchemaFile) {
4902
- commandArgs.push("--output-schema", args.outputSchemaFile);
4903
- }
4904
- if (args.modelReasoningEffort) {
4905
- commandArgs.push("--config", `model_reasoning_effort="${args.modelReasoningEffort}"`);
4906
- }
4907
- if (args.networkAccessEnabled !== void 0) {
4908
- commandArgs.push(
4909
- "--config",
4910
- `sandbox_workspace_write.network_access=${args.networkAccessEnabled}`
4911
- );
4912
- }
4913
- if (args.webSearchMode) {
4914
- commandArgs.push("--config", `web_search="${args.webSearchMode}"`);
4915
- } else if (args.webSearchEnabled === true) {
4916
- commandArgs.push("--config", `web_search="live"`);
4917
- } else if (args.webSearchEnabled === false) {
4918
- commandArgs.push("--config", `web_search="disabled"`);
4919
- }
4920
- if (args.approvalPolicy) {
4921
- commandArgs.push("--config", `approval_policy="${args.approvalPolicy}"`);
4922
- }
4923
- if (args.threadId) {
4924
- commandArgs.push("resume", args.threadId);
4925
- }
4926
- if (args.images?.length) {
4927
- for (const image of args.images) {
4928
- commandArgs.push("--image", image);
4929
- }
4930
- }
4931
- const env = {};
4932
- if (this.envOverride) {
4933
- Object.assign(env, this.envOverride);
4934
- } else {
4935
- for (const [key, value] of Object.entries(process.env)) {
4936
- if (value !== void 0) {
4937
- env[key] = value;
4938
- }
4939
- }
4940
- }
4941
- if (!env[INTERNAL_ORIGINATOR_ENV]) {
4942
- env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR;
4943
- }
4944
- if (args.apiKey) {
4945
- env.CODEX_API_KEY = args.apiKey;
4946
- }
4947
- if (this.pathDirs.length > 0) {
4948
- prependPathDirs(env, this.pathDirs);
4949
- }
4950
- const child = spawn4(this.executablePath, commandArgs, {
4951
- env,
4952
- signal: args.signal
4953
- });
4954
- let spawnError = null;
4955
- child.once("error", (err) => spawnError = err);
4956
- if (!child.stdin) {
4957
- child.kill();
4958
- throw new Error("Child process has no stdin");
4959
- }
4960
- child.stdin.write(args.input);
4961
- child.stdin.end();
4962
- if (!child.stdout) {
4963
- child.kill();
4964
- throw new Error("Child process has no stdout");
4965
- }
4966
- const stderrChunks = [];
4967
- if (child.stderr) {
4968
- child.stderr.on("data", (data) => {
4969
- stderrChunks.push(data);
4970
- });
4971
- }
4972
- const exitPromise = new Promise(
4973
- (resolve) => {
4974
- child.once("exit", (code, signal) => {
4975
- resolve({ code, signal });
4976
- });
4977
- }
4978
- );
4979
- const rl = readline.createInterface({
4980
- input: child.stdout,
4981
- crlfDelay: Infinity
4982
- });
4983
- try {
4984
- for await (const line of rl) {
4985
- yield line;
4986
- }
4987
- if (spawnError) throw spawnError;
4988
- const { code, signal } = await exitPromise;
4989
- if (code !== 0 || signal) {
4990
- const stderrBuffer = Buffer.concat(stderrChunks);
4991
- const detail = signal ? `signal ${signal}` : `code ${code ?? 1}`;
4992
- throw new Error(`Codex Exec exited with ${detail}: ${stderrBuffer.toString("utf8")}`);
4993
- }
4994
- } finally {
4995
- rl.close();
4996
- child.removeAllListeners();
4997
- try {
4998
- if (!child.killed) child.kill();
4999
- } catch {
5000
- }
5001
- }
5002
- }
5003
- };
5004
- function serializeConfigOverrides(configOverrides) {
5005
- const overrides = [];
5006
- flattenConfigOverrides(configOverrides, "", overrides);
5007
- return overrides;
5008
- }
5009
- function flattenConfigOverrides(value, prefix, overrides) {
5010
- if (!isPlainObject(value)) {
5011
- if (prefix) {
5012
- overrides.push(`${prefix}=${toTomlValue(value, prefix)}`);
5013
- return;
5014
- } else {
5015
- throw new Error("Codex config overrides must be a plain object");
5016
- }
5017
- }
5018
- const entries = Object.entries(value);
5019
- if (!prefix && entries.length === 0) {
5020
- return;
5021
- }
5022
- if (prefix && entries.length === 0) {
5023
- overrides.push(`${prefix}={}`);
5024
- return;
5025
- }
5026
- for (const [key, child] of entries) {
5027
- if (!key) {
5028
- throw new Error("Codex config override keys must be non-empty strings");
5029
- }
5030
- if (child === void 0) {
5031
- continue;
5032
- }
5033
- const path3 = prefix ? `${prefix}.${key}` : key;
5034
- if (isPlainObject(child)) {
5035
- flattenConfigOverrides(child, path3, overrides);
5036
- } else {
5037
- overrides.push(`${path3}=${toTomlValue(child, path3)}`);
5038
- }
5039
- }
5040
- }
5041
- function toTomlValue(value, path3) {
5042
- if (typeof value === "string") {
5043
- return JSON.stringify(value);
5044
- } else if (typeof value === "number") {
5045
- if (!Number.isFinite(value)) {
5046
- throw new Error(`Codex config override at ${path3} must be a finite number`);
5047
- }
5048
- return `${value}`;
5049
- } else if (typeof value === "boolean") {
5050
- return value ? "true" : "false";
5051
- } else if (Array.isArray(value)) {
5052
- const rendered = value.map((item, index) => toTomlValue(item, `${path3}[${index}]`));
5053
- return `[${rendered.join(", ")}]`;
5054
- } else if (isPlainObject(value)) {
5055
- const parts = [];
5056
- for (const [key, child] of Object.entries(value)) {
5057
- if (!key) {
5058
- throw new Error("Codex config override keys must be non-empty strings");
5059
- }
5060
- if (child === void 0) {
5061
- continue;
5062
- }
5063
- parts.push(`${formatTomlKey(key)} = ${toTomlValue(child, `${path3}.${key}`)}`);
5064
- }
5065
- return `{${parts.join(", ")}}`;
5066
- } else if (value === null) {
5067
- throw new Error(`Codex config override at ${path3} cannot be null`);
5068
- } else {
5069
- const typeName = typeof value;
5070
- throw new Error(`Unsupported Codex config override value at ${path3}: ${typeName}`);
5071
- }
5072
- }
5073
- var TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
5074
- function formatTomlKey(key) {
5075
- return TOML_BARE_KEY.test(key) ? key : JSON.stringify(key);
5076
- }
5077
- function isPlainObject(value) {
5078
- return typeof value === "object" && value !== null && !Array.isArray(value);
5079
- }
5080
- function findCodexPath() {
5081
- const { platform, arch } = process;
5082
- let targetTriple = null;
5083
- switch (platform) {
5084
- case "linux":
5085
- case "android":
5086
- switch (arch) {
5087
- case "x64":
5088
- targetTriple = "x86_64-unknown-linux-musl";
5089
- break;
5090
- case "arm64":
5091
- targetTriple = "aarch64-unknown-linux-musl";
5092
- break;
5093
- default:
5094
- break;
5095
- }
5096
- break;
5097
- case "darwin":
5098
- switch (arch) {
5099
- case "x64":
5100
- targetTriple = "x86_64-apple-darwin";
5101
- break;
5102
- case "arm64":
5103
- targetTriple = "aarch64-apple-darwin";
5104
- break;
5105
- default:
5106
- break;
5107
- }
5108
- break;
5109
- case "win32":
5110
- switch (arch) {
5111
- case "x64":
5112
- targetTriple = "x86_64-pc-windows-msvc";
5113
- break;
5114
- case "arm64":
5115
- targetTriple = "aarch64-pc-windows-msvc";
5116
- break;
5117
- default:
5118
- break;
5119
- }
5120
- break;
5121
- default:
5122
- break;
5123
- }
5124
- if (!targetTriple) {
5125
- throw new Error(`Unsupported platform: ${platform} (${arch})`);
5126
- }
5127
- const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple];
5128
- if (!platformPackage) {
5129
- throw new Error(`Unsupported target triple: ${targetTriple}`);
5130
- }
5131
- let vendorRoot;
5132
- try {
5133
- const codexPackageJsonPath = moduleRequire.resolve(`${CODEX_NPM_NAME}/package.json`);
5134
- const codexRequire = createRequire(codexPackageJsonPath);
5135
- const platformPackageJsonPath = codexRequire.resolve(`${platformPackage}/package.json`);
5136
- vendorRoot = path2.join(path2.dirname(platformPackageJsonPath), "vendor");
5137
- } catch {
5138
- throw new Error(
5139
- `Unable to locate Codex CLI binaries. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
5140
- );
5141
- }
5142
- const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
5143
- const nativePackage = resolveNativePackage(vendorRoot, targetTriple, codexBinaryName);
5144
- if (!nativePackage) {
5145
- throw new Error(
5146
- `Unable to locate Codex CLI binaries for ${targetTriple}. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
5147
- );
5148
- }
5149
- return nativePackage;
5150
- }
5151
- function resolveNativePackage(vendorRoot, targetTriple, codexBinaryName) {
5152
- const packageRoot = path2.join(vendorRoot, targetTriple);
5153
- const packageBinaryPath = path2.join(packageRoot, "bin", codexBinaryName);
5154
- if (isFile(packageBinaryPath) && isFile(path2.join(packageRoot, "codex-package.json"))) {
5155
- return {
5156
- executablePath: packageBinaryPath,
5157
- pathDirs: existingDirs(path2.join(packageRoot, "codex-path"))
5158
- };
5159
- }
5160
- const legacyBinaryPath = path2.join(packageRoot, "codex", codexBinaryName);
5161
- if (isFile(legacyBinaryPath)) {
5162
- return {
5163
- executablePath: legacyBinaryPath,
5164
- pathDirs: existingDirs(path2.join(packageRoot, "path"))
5165
- };
5166
- }
5167
- return null;
5168
- }
5169
- function existingDirs(...dirs) {
5170
- return dirs.filter(isDirectory);
5171
- }
5172
- function prependPathDirs(env, pathDirs, platform = process.platform) {
5173
- const pathKey = pathEnvKey(env, platform);
5174
- if (platform === "win32") {
5175
- for (const key of Object.keys(env)) {
5176
- if (key.toLowerCase() === "path" && key !== pathKey) {
5177
- delete env[key];
5178
- }
5179
- }
5180
- }
5181
- const existingEntries = (env[pathKey] ?? "").split(path2.delimiter).filter((entry) => entry.length > 0 && !pathDirs.includes(entry));
5182
- env[pathKey] = [...pathDirs, ...existingEntries].join(path2.delimiter);
5183
- }
5184
- function pathEnvKey(env, platform) {
5185
- if (platform !== "win32") {
5186
- return "PATH";
5187
- }
5188
- const matchingKeys = Object.keys(env).filter((key) => key.toLowerCase() === "path");
5189
- return matchingKeys.includes("Path") ? "Path" : matchingKeys.at(-1) ?? "PATH";
5190
- }
5191
- function isFile(filePath) {
5192
- try {
5193
- return statSync(filePath).isFile();
5194
- } catch {
5195
- return false;
5196
- }
5197
- }
5198
- function isDirectory(filePath) {
5199
- try {
5200
- return statSync(filePath).isDirectory();
5201
- } catch {
5202
- return false;
5203
- }
5204
- }
5205
- var Codex = class {
5206
- exec;
5207
- options;
5208
- constructor(options = {}) {
5209
- const { codexPathOverride, env, config } = options;
5210
- this.exec = new CodexExec(codexPathOverride, env, config);
5211
- this.options = options;
5212
- }
5213
- /**
5214
- * Starts a new conversation with an agent.
5215
- * @returns A new thread instance.
5216
- */
5217
- startThread(options = {}) {
5218
- return new Thread(this.exec, this.options, options);
5219
- }
5220
- /**
5221
- * Resumes a conversation with an agent based on the thread id.
5222
- * Threads are persisted in ~/.codex/sessions.
5223
- *
5224
- * @param id The id of the thread to resume.
5225
- * @returns A new thread instance.
5226
- */
5227
- resumeThread(id, options = {}) {
5228
- return new Thread(this.exec, this.options, options, id);
5229
- }
5230
- };
5231
-
5232
4728
  // packages/agent-runtime/src/codex/transport.ts
4729
+ import {
4730
+ Codex
4731
+ } from "@openai/codex-sdk";
5233
4732
  function buildSdkThreadOptions(spec) {
5234
4733
  return {
5235
4734
  ...spec.model ? { model: spec.model } : {},
@@ -5283,6 +4782,7 @@ function createCodexAdapter(deps = {}) {
5283
4782
  const directory = req.local.cwd ?? "";
5284
4783
  const decision = decideResume3(req.session, directory);
5285
4784
  const resumeThreadId = "resume" in decision ? decision.resume : null;
4785
+ const threadPromptFingerprint = "resume" in decision ? decision.promptFingerprint : null;
5286
4786
  if ("fresh" in decision && decision.reason === "cwd_mismatch") {
5287
4787
  deps.onWarn?.(
5288
4788
  "codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread (SJ527-analogue guard)",
@@ -5308,7 +4808,12 @@ function createCodexAdapter(deps = {}) {
5308
4808
  }
5309
4809
  }
5310
4810
  try {
5311
- const spec = buildRunSpec2(req, resumeThreadId, instructions?.path ?? null);
4811
+ const spec = buildRunSpec2(
4812
+ req,
4813
+ resumeThreadId,
4814
+ instructions?.path ?? null,
4815
+ threadPromptFingerprint
4816
+ );
5312
4817
  const result = await transport.run(spec, signal);
5313
4818
  if (signal.aborted) return;
5314
4819
  yield* decodeCodexStream(result.events, {
@@ -5316,6 +4821,14 @@ function createCodexAdapter(deps = {}) {
5316
4821
  cwd: req.local.cwd,
5317
4822
  resumedThreadId: resumeThreadId,
5318
4823
  degraded,
4824
+ // CT1075: which prompt the thread carries once this turn lands. If we sent
4825
+ // one, it's that one — a later copy is what the model reads, so a changed
4826
+ // prompt supersedes the older copy still sitting above it. If we withheld
4827
+ // it, the thread still carries whatever it did before. Read off the spec
4828
+ // rather than recomputed, so the recorded fact can't disagree with what
4829
+ // was actually sent.
4830
+ promptFingerprint: spec.promptRidesInput ? spec.promptFingerprint : threadPromptFingerprint,
4831
+ storedState: req.session,
5319
4832
  // CT601: Codex reports no model in-stream, so record what the thread was
5320
4833
  // started with (null on the default token). Same for the reasoning effort.
5321
4834
  resolvedModel: spec.model,
@@ -5350,9 +4863,10 @@ var COMPANION_POLICY2 = {
5350
4863
  uiPrompts: "never"
5351
4864
  };
5352
4865
  var DIR2 = "/env/here";
4866
+ var PROMPT = "system";
5353
4867
  function makeRequest3(overrides = {}) {
5354
4868
  return {
5355
- systemPrompt: "system",
4869
+ systemPrompt: PROMPT,
5356
4870
  prompt: "hi there",
5357
4871
  content: [{ type: "text", text: "hi there" }],
5358
4872
  // CT601: the resolved model + reasoning effort ride the terminal `result` off
@@ -5395,9 +4909,9 @@ var errorItem = (id, message) => ({
5395
4909
  type: "item.completed",
5396
4910
  item: { id, type: "error", message }
5397
4911
  });
5398
- var sessionEvent3 = (threadId, cwd = DIR2, degraded = false) => ({
4912
+ var sessionEvent3 = (threadId, cwd = DIR2, degraded = false, promptFingerprint = fingerprintPrompt(PROMPT)) => ({
5399
4913
  type: "session",
5400
- state: encodeSession3({ threadId, cwd }),
4914
+ state: encodeSession3({ threadId, cwd, promptFingerprint }),
5401
4915
  ...degraded ? { degraded: true } : {}
5402
4916
  });
5403
4917
  var CODEX_CONFORMANCE_FIXTURES = [
@@ -5855,23 +5369,91 @@ var CODEX_CONFORMANCE_FIXTURES = [
5855
5369
  {
5856
5370
  // Session resume: the stored state matches the current directory, so the adapter
5857
5371
  // resumes its thread id and `thread.started` echoes the SAME id — NO session
5858
- // event fires (the id didn't change). This is what carries cross-turn memory.
5372
+ // event fires (the id didn't change AND the state is unchanged). This is what
5373
+ // carries cross-turn memory.
5374
+ //
5375
+ // CT1075: the stored state also says the thread already holds the composed
5376
+ // prompt, so this turn withholds the preamble and the state it would write is
5377
+ // byte-identical to the stored one. The silence is now proof of BOTH: an id that
5378
+ // didn't change and a prompt that didn't need re-sending.
5859
5379
  name: "session resume (no re-emit on echo)",
5860
- request: makeRequest3({ session: encodeSession3({ threadId: "th1", cwd: DIR2 }) }),
5380
+ request: makeRequest3({
5381
+ session: encodeSession3({
5382
+ threadId: "th1",
5383
+ cwd: DIR2,
5384
+ promptFingerprint: fingerprintPrompt(PROMPT)
5385
+ })
5386
+ }),
5861
5387
  nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
5862
5388
  expected: [
5863
5389
  { type: "text", body: "Back again.", terminal: true },
5864
5390
  { type: "result", ok: true }
5865
5391
  ]
5866
5392
  },
5393
+ {
5394
+ // CT1075: a resumed thread the platform's state does NOT vouch for — a state
5395
+ // written before CT1075 existed, so it carries no `promptFingerprint` field. The
5396
+ // adapter reads the absence as "we can't say which prompt this thread holds",
5397
+ // sends the full prompt exactly as it did pre-CT1075, and re-emits the state with
5398
+ // the fingerprint recorded — on the SAME thread id, which pre-CT1075 emitted
5399
+ // nothing at all. That re-emit is the upgrade path: without it a long-lived
5400
+ // thread would re-send the whole prompt forever.
5401
+ //
5402
+ // This is the fixture that fails if the static half is withheld on a thread we
5403
+ // have no evidence about.
5404
+ name: "session resume with a pre-CT1075 state re-emits with the prompt fingerprint",
5405
+ request: makeRequest3({
5406
+ session: JSON.stringify({ threadId: "th1", cwd: DIR2 })
5407
+ }),
5408
+ nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
5409
+ expected: [
5410
+ sessionEvent3("th1"),
5411
+ { type: "text", body: "Back again.", terminal: true },
5412
+ { type: "result", ok: true }
5413
+ ]
5414
+ },
5415
+ {
5416
+ // CT1075 (review finding): the thread carries a DIFFERENT prompt than the one
5417
+ // this turn composed — a charter edit, a renamed agent, a redeployed prompt
5418
+ // body. The stored fingerprint doesn't match, so the new prompt is sent exactly
5419
+ // as on a fresh thread and the state is re-recorded against it.
5420
+ //
5421
+ // This is the fixture that fails if a stale prompt is treated as good enough. A
5422
+ // boolean flag would have passed the resume fixture above and silently withheld
5423
+ // every future charter edit from this thread.
5424
+ name: "session resume with a stale prompt fingerprint re-sends the prompt",
5425
+ request: makeRequest3({
5426
+ session: encodeSession3({
5427
+ threadId: "th1",
5428
+ cwd: DIR2,
5429
+ promptFingerprint: fingerprintPrompt("an older composed prompt")
5430
+ })
5431
+ }),
5432
+ nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
5433
+ expected: [
5434
+ sessionEvent3("th1"),
5435
+ { type: "text", body: "Back again.", terminal: true },
5436
+ { type: "result", ok: true }
5437
+ ]
5438
+ },
5867
5439
  {
5868
5440
  // Directory-mismatch degrade (the SJ527 analogue): the stored state was created
5869
5441
  // under a DIFFERENT directory, so the adapter refuses to resume and starts
5870
5442
  // fresh — `thread.started` reports a NEW id, and the emitted session event
5871
5443
  // records the CURRENT directory with `degraded: true` so the host rewinds the
5872
5444
  // catch-up mark.
5445
+ //
5446
+ // CT1075: the refused resume means the new thread is EMPTY, so the full prompt
5447
+ // goes in even though the stored state vouched for a copy — of a thread we're
5448
+ // no longer in. The emitted state records the fresh thread's own answer.
5873
5449
  name: "session directory-mismatch degrades to fresh",
5874
- request: makeRequest3({ session: encodeSession3({ threadId: "old", cwd: "/env/gone" }) }),
5450
+ request: makeRequest3({
5451
+ session: encodeSession3({
5452
+ threadId: "old",
5453
+ cwd: "/env/gone",
5454
+ promptFingerprint: fingerprintPrompt(PROMPT)
5455
+ })
5456
+ }),
5875
5457
  nativeStream: [
5876
5458
  threadStarted(NEW_THREAD_ID),
5877
5459
  agentMessage("m1", "Fresh start."),
@@ -5938,7 +5520,7 @@ var ConnectorHealthStore = class {
5938
5520
 
5939
5521
  // src/dispatcher.ts
5940
5522
  import { randomUUID } from "crypto";
5941
- import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync as statSync2 } from "fs";
5523
+ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync } from "fs";
5942
5524
  import { join as join13 } from "path";
5943
5525
 
5944
5526
  // src/summon.ts
@@ -6237,9 +5819,16 @@ function buildCompanionTurnRequest(params) {
6237
5819
  ...t.workspaceToolSurface ? { workspaceToolSurface: t.workspaceToolSurface } : {},
6238
5820
  // CT714: mount the turn-control surface ONLY when a real turn token backs
6239
5821
  // this turn — the surface admits `turn_token` auth exclusively, so a
6240
- // PAT-fallback bearer (older API / unresolved delegation) would be rejected
6241
- // there. Absent it, external adapters simply don't mount it that turn (the
6242
- // same graceful degrade as the rest of the OBO path).
5822
+ // PAT-fallback bearer would be rejected there. Absent it, external adapters
5823
+ // simply don't mount it that turn.
5824
+ //
5825
+ // CT1074: that fallback is now an OLD-API case only. The server mints a token
5826
+ // for every turn it dispatched, so "no token" no longer means "no human behind
5827
+ // the turn" — it means the API predates this change. The condition is right as
5828
+ // it stands and stays where the failure is honest (a rejected bearer is worse
5829
+ // than an unmounted server); what was wrong was upstream, where an undelegated
5830
+ // turn minted nothing and this silently dropped all five verbs on Codex and
5831
+ // opencode with no refusal anyone could see.
6243
5832
  ...params.turnToken ? { turnControlUrl: turnControlMcpUrl(params.baseUrl) } : {}
6244
5833
  },
6245
5834
  local: {
@@ -6269,10 +5858,10 @@ import { join as join9 } from "path";
6269
5858
  var PREFIX = "cabane-codex-instructions-";
6270
5859
  async function writeCodexInstructionsFile(contents) {
6271
5860
  const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
6272
- const path3 = join9(dir2, "instructions.md");
6273
- await writeFile(path3, contents, { encoding: "utf8", mode: 384 });
5861
+ const path = join9(dir2, "instructions.md");
5862
+ await writeFile(path, contents, { encoding: "utf8", mode: 384 });
6274
5863
  return {
6275
- path: path3,
5864
+ path,
6276
5865
  cleanup: async () => {
6277
5866
  await rm(dir2, { recursive: true, force: true });
6278
5867
  }
@@ -6292,10 +5881,10 @@ function pathFor3(workspaceId, conversationId, agentId) {
6292
5881
  return join10(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6293
5882
  }
6294
5883
  function readPrepared(workspaceId, conversationId, agentId) {
6295
- const path3 = pathFor3(workspaceId, conversationId, agentId);
6296
- if (!existsSync7(path3)) return null;
5884
+ const path = pathFor3(workspaceId, conversationId, agentId);
5885
+ if (!existsSync7(path)) return null;
6297
5886
  try {
6298
- const parsed = JSON.parse(readFileSync6(path3, "utf8"));
5887
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
6299
5888
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
6300
5889
  return {
6301
5890
  cwd: parsed.cwd,
@@ -6329,14 +5918,14 @@ function secretsPath() {
6329
5918
  }
6330
5919
  var secretStoreSchema = z13.record(z13.string(), z13.string());
6331
5920
  function loadSecretStore() {
6332
- const path3 = secretsPath();
6333
- if (!existsSync8(path3)) return makeStore({});
5921
+ const path = secretsPath();
5922
+ if (!existsSync8(path)) return makeStore({});
6334
5923
  let raw;
6335
5924
  try {
6336
- raw = readFileSync7(path3, "utf8");
5925
+ raw = readFileSync7(path, "utf8");
6337
5926
  } catch (err) {
6338
5927
  throw new ConfigError(
6339
- `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
5928
+ `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
6340
5929
  );
6341
5930
  }
6342
5931
  if (raw.trim().length === 0) return makeStore({});
@@ -6345,13 +5934,13 @@ function loadSecretStore() {
6345
5934
  parsed = JSON.parse(raw);
6346
5935
  } catch (err) {
6347
5936
  throw new ConfigError(
6348
- `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
5937
+ `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
6349
5938
  );
6350
5939
  }
6351
5940
  const result = secretStoreSchema.safeParse(parsed);
6352
5941
  if (!result.success) {
6353
5942
  throw new ConfigError(
6354
- `${path3} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
5943
+ `${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
6355
5944
  );
6356
5945
  }
6357
5946
  return makeStore(result.data);
@@ -6787,7 +6376,7 @@ function checkoutState(cwd) {
6787
6376
  if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
6788
6377
  let stat;
6789
6378
  try {
6790
- stat = statSync2(gitPath);
6379
+ stat = statSync(gitPath);
6791
6380
  } catch (error) {
6792
6381
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
6793
6382
  }
@@ -8218,8 +7807,8 @@ function sleep2(ms) {
8218
7807
  }
8219
7808
 
8220
7809
  // src/version.ts
8221
- import { createRequire as createRequire2 } from "module";
8222
- var pkg = createRequire2(import.meta.url)("../package.json");
7810
+ import { createRequire } from "module";
7811
+ var pkg = createRequire(import.meta.url)("../package.json");
8223
7812
  var COMPANION_VERSION = pkg.version;
8224
7813
 
8225
7814
  // src/supervisor.ts
@@ -9023,9 +8612,9 @@ var CompanionSupervisor = class {
9023
8612
  };
9024
8613
  function defaultReexec() {
9025
8614
  clearRuntimeState();
9026
- void import("child_process").then(({ spawn: spawn5 }) => {
8615
+ void import("child_process").then(({ spawn: spawn4 }) => {
9027
8616
  try {
9028
- const child = spawn5(process.execPath, process.argv.slice(1), {
8617
+ const child = spawn4(process.execPath, process.argv.slice(1), {
9029
8618
  stdio: "inherit",
9030
8619
  detached: false
9031
8620
  });
@@ -9109,8 +8698,8 @@ function recordCrash(rec) {
9109
8698
  }
9110
8699
  function clearCrash() {
9111
8700
  try {
9112
- const path3 = crashMarkerPath();
9113
- if (existsSync11(path3)) rmSync7(path3, { force: true });
8701
+ const path = crashMarkerPath();
8702
+ if (existsSync11(path)) rmSync7(path, { force: true });
9114
8703
  } catch {
9115
8704
  }
9116
8705
  }