@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/cli.js CHANGED
@@ -316,14 +316,14 @@ function localAgentConfig(cfg, agent) {
316
316
  return map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
317
317
  }
318
318
  function loadConfig() {
319
- const path3 = configPath();
320
- if (!existsSync(path3)) return null;
319
+ const path = configPath();
320
+ if (!existsSync(path)) return null;
321
321
  let raw;
322
322
  try {
323
- raw = readFileSync(path3, "utf8");
323
+ raw = readFileSync(path, "utf8");
324
324
  } catch (err) {
325
325
  throw new ConfigError(
326
- `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
326
+ `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
327
327
  );
328
328
  }
329
329
  if (raw.trim().length === 0) return null;
@@ -332,7 +332,7 @@ function loadConfig() {
332
332
  parsed = JSON.parse(raw);
333
333
  } catch (err) {
334
334
  throw new ConfigError(
335
- `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
335
+ `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
336
336
  );
337
337
  }
338
338
  const result = companionConfigSchema.safeParse(parsed);
@@ -340,22 +340,22 @@ function loadConfig() {
340
340
  const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
341
341
  if (agentIssue) {
342
342
  throw new ConfigError(
343
- `${path3}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
343
+ `${path}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
344
344
  );
345
345
  }
346
346
  throw new ConfigError(
347
- `${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.`
347
+ `${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.`
348
348
  );
349
349
  }
350
350
  return result.data;
351
351
  }
352
352
  function loadConfigTolerant() {
353
- const path3 = configPath();
353
+ const path = configPath();
354
354
  const empty = {};
355
- if (!existsSync(path3)) return { local: empty, note: null };
355
+ if (!existsSync(path)) return { local: empty, note: null };
356
356
  let raw;
357
357
  try {
358
- raw = readFileSync(path3, "utf8");
358
+ raw = readFileSync(path, "utf8");
359
359
  } catch {
360
360
  return { local: empty, note: null };
361
361
  }
@@ -364,7 +364,7 @@ function loadConfigTolerant() {
364
364
  try {
365
365
  parsed = JSON.parse(raw);
366
366
  } catch {
367
- return { local: empty, note: `${path3} was unreadable (invalid JSON) and has been reset.` };
367
+ return { local: empty, note: `${path} was unreadable (invalid JSON) and has been reset.` };
368
368
  }
369
369
  const strict = companionConfigSchema.safeParse(parsed);
370
370
  if (strict.success) {
@@ -390,24 +390,24 @@ function loadConfigTolerant() {
390
390
  }
391
391
  return {
392
392
  local,
393
- note: `the existing ${path3} was from an older or incompatible companion; re-pairing rewrote it.`
393
+ note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`
394
394
  };
395
395
  }
396
396
  function saveConfig(cfg) {
397
- const path3 = configPath();
398
- mkdirSync(dirname(path3), { recursive: true });
397
+ const path = configPath();
398
+ mkdirSync(dirname(path), { recursive: true });
399
399
  try {
400
400
  chmodSync(cabaneDir(), 448);
401
401
  } catch {
402
402
  }
403
- const tmp = `${path3}.${process.pid}.tmp`;
403
+ const tmp = `${path}.${process.pid}.tmp`;
404
404
  try {
405
405
  writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
406
406
  try {
407
407
  chmodSync(tmp, 384);
408
408
  } catch {
409
409
  }
410
- renameSync(tmp, path3);
410
+ renameSync(tmp, path);
411
411
  } catch (err) {
412
412
  try {
413
413
  rmSync(tmp, { force: true });
@@ -426,9 +426,9 @@ function requireConfig() {
426
426
  return cfg;
427
427
  }
428
428
  function deleteConfig() {
429
- const path3 = configPath();
430
- if (existsSync(path3)) {
431
- writeFileSync(path3, "", { mode: 384 });
429
+ const path = configPath();
430
+ if (existsSync(path)) {
431
+ writeFileSync(path, "", { mode: 384 });
432
432
  }
433
433
  }
434
434
 
@@ -462,8 +462,8 @@ function consoleMessageFormat(log, messageKey) {
462
462
  var cached = null;
463
463
  function getLogger() {
464
464
  if (cached) return cached;
465
- const path3 = companionLogPath();
466
- mkdirSync2(dirname2(path3), { recursive: true });
465
+ const path = companionLogPath();
466
+ mkdirSync2(dirname2(path), { recursive: true });
467
467
  const streams = [];
468
468
  if (process.env.CABANE_COMPANION_DAEMON !== "1") {
469
469
  const consoleStream = pretty({
@@ -473,7 +473,7 @@ function getLogger() {
473
473
  });
474
474
  streams.push({ level: "info", stream: consoleStream });
475
475
  }
476
- streams.push({ level: "debug", stream: createWriteStream(path3, { flags: "a" }) });
476
+ streams.push({ level: "debug", stream: createWriteStream(path, { flags: "a" }) });
477
477
  cached = pino({ level: "debug" }, pino.multistream(streams));
478
478
  return cached;
479
479
  }
@@ -634,9 +634,9 @@ function serialize(state) {
634
634
  return JSON.stringify(state, null, 2) + "\n";
635
635
  }
636
636
  function writeRuntimeState(state) {
637
- const path3 = runtimePath();
637
+ const path = runtimePath();
638
638
  mkdirSync3(cabaneDir(), { recursive: true });
639
- writeFileSync2(path3, serialize(state), "utf8");
639
+ writeFileSync2(path, serialize(state), "utf8");
640
640
  }
641
641
  function acquireRuntimeState(state) {
642
642
  const live = readLiveRuntimeState();
@@ -656,15 +656,15 @@ function acquireRuntimeState(state) {
656
656
  return { acquired: true };
657
657
  }
658
658
  function clearRuntimeState() {
659
- const path3 = runtimePath();
660
- if (existsSync2(path3)) rmSync2(path3, { force: true });
659
+ const path = runtimePath();
660
+ if (existsSync2(path)) rmSync2(path, { force: true });
661
661
  }
662
662
  function readLiveRuntimeState() {
663
- const path3 = runtimePath();
664
- if (!existsSync2(path3)) return null;
663
+ const path = runtimePath();
664
+ if (!existsSync2(path)) return null;
665
665
  let parsed;
666
666
  try {
667
- parsed = JSON.parse(readFileSync2(path3, "utf8"));
667
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
668
668
  } catch {
669
669
  return null;
670
670
  }
@@ -795,11 +795,11 @@ function credentialsPath() {
795
795
  }
796
796
  var credentialStoreSchema = z3.record(z3.string(), z3.string());
797
797
  function load() {
798
- const path3 = credentialsPath();
799
- if (!existsSync3(path3)) return {};
798
+ const path = credentialsPath();
799
+ if (!existsSync3(path)) return {};
800
800
  let raw;
801
801
  try {
802
- raw = readFileSync3(path3, "utf8");
802
+ raw = readFileSync3(path, "utf8");
803
803
  } catch {
804
804
  return {};
805
805
  }
@@ -812,20 +812,20 @@ function load() {
812
812
  }
813
813
  }
814
814
  function save(map) {
815
- const path3 = credentialsPath();
816
- mkdirSync5(dirname3(path3), { recursive: true });
815
+ const path = credentialsPath();
816
+ mkdirSync5(dirname3(path), { recursive: true });
817
817
  try {
818
818
  chmodSync2(cabaneDir(), 448);
819
819
  } catch {
820
820
  }
821
- const tmp = `${path3}.${process.pid}.tmp`;
821
+ const tmp = `${path}.${process.pid}.tmp`;
822
822
  try {
823
823
  writeFileSync3(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
824
824
  try {
825
825
  chmodSync2(tmp, 384);
826
826
  } catch {
827
827
  }
828
- renameSync2(tmp, path3);
828
+ renameSync2(tmp, path);
829
829
  } catch (err) {
830
830
  try {
831
831
  rmSync3(tmp, { force: true });
@@ -857,8 +857,8 @@ function pruneCredentials(keepAgentIds) {
857
857
  return removed;
858
858
  }
859
859
  function clearCredentials() {
860
- const path3 = credentialsPath();
861
- if (existsSync3(path3)) writeFileSync3(path3, "", { mode: 384 });
860
+ const path = credentialsPath();
861
+ if (existsSync3(path)) writeFileSync3(path, "", { mode: 384 });
862
862
  }
863
863
 
864
864
  // src/commands/logout.ts
@@ -897,10 +897,10 @@ async function logout(opts = {}) {
897
897
  function trimBase(baseUrl) {
898
898
  return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
899
899
  }
900
- async function postJson(baseUrl, path3, body) {
900
+ async function postJson(baseUrl, path, body) {
901
901
  let res;
902
902
  try {
903
- res = await fetch(`${trimBase(baseUrl)}${path3}`, {
903
+ res = await fetch(`${trimBase(baseUrl)}${path}`, {
904
904
  method: "POST",
905
905
  headers: { "content-type": "application/json", accept: "application/json" },
906
906
  body: JSON.stringify(body)
@@ -920,7 +920,7 @@ async function postJson(baseUrl, path3, body) {
920
920
  }
921
921
  }
922
922
  if (res.status >= 400) {
923
- if (res.status === 404 && path3.endsWith("/code")) {
923
+ if (res.status === 404 && path.endsWith("/code")) {
924
924
  throw new CompanionError(
925
925
  `no device-flow pairing endpoint at ${baseUrl}. Either that server predates \`cabane-companion pair\`, or it isn't a Cabane API origin \u2014 the hosted app is https://app.cabane.ai (pass \`--server <url>\` for your own instance).`
926
926
  );
@@ -1458,12 +1458,12 @@ function clampLimit(raw, fallback, max = 200) {
1458
1458
  if (!Number.isFinite(n) || n <= 0) return fallback;
1459
1459
  return Math.min(Math.floor(n), max);
1460
1460
  }
1461
- function tailFile(path3, lines) {
1462
- if (!existsSync4(path3)) return [];
1461
+ function tailFile(path, lines) {
1462
+ if (!existsSync4(path)) return [];
1463
1463
  const MAX_BYTES = 256 * 1024;
1464
1464
  let fd;
1465
1465
  try {
1466
- fd = openSync3(path3, "r");
1466
+ fd = openSync3(path, "r");
1467
1467
  const size = fstatSync(fd).size;
1468
1468
  const start2 = Math.max(0, size - MAX_BYTES);
1469
1469
  const len = size - start2;
@@ -1574,8 +1574,8 @@ var CabaneApi = class {
1574
1574
  // One HTTP attempt — no retry. Throws `ApiError` on a 4xx/5xx response and
1575
1575
  // rethrows transport errors (fetch reject) unchanged so the caller's retry
1576
1576
  // logic can classify them.
1577
- async attempt(method, path3, body, signal) {
1578
- const res = await fetch(`${this.base}${path3}`, {
1577
+ async attempt(method, path, body, signal) {
1578
+ const res = await fetch(`${this.base}${path}`, {
1579
1579
  method,
1580
1580
  headers: {
1581
1581
  Authorization: `Bearer ${this.opts.token}`,
@@ -1601,12 +1601,12 @@ var CabaneApi = class {
1601
1601
  }
1602
1602
  return parsed;
1603
1603
  }
1604
- async request(method, path3, body, opts = {}) {
1604
+ async request(method, path, body, opts = {}) {
1605
1605
  const { signal, retry = false } = opts;
1606
1606
  const maxAttempts = retry ? RETRY_BACKOFF_MS.length + 1 : 1;
1607
1607
  for (let attempt = 1; ; attempt++) {
1608
1608
  try {
1609
- return await this.attempt(method, path3, body, signal);
1609
+ return await this.attempt(method, path, body, signal);
1610
1610
  } catch (err) {
1611
1611
  if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
1612
1612
  await sleep2(RETRY_BACKOFF_MS[attempt - 1], signal);
@@ -1632,9 +1632,9 @@ var CabaneApi = class {
1632
1632
  // delivers it once the API returns. `(turnId, seq)` is the server's
1633
1633
  // idempotency key, so a replay whose original POST's fate is unknown
1634
1634
  // converges instead of duplicating.
1635
- async durableCommit(kind, path3, body, turnId, seq, signal) {
1635
+ async durableCommit(kind, path, body, turnId, seq, signal) {
1636
1636
  try {
1637
- await this.request("POST", path3, body, {
1637
+ await this.request("POST", path, body, {
1638
1638
  retry: true,
1639
1639
  ...signal ? { signal } : {}
1640
1640
  });
@@ -1643,7 +1643,7 @@ var CabaneApi = class {
1643
1643
  if (!outbox) throw err;
1644
1644
  if (signal?.aborted || isAbortError(err)) throw err;
1645
1645
  if (!isRetryable(err)) throw err;
1646
- outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path: path3, body, kind });
1646
+ outbox.persist({ enqueuedAt: Date.now(), turnId, seq, method: "POST", path, body, kind });
1647
1647
  this.opts.log?.warn(
1648
1648
  { kind, turnId, seq, err: err instanceof Error ? err.message : String(err) },
1649
1649
  "companion: commit queued to outbox after transient failure (will drain when the API returns)"
@@ -1795,12 +1795,12 @@ var CabaneApi = class {
1795
1795
  // left best-effort: it's lower-stakes and self-heals on the next turn, so it
1796
1796
  // stays a single-shot PATCH and is deliberately out of CT93's scope.
1797
1797
  setActiveRun(workspaceId, conversationId, agentId, body) {
1798
- const path3 = `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/active-run`;
1798
+ const path = `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/active-run`;
1799
1799
  const touchesFlag = Object.prototype.hasOwnProperty.call(body, "activeRunStartedAt");
1800
1800
  if (touchesFlag && this.opts.outbox) {
1801
- return this.durableActiveRunWrite(path3, conversationId, agentId, body);
1801
+ return this.durableActiveRunWrite(path, conversationId, agentId, body);
1802
1802
  }
1803
- return this.request("PATCH", path3, body);
1803
+ return this.request("PATCH", path, body);
1804
1804
  }
1805
1805
  // CT93: send-or-enqueue for an active-run flag write, with last-writer-wins
1806
1806
  // across the (conversation, agent) pair. Mirrors `durableCommit`, with two
@@ -1814,11 +1814,11 @@ var CabaneApi = class {
1814
1814
  // later and clobber the state we just wrote (the cross-turn race: turn N's
1815
1815
  // queued clear vs. turn N+1's live set). Combined with persist-overwrites-
1816
1816
  // by-key, this is the full last-writer-wins guarantee.
1817
- async durableActiveRunWrite(path3, conversationId, agentId, body) {
1817
+ async durableActiveRunWrite(path, conversationId, agentId, body) {
1818
1818
  const outbox = this.opts.outbox;
1819
1819
  const key = activeRunOutboxKey(conversationId, agentId);
1820
1820
  try {
1821
- await this.request("PATCH", path3, body, { retry: true });
1821
+ await this.request("PATCH", path, body, { retry: true });
1822
1822
  outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
1823
1823
  } catch (err) {
1824
1824
  if (!outbox) throw err;
@@ -1831,7 +1831,7 @@ var CabaneApi = class {
1831
1831
  turnId: key,
1832
1832
  seq: ACTIVE_RUN_OUTBOX_SEQ,
1833
1833
  method: "PATCH",
1834
- path: path3,
1834
+ path,
1835
1835
  body,
1836
1836
  kind: "active-run"
1837
1837
  });
@@ -1919,8 +1919,8 @@ var CabaneApi = class {
1919
1919
  // shared resolver the in-app path uses. Omitting it (older call sites) returns
1920
1920
  // the agent default — graceful degradation, no version coupling.
1921
1921
  getAgentSelf(conversationId) {
1922
- const path3 = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
1923
- return this.request("GET", path3);
1922
+ const path = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
1923
+ return this.request("GET", path);
1924
1924
  }
1925
1925
  // The companion fetches the triggering message body by listing the
1926
1926
  // conversation's messages and finding the one with `id === messageId`.
@@ -1975,8 +1975,8 @@ var DeviceApi = class {
1975
1975
  get base() {
1976
1976
  return this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
1977
1977
  }
1978
- async request(method, path3, body) {
1979
- const res = await fetch(`${this.base}${path3}`, {
1978
+ async request(method, path, body) {
1979
+ const res = await fetch(`${this.base}${path}`, {
1980
1980
  method,
1981
1981
  headers: {
1982
1982
  Authorization: `Bearer ${this.opts.deviceToken}`,
@@ -2030,15 +2030,15 @@ function pathFor(workspaceId) {
2030
2030
  return join7(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
2031
2031
  }
2032
2032
  function readCursor(workspaceId) {
2033
- const path3 = pathFor(workspaceId);
2034
- if (!existsSync5(path3)) return null;
2035
- const raw = readFileSync4(path3, "utf8").trim();
2033
+ const path = pathFor(workspaceId);
2034
+ if (!existsSync5(path)) return null;
2035
+ const raw = readFileSync4(path, "utf8").trim();
2036
2036
  return raw.length > 0 ? raw : null;
2037
2037
  }
2038
2038
  function writeCursor(workspaceId, eventId) {
2039
- const path3 = pathFor(workspaceId);
2039
+ const path = pathFor(workspaceId);
2040
2040
  mkdirSync6(join7(cabaneDir(), "cursors"), { recursive: true });
2041
- writeFileSync4(path3, eventId + "\n", "utf8");
2041
+ writeFileSync4(path, eventId + "\n", "utf8");
2042
2042
  }
2043
2043
 
2044
2044
  // src/cursor-tracker.ts
@@ -2091,10 +2091,10 @@ function pathFor2(log, workspaceId) {
2091
2091
  return join8(dir(log), encodeURIComponent(workspaceId));
2092
2092
  }
2093
2093
  function readIds(log, workspaceId) {
2094
- const path3 = pathFor2(log, workspaceId);
2095
- if (!existsSync6(path3)) return [];
2094
+ const path = pathFor2(log, workspaceId);
2095
+ if (!existsSync6(path)) return [];
2096
2096
  try {
2097
- return readFileSync5(path3, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2097
+ return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2098
2098
  } catch {
2099
2099
  return [];
2100
2100
  }
@@ -2131,10 +2131,10 @@ function resumePathFor(workspaceId) {
2131
2131
  }
2132
2132
  function readResumeCounts(workspaceId) {
2133
2133
  const out = /* @__PURE__ */ new Map();
2134
- const path3 = resumePathFor(workspaceId);
2135
- if (!existsSync6(path3)) return out;
2134
+ const path = resumePathFor(workspaceId);
2135
+ if (!existsSync6(path)) return out;
2136
2136
  try {
2137
- for (const line of readFileSync5(path3, "utf8").split("\n")) {
2137
+ for (const line of readFileSync5(path, "utf8").split("\n")) {
2138
2138
  const trimmed = line.trim();
2139
2139
  if (!trimmed) continue;
2140
2140
  const tab = trimmed.lastIndexOf(" ");
@@ -4726,6 +4726,16 @@ function readMcpError(error) {
4726
4726
  }
4727
4727
 
4728
4728
  // packages/agent-runtime/src/codex/session.ts
4729
+ function fingerprintPrompt(prompt) {
4730
+ let fnv = 2166136261;
4731
+ let djb = 5381;
4732
+ for (let i = 0; i < prompt.length; i += 1) {
4733
+ const code = prompt.charCodeAt(i);
4734
+ fnv = Math.imul(fnv ^ code, 16777619);
4735
+ djb = Math.imul(djb, 33) + code | 0;
4736
+ }
4737
+ return `${prompt.length}.${(fnv >>> 0).toString(36)}.${(djb >>> 0).toString(36)}`;
4738
+ }
4729
4739
  function encodeSession3(state) {
4730
4740
  return JSON.stringify(state);
4731
4741
  }
@@ -4745,7 +4755,10 @@ function decideResume3(stored, currentCwd) {
4745
4755
  if (storedCwd !== (currentCwd ?? "")) {
4746
4756
  return { fresh: true, reason: "cwd_mismatch" };
4747
4757
  }
4748
- return { resume: state.threadId };
4758
+ return {
4759
+ resume: state.threadId,
4760
+ promptFingerprint: typeof state.promptFingerprint === "string" ? state.promptFingerprint : null
4761
+ };
4749
4762
  }
4750
4763
 
4751
4764
  // packages/agent-runtime/src/codex/loop.ts
@@ -4768,13 +4781,20 @@ async function* decodeCodexStream(events, ctx) {
4768
4781
  if (ctx.signal.aborted) return;
4769
4782
  if (ev.type === "thread.started") {
4770
4783
  const threadId = readThreadId(ev);
4771
- if (!sessionEmitted && threadId && threadId !== ctx.resumedThreadId) {
4772
- sessionEmitted = true;
4773
- yield {
4774
- type: "session",
4775
- state: encodeSession3({ threadId, cwd: ctx.cwd ?? "" }),
4776
- ...ctx.degraded ? { degraded: true } : {}
4777
- };
4784
+ if (threadId && !sessionEmitted) {
4785
+ const state = encodeSession3({
4786
+ threadId,
4787
+ cwd: ctx.cwd ?? "",
4788
+ promptFingerprint: ctx.promptFingerprint ?? null
4789
+ });
4790
+ if (threadId !== ctx.resumedThreadId || state !== ctx.storedState) {
4791
+ sessionEmitted = true;
4792
+ yield {
4793
+ type: "session",
4794
+ state,
4795
+ ...ctx.degraded ? { degraded: true } : {}
4796
+ };
4797
+ }
4778
4798
  }
4779
4799
  continue;
4780
4800
  }
@@ -4897,7 +4917,7 @@ function codexToolPolicy(policy) {
4897
4917
  networkAccessEnabled: policy.web
4898
4918
  };
4899
4919
  }
4900
- var CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
4920
+ var CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
4901
4921
  var codexDialectSchema = z11.object({
4902
4922
  modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
4903
4923
  }).loose();
@@ -4919,7 +4939,7 @@ var CABANE_MCP_SERVER3 = "cabane";
4919
4939
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
4920
4940
  var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
4921
4941
  var ENV_ENVELOPE_KEYS = ["CABANE_ENV_TIER", "CABANE_ENV_KEY", "CABANE_ENV_BINDING"];
4922
- function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4942
+ function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null) {
4923
4943
  const { policy, config } = req;
4924
4944
  const directory = req.local.cwd ?? "";
4925
4945
  const dialect = readCodexDialect(config.runtimeOptions);
@@ -4930,6 +4950,9 @@ function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4930
4950
  `[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.`
4931
4951
  );
4932
4952
  }
4953
+ const promptFingerprint = fingerprintPrompt(req.systemPrompt);
4954
+ const threadHasThisPrompt = resumeThreadId !== null && threadPromptFingerprint !== null && threadPromptFingerprint === promptFingerprint;
4955
+ const promptRidesInput = baseInstructionsFile === null && !threadHasThisPrompt;
4933
4956
  return {
4934
4957
  resumeThreadId,
4935
4958
  directory,
@@ -4938,14 +4961,16 @@ function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4938
4961
  skipGitRepoCheck: true,
4939
4962
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
4940
4963
  baseInstructionsFile,
4941
- input: buildInput(req, baseInstructionsFile !== null),
4964
+ input: buildInput(req, promptRidesInput),
4965
+ promptRidesInput,
4966
+ promptFingerprint,
4942
4967
  config: buildConfig(req, baseInstructionsFile)
4943
4968
  };
4944
4969
  }
4945
- function buildInput(req, promptRidesInstructionsFile) {
4970
+ function buildInput(req, promptRidesInput) {
4946
4971
  const userText = req.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
4947
4972
  const body = userText.trim().length > 0 ? userText : req.prompt;
4948
- if (promptRidesInstructionsFile) return body;
4973
+ if (!promptRidesInput) return body;
4949
4974
  const system = req.systemPrompt.trim();
4950
4975
  return system.length > 0 ? `${system}
4951
4976
 
@@ -5047,536 +5072,10 @@ function isStringRecord2(v) {
5047
5072
  return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
5048
5073
  }
5049
5074
 
5050
- // node_modules/.pnpm/@openai+codex-sdk@0.146.0/node_modules/@openai/codex-sdk/dist/index.js
5051
- import { promises as fs } from "fs";
5052
- import os from "os";
5053
- import path from "path";
5054
- import { spawn as spawn6 } from "child_process";
5055
- import { statSync } from "fs";
5056
- import path2 from "path";
5057
- import readline from "readline";
5058
- import { createRequire } from "module";
5059
- async function createOutputSchemaFile(schema) {
5060
- if (schema === void 0) {
5061
- return { cleanup: async () => {
5062
- } };
5063
- }
5064
- if (!isJsonObject(schema)) {
5065
- throw new Error("outputSchema must be a plain JSON object");
5066
- }
5067
- const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
5068
- const schemaPath = path.join(schemaDir, "schema.json");
5069
- const cleanup = async () => {
5070
- try {
5071
- await fs.rm(schemaDir, { recursive: true, force: true });
5072
- } catch {
5073
- }
5074
- };
5075
- try {
5076
- await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
5077
- return { schemaPath, cleanup };
5078
- } catch (error) {
5079
- await cleanup();
5080
- throw error;
5081
- }
5082
- }
5083
- function isJsonObject(value) {
5084
- return typeof value === "object" && value !== null && !Array.isArray(value);
5085
- }
5086
- var Thread = class {
5087
- _exec;
5088
- _options;
5089
- _id;
5090
- _threadOptions;
5091
- /** Returns the ID of the thread. Populated after the first turn starts. */
5092
- get id() {
5093
- return this._id;
5094
- }
5095
- /* @internal */
5096
- constructor(exec, options, threadOptions, id = null) {
5097
- this._exec = exec;
5098
- this._options = options;
5099
- this._id = id;
5100
- this._threadOptions = threadOptions;
5101
- }
5102
- /** Provides the input to the agent and streams events as they are produced during the turn. */
5103
- async runStreamed(input, turnOptions = {}) {
5104
- return { events: this.runStreamedInternal(input, turnOptions) };
5105
- }
5106
- async *runStreamedInternal(input, turnOptions = {}) {
5107
- const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema);
5108
- const options = this._threadOptions;
5109
- const { prompt, images } = normalizeInput(input);
5110
- const generator = this._exec.run({
5111
- input: prompt,
5112
- baseUrl: this._options.baseUrl,
5113
- apiKey: this._options.apiKey,
5114
- threadId: this._id,
5115
- images,
5116
- model: options?.model,
5117
- sandboxMode: options?.sandboxMode,
5118
- workingDirectory: options?.workingDirectory,
5119
- skipGitRepoCheck: options?.skipGitRepoCheck,
5120
- outputSchemaFile: schemaPath,
5121
- modelReasoningEffort: options?.modelReasoningEffort,
5122
- signal: turnOptions.signal,
5123
- networkAccessEnabled: options?.networkAccessEnabled,
5124
- webSearchMode: options?.webSearchMode,
5125
- webSearchEnabled: options?.webSearchEnabled,
5126
- approvalPolicy: options?.approvalPolicy,
5127
- additionalDirectories: options?.additionalDirectories
5128
- });
5129
- try {
5130
- for await (const item of generator) {
5131
- let parsed;
5132
- try {
5133
- parsed = JSON.parse(item);
5134
- } catch (error) {
5135
- throw new Error(`Failed to parse item: ${item}`, { cause: error });
5136
- }
5137
- if (parsed.type === "thread.started") {
5138
- this._id = parsed.thread_id;
5139
- } else if (parsed.type === "turn.completed") {
5140
- parsed.usage.cache_write_input_tokens ??= 0;
5141
- }
5142
- yield parsed;
5143
- }
5144
- } finally {
5145
- await cleanup();
5146
- }
5147
- }
5148
- /** Provides the input to the agent and returns the completed turn. */
5149
- async run(input, turnOptions = {}) {
5150
- const generator = this.runStreamedInternal(input, turnOptions);
5151
- const items = [];
5152
- let finalResponse = "";
5153
- let usage = null;
5154
- let turnFailure = null;
5155
- for await (const event of generator) {
5156
- if (event.type === "item.completed") {
5157
- if (event.item.type === "agent_message") {
5158
- finalResponse = event.item.text;
5159
- }
5160
- items.push(event.item);
5161
- } else if (event.type === "turn.completed") {
5162
- usage = event.usage;
5163
- } else if (event.type === "turn.failed") {
5164
- turnFailure = event.error;
5165
- break;
5166
- }
5167
- }
5168
- if (turnFailure) {
5169
- throw new Error(turnFailure.message);
5170
- }
5171
- return { items, finalResponse, usage };
5172
- }
5173
- };
5174
- function normalizeInput(input) {
5175
- if (typeof input === "string") {
5176
- return { prompt: input, images: [] };
5177
- }
5178
- const promptParts = [];
5179
- const images = [];
5180
- for (const item of input) {
5181
- if (item.type === "text") {
5182
- promptParts.push(item.text);
5183
- } else if (item.type === "local_image") {
5184
- images.push(item.path);
5185
- }
5186
- }
5187
- return { prompt: promptParts.join("\n\n"), images };
5188
- }
5189
- var INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
5190
- var TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
5191
- var CODEX_NPM_NAME = "@openai/codex";
5192
- var PLATFORM_PACKAGE_BY_TARGET = {
5193
- "x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
5194
- "aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
5195
- "x86_64-apple-darwin": "@openai/codex-darwin-x64",
5196
- "aarch64-apple-darwin": "@openai/codex-darwin-arm64",
5197
- "x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
5198
- "aarch64-pc-windows-msvc": "@openai/codex-win32-arm64"
5199
- };
5200
- var moduleRequire = createRequire(import.meta.url);
5201
- var CodexExec = class {
5202
- executablePath;
5203
- pathDirs;
5204
- envOverride;
5205
- configOverrides;
5206
- constructor(executablePath = null, env, configOverrides) {
5207
- if (executablePath) {
5208
- this.executablePath = executablePath;
5209
- this.pathDirs = [];
5210
- } else {
5211
- const resolved = findCodexPath();
5212
- this.executablePath = resolved.executablePath;
5213
- this.pathDirs = resolved.pathDirs;
5214
- }
5215
- this.envOverride = env;
5216
- this.configOverrides = configOverrides;
5217
- }
5218
- async *run(args) {
5219
- const commandArgs = ["exec", "--experimental-json"];
5220
- if (this.configOverrides) {
5221
- for (const override of serializeConfigOverrides(this.configOverrides)) {
5222
- commandArgs.push("--config", override);
5223
- }
5224
- }
5225
- if (args.baseUrl) {
5226
- commandArgs.push(
5227
- "--config",
5228
- `openai_base_url=${toTomlValue(args.baseUrl, "openai_base_url")}`
5229
- );
5230
- }
5231
- if (args.model) {
5232
- commandArgs.push("--model", args.model);
5233
- }
5234
- if (args.sandboxMode) {
5235
- commandArgs.push("--sandbox", args.sandboxMode);
5236
- }
5237
- if (args.workingDirectory) {
5238
- commandArgs.push("--cd", args.workingDirectory);
5239
- }
5240
- if (args.additionalDirectories?.length) {
5241
- for (const dir2 of args.additionalDirectories) {
5242
- commandArgs.push("--add-dir", dir2);
5243
- }
5244
- }
5245
- if (args.skipGitRepoCheck) {
5246
- commandArgs.push("--skip-git-repo-check");
5247
- }
5248
- if (args.outputSchemaFile) {
5249
- commandArgs.push("--output-schema", args.outputSchemaFile);
5250
- }
5251
- if (args.modelReasoningEffort) {
5252
- commandArgs.push("--config", `model_reasoning_effort="${args.modelReasoningEffort}"`);
5253
- }
5254
- if (args.networkAccessEnabled !== void 0) {
5255
- commandArgs.push(
5256
- "--config",
5257
- `sandbox_workspace_write.network_access=${args.networkAccessEnabled}`
5258
- );
5259
- }
5260
- if (args.webSearchMode) {
5261
- commandArgs.push("--config", `web_search="${args.webSearchMode}"`);
5262
- } else if (args.webSearchEnabled === true) {
5263
- commandArgs.push("--config", `web_search="live"`);
5264
- } else if (args.webSearchEnabled === false) {
5265
- commandArgs.push("--config", `web_search="disabled"`);
5266
- }
5267
- if (args.approvalPolicy) {
5268
- commandArgs.push("--config", `approval_policy="${args.approvalPolicy}"`);
5269
- }
5270
- if (args.threadId) {
5271
- commandArgs.push("resume", args.threadId);
5272
- }
5273
- if (args.images?.length) {
5274
- for (const image of args.images) {
5275
- commandArgs.push("--image", image);
5276
- }
5277
- }
5278
- const env = {};
5279
- if (this.envOverride) {
5280
- Object.assign(env, this.envOverride);
5281
- } else {
5282
- for (const [key, value] of Object.entries(process.env)) {
5283
- if (value !== void 0) {
5284
- env[key] = value;
5285
- }
5286
- }
5287
- }
5288
- if (!env[INTERNAL_ORIGINATOR_ENV]) {
5289
- env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR;
5290
- }
5291
- if (args.apiKey) {
5292
- env.CODEX_API_KEY = args.apiKey;
5293
- }
5294
- if (this.pathDirs.length > 0) {
5295
- prependPathDirs(env, this.pathDirs);
5296
- }
5297
- const child = spawn6(this.executablePath, commandArgs, {
5298
- env,
5299
- signal: args.signal
5300
- });
5301
- let spawnError = null;
5302
- child.once("error", (err) => spawnError = err);
5303
- if (!child.stdin) {
5304
- child.kill();
5305
- throw new Error("Child process has no stdin");
5306
- }
5307
- child.stdin.write(args.input);
5308
- child.stdin.end();
5309
- if (!child.stdout) {
5310
- child.kill();
5311
- throw new Error("Child process has no stdout");
5312
- }
5313
- const stderrChunks = [];
5314
- if (child.stderr) {
5315
- child.stderr.on("data", (data) => {
5316
- stderrChunks.push(data);
5317
- });
5318
- }
5319
- const exitPromise = new Promise(
5320
- (resolve) => {
5321
- child.once("exit", (code, signal) => {
5322
- resolve({ code, signal });
5323
- });
5324
- }
5325
- );
5326
- const rl = readline.createInterface({
5327
- input: child.stdout,
5328
- crlfDelay: Infinity
5329
- });
5330
- try {
5331
- for await (const line of rl) {
5332
- yield line;
5333
- }
5334
- if (spawnError) throw spawnError;
5335
- const { code, signal } = await exitPromise;
5336
- if (code !== 0 || signal) {
5337
- const stderrBuffer = Buffer.concat(stderrChunks);
5338
- const detail = signal ? `signal ${signal}` : `code ${code ?? 1}`;
5339
- throw new Error(`Codex Exec exited with ${detail}: ${stderrBuffer.toString("utf8")}`);
5340
- }
5341
- } finally {
5342
- rl.close();
5343
- child.removeAllListeners();
5344
- try {
5345
- if (!child.killed) child.kill();
5346
- } catch {
5347
- }
5348
- }
5349
- }
5350
- };
5351
- function serializeConfigOverrides(configOverrides) {
5352
- const overrides = [];
5353
- flattenConfigOverrides(configOverrides, "", overrides);
5354
- return overrides;
5355
- }
5356
- function flattenConfigOverrides(value, prefix, overrides) {
5357
- if (!isPlainObject(value)) {
5358
- if (prefix) {
5359
- overrides.push(`${prefix}=${toTomlValue(value, prefix)}`);
5360
- return;
5361
- } else {
5362
- throw new Error("Codex config overrides must be a plain object");
5363
- }
5364
- }
5365
- const entries = Object.entries(value);
5366
- if (!prefix && entries.length === 0) {
5367
- return;
5368
- }
5369
- if (prefix && entries.length === 0) {
5370
- overrides.push(`${prefix}={}`);
5371
- return;
5372
- }
5373
- for (const [key, child] of entries) {
5374
- if (!key) {
5375
- throw new Error("Codex config override keys must be non-empty strings");
5376
- }
5377
- if (child === void 0) {
5378
- continue;
5379
- }
5380
- const path3 = prefix ? `${prefix}.${key}` : key;
5381
- if (isPlainObject(child)) {
5382
- flattenConfigOverrides(child, path3, overrides);
5383
- } else {
5384
- overrides.push(`${path3}=${toTomlValue(child, path3)}`);
5385
- }
5386
- }
5387
- }
5388
- function toTomlValue(value, path3) {
5389
- if (typeof value === "string") {
5390
- return JSON.stringify(value);
5391
- } else if (typeof value === "number") {
5392
- if (!Number.isFinite(value)) {
5393
- throw new Error(`Codex config override at ${path3} must be a finite number`);
5394
- }
5395
- return `${value}`;
5396
- } else if (typeof value === "boolean") {
5397
- return value ? "true" : "false";
5398
- } else if (Array.isArray(value)) {
5399
- const rendered = value.map((item, index) => toTomlValue(item, `${path3}[${index}]`));
5400
- return `[${rendered.join(", ")}]`;
5401
- } else if (isPlainObject(value)) {
5402
- const parts = [];
5403
- for (const [key, child] of Object.entries(value)) {
5404
- if (!key) {
5405
- throw new Error("Codex config override keys must be non-empty strings");
5406
- }
5407
- if (child === void 0) {
5408
- continue;
5409
- }
5410
- parts.push(`${formatTomlKey(key)} = ${toTomlValue(child, `${path3}.${key}`)}`);
5411
- }
5412
- return `{${parts.join(", ")}}`;
5413
- } else if (value === null) {
5414
- throw new Error(`Codex config override at ${path3} cannot be null`);
5415
- } else {
5416
- const typeName = typeof value;
5417
- throw new Error(`Unsupported Codex config override value at ${path3}: ${typeName}`);
5418
- }
5419
- }
5420
- var TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
5421
- function formatTomlKey(key) {
5422
- return TOML_BARE_KEY.test(key) ? key : JSON.stringify(key);
5423
- }
5424
- function isPlainObject(value) {
5425
- return typeof value === "object" && value !== null && !Array.isArray(value);
5426
- }
5427
- function findCodexPath() {
5428
- const { platform: platform2, arch } = process;
5429
- let targetTriple = null;
5430
- switch (platform2) {
5431
- case "linux":
5432
- case "android":
5433
- switch (arch) {
5434
- case "x64":
5435
- targetTriple = "x86_64-unknown-linux-musl";
5436
- break;
5437
- case "arm64":
5438
- targetTriple = "aarch64-unknown-linux-musl";
5439
- break;
5440
- default:
5441
- break;
5442
- }
5443
- break;
5444
- case "darwin":
5445
- switch (arch) {
5446
- case "x64":
5447
- targetTriple = "x86_64-apple-darwin";
5448
- break;
5449
- case "arm64":
5450
- targetTriple = "aarch64-apple-darwin";
5451
- break;
5452
- default:
5453
- break;
5454
- }
5455
- break;
5456
- case "win32":
5457
- switch (arch) {
5458
- case "x64":
5459
- targetTriple = "x86_64-pc-windows-msvc";
5460
- break;
5461
- case "arm64":
5462
- targetTriple = "aarch64-pc-windows-msvc";
5463
- break;
5464
- default:
5465
- break;
5466
- }
5467
- break;
5468
- default:
5469
- break;
5470
- }
5471
- if (!targetTriple) {
5472
- throw new Error(`Unsupported platform: ${platform2} (${arch})`);
5473
- }
5474
- const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple];
5475
- if (!platformPackage) {
5476
- throw new Error(`Unsupported target triple: ${targetTriple}`);
5477
- }
5478
- let vendorRoot;
5479
- try {
5480
- const codexPackageJsonPath = moduleRequire.resolve(`${CODEX_NPM_NAME}/package.json`);
5481
- const codexRequire = createRequire(codexPackageJsonPath);
5482
- const platformPackageJsonPath = codexRequire.resolve(`${platformPackage}/package.json`);
5483
- vendorRoot = path2.join(path2.dirname(platformPackageJsonPath), "vendor");
5484
- } catch {
5485
- throw new Error(
5486
- `Unable to locate Codex CLI binaries. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
5487
- );
5488
- }
5489
- const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
5490
- const nativePackage = resolveNativePackage(vendorRoot, targetTriple, codexBinaryName);
5491
- if (!nativePackage) {
5492
- throw new Error(
5493
- `Unable to locate Codex CLI binaries for ${targetTriple}. Ensure ${CODEX_NPM_NAME} is installed with optional dependencies.`
5494
- );
5495
- }
5496
- return nativePackage;
5497
- }
5498
- function resolveNativePackage(vendorRoot, targetTriple, codexBinaryName) {
5499
- const packageRoot = path2.join(vendorRoot, targetTriple);
5500
- const packageBinaryPath = path2.join(packageRoot, "bin", codexBinaryName);
5501
- if (isFile(packageBinaryPath) && isFile(path2.join(packageRoot, "codex-package.json"))) {
5502
- return {
5503
- executablePath: packageBinaryPath,
5504
- pathDirs: existingDirs(path2.join(packageRoot, "codex-path"))
5505
- };
5506
- }
5507
- const legacyBinaryPath = path2.join(packageRoot, "codex", codexBinaryName);
5508
- if (isFile(legacyBinaryPath)) {
5509
- return {
5510
- executablePath: legacyBinaryPath,
5511
- pathDirs: existingDirs(path2.join(packageRoot, "path"))
5512
- };
5513
- }
5514
- return null;
5515
- }
5516
- function existingDirs(...dirs) {
5517
- return dirs.filter(isDirectory);
5518
- }
5519
- function prependPathDirs(env, pathDirs, platform2 = process.platform) {
5520
- const pathKey = pathEnvKey(env, platform2);
5521
- if (platform2 === "win32") {
5522
- for (const key of Object.keys(env)) {
5523
- if (key.toLowerCase() === "path" && key !== pathKey) {
5524
- delete env[key];
5525
- }
5526
- }
5527
- }
5528
- const existingEntries = (env[pathKey] ?? "").split(path2.delimiter).filter((entry) => entry.length > 0 && !pathDirs.includes(entry));
5529
- env[pathKey] = [...pathDirs, ...existingEntries].join(path2.delimiter);
5530
- }
5531
- function pathEnvKey(env, platform2) {
5532
- if (platform2 !== "win32") {
5533
- return "PATH";
5534
- }
5535
- const matchingKeys = Object.keys(env).filter((key) => key.toLowerCase() === "path");
5536
- return matchingKeys.includes("Path") ? "Path" : matchingKeys.at(-1) ?? "PATH";
5537
- }
5538
- function isFile(filePath) {
5539
- try {
5540
- return statSync(filePath).isFile();
5541
- } catch {
5542
- return false;
5543
- }
5544
- }
5545
- function isDirectory(filePath) {
5546
- try {
5547
- return statSync(filePath).isDirectory();
5548
- } catch {
5549
- return false;
5550
- }
5551
- }
5552
- var Codex = class {
5553
- exec;
5554
- options;
5555
- constructor(options = {}) {
5556
- const { codexPathOverride, env, config } = options;
5557
- this.exec = new CodexExec(codexPathOverride, env, config);
5558
- this.options = options;
5559
- }
5560
- /**
5561
- * Starts a new conversation with an agent.
5562
- * @returns A new thread instance.
5563
- */
5564
- startThread(options = {}) {
5565
- return new Thread(this.exec, this.options, options);
5566
- }
5567
- /**
5568
- * Resumes a conversation with an agent based on the thread id.
5569
- * Threads are persisted in ~/.codex/sessions.
5570
- *
5571
- * @param id The id of the thread to resume.
5572
- * @returns A new thread instance.
5573
- */
5574
- resumeThread(id, options = {}) {
5575
- return new Thread(this.exec, this.options, options, id);
5576
- }
5577
- };
5578
-
5579
5075
  // packages/agent-runtime/src/codex/transport.ts
5076
+ import {
5077
+ Codex
5078
+ } from "@openai/codex-sdk";
5580
5079
  function buildSdkThreadOptions(spec) {
5581
5080
  return {
5582
5081
  ...spec.model ? { model: spec.model } : {},
@@ -5630,6 +5129,7 @@ function createCodexAdapter(deps = {}) {
5630
5129
  const directory = req.local.cwd ?? "";
5631
5130
  const decision = decideResume3(req.session, directory);
5632
5131
  const resumeThreadId = "resume" in decision ? decision.resume : null;
5132
+ const threadPromptFingerprint = "resume" in decision ? decision.promptFingerprint : null;
5633
5133
  if ("fresh" in decision && decision.reason === "cwd_mismatch") {
5634
5134
  deps.onWarn?.(
5635
5135
  "codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread (SJ527-analogue guard)",
@@ -5655,7 +5155,12 @@ function createCodexAdapter(deps = {}) {
5655
5155
  }
5656
5156
  }
5657
5157
  try {
5658
- const spec = buildRunSpec2(req, resumeThreadId, instructions?.path ?? null);
5158
+ const spec = buildRunSpec2(
5159
+ req,
5160
+ resumeThreadId,
5161
+ instructions?.path ?? null,
5162
+ threadPromptFingerprint
5163
+ );
5659
5164
  const result = await transport.run(spec, signal);
5660
5165
  if (signal.aborted) return;
5661
5166
  yield* decodeCodexStream(result.events, {
@@ -5663,6 +5168,14 @@ function createCodexAdapter(deps = {}) {
5663
5168
  cwd: req.local.cwd,
5664
5169
  resumedThreadId: resumeThreadId,
5665
5170
  degraded,
5171
+ // CT1075: which prompt the thread carries once this turn lands. If we sent
5172
+ // one, it's that one — a later copy is what the model reads, so a changed
5173
+ // prompt supersedes the older copy still sitting above it. If we withheld
5174
+ // it, the thread still carries whatever it did before. Read off the spec
5175
+ // rather than recomputed, so the recorded fact can't disagree with what
5176
+ // was actually sent.
5177
+ promptFingerprint: spec.promptRidesInput ? spec.promptFingerprint : threadPromptFingerprint,
5178
+ storedState: req.session,
5666
5179
  // CT601: Codex reports no model in-stream, so record what the thread was
5667
5180
  // started with (null on the default token). Same for the reasoning effort.
5668
5181
  resolvedModel: spec.model,
@@ -5697,9 +5210,10 @@ var COMPANION_POLICY2 = {
5697
5210
  uiPrompts: "never"
5698
5211
  };
5699
5212
  var DIR2 = "/env/here";
5213
+ var PROMPT = "system";
5700
5214
  function makeRequest3(overrides = {}) {
5701
5215
  return {
5702
- systemPrompt: "system",
5216
+ systemPrompt: PROMPT,
5703
5217
  prompt: "hi there",
5704
5218
  content: [{ type: "text", text: "hi there" }],
5705
5219
  // CT601: the resolved model + reasoning effort ride the terminal `result` off
@@ -5742,9 +5256,9 @@ var errorItem = (id, message) => ({
5742
5256
  type: "item.completed",
5743
5257
  item: { id, type: "error", message }
5744
5258
  });
5745
- var sessionEvent3 = (threadId, cwd = DIR2, degraded = false) => ({
5259
+ var sessionEvent3 = (threadId, cwd = DIR2, degraded = false, promptFingerprint = fingerprintPrompt(PROMPT)) => ({
5746
5260
  type: "session",
5747
- state: encodeSession3({ threadId, cwd }),
5261
+ state: encodeSession3({ threadId, cwd, promptFingerprint }),
5748
5262
  ...degraded ? { degraded: true } : {}
5749
5263
  });
5750
5264
  var CODEX_CONFORMANCE_FIXTURES = [
@@ -6202,23 +5716,91 @@ var CODEX_CONFORMANCE_FIXTURES = [
6202
5716
  {
6203
5717
  // Session resume: the stored state matches the current directory, so the adapter
6204
5718
  // resumes its thread id and `thread.started` echoes the SAME id — NO session
6205
- // event fires (the id didn't change). This is what carries cross-turn memory.
5719
+ // event fires (the id didn't change AND the state is unchanged). This is what
5720
+ // carries cross-turn memory.
5721
+ //
5722
+ // CT1075: the stored state also says the thread already holds the composed
5723
+ // prompt, so this turn withholds the preamble and the state it would write is
5724
+ // byte-identical to the stored one. The silence is now proof of BOTH: an id that
5725
+ // didn't change and a prompt that didn't need re-sending.
6206
5726
  name: "session resume (no re-emit on echo)",
6207
- request: makeRequest3({ session: encodeSession3({ threadId: "th1", cwd: DIR2 }) }),
5727
+ request: makeRequest3({
5728
+ session: encodeSession3({
5729
+ threadId: "th1",
5730
+ cwd: DIR2,
5731
+ promptFingerprint: fingerprintPrompt(PROMPT)
5732
+ })
5733
+ }),
6208
5734
  nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
6209
5735
  expected: [
6210
5736
  { type: "text", body: "Back again.", terminal: true },
6211
5737
  { type: "result", ok: true }
6212
5738
  ]
6213
5739
  },
5740
+ {
5741
+ // CT1075: a resumed thread the platform's state does NOT vouch for — a state
5742
+ // written before CT1075 existed, so it carries no `promptFingerprint` field. The
5743
+ // adapter reads the absence as "we can't say which prompt this thread holds",
5744
+ // sends the full prompt exactly as it did pre-CT1075, and re-emits the state with
5745
+ // the fingerprint recorded — on the SAME thread id, which pre-CT1075 emitted
5746
+ // nothing at all. That re-emit is the upgrade path: without it a long-lived
5747
+ // thread would re-send the whole prompt forever.
5748
+ //
5749
+ // This is the fixture that fails if the static half is withheld on a thread we
5750
+ // have no evidence about.
5751
+ name: "session resume with a pre-CT1075 state re-emits with the prompt fingerprint",
5752
+ request: makeRequest3({
5753
+ session: JSON.stringify({ threadId: "th1", cwd: DIR2 })
5754
+ }),
5755
+ nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
5756
+ expected: [
5757
+ sessionEvent3("th1"),
5758
+ { type: "text", body: "Back again.", terminal: true },
5759
+ { type: "result", ok: true }
5760
+ ]
5761
+ },
5762
+ {
5763
+ // CT1075 (review finding): the thread carries a DIFFERENT prompt than the one
5764
+ // this turn composed — a charter edit, a renamed agent, a redeployed prompt
5765
+ // body. The stored fingerprint doesn't match, so the new prompt is sent exactly
5766
+ // as on a fresh thread and the state is re-recorded against it.
5767
+ //
5768
+ // This is the fixture that fails if a stale prompt is treated as good enough. A
5769
+ // boolean flag would have passed the resume fixture above and silently withheld
5770
+ // every future charter edit from this thread.
5771
+ name: "session resume with a stale prompt fingerprint re-sends the prompt",
5772
+ request: makeRequest3({
5773
+ session: encodeSession3({
5774
+ threadId: "th1",
5775
+ cwd: DIR2,
5776
+ promptFingerprint: fingerprintPrompt("an older composed prompt")
5777
+ })
5778
+ }),
5779
+ nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
5780
+ expected: [
5781
+ sessionEvent3("th1"),
5782
+ { type: "text", body: "Back again.", terminal: true },
5783
+ { type: "result", ok: true }
5784
+ ]
5785
+ },
6214
5786
  {
6215
5787
  // Directory-mismatch degrade (the SJ527 analogue): the stored state was created
6216
5788
  // under a DIFFERENT directory, so the adapter refuses to resume and starts
6217
5789
  // fresh — `thread.started` reports a NEW id, and the emitted session event
6218
5790
  // records the CURRENT directory with `degraded: true` so the host rewinds the
6219
5791
  // catch-up mark.
5792
+ //
5793
+ // CT1075: the refused resume means the new thread is EMPTY, so the full prompt
5794
+ // goes in even though the stored state vouched for a copy — of a thread we're
5795
+ // no longer in. The emitted state records the fresh thread's own answer.
6220
5796
  name: "session directory-mismatch degrades to fresh",
6221
- request: makeRequest3({ session: encodeSession3({ threadId: "old", cwd: "/env/gone" }) }),
5797
+ request: makeRequest3({
5798
+ session: encodeSession3({
5799
+ threadId: "old",
5800
+ cwd: "/env/gone",
5801
+ promptFingerprint: fingerprintPrompt(PROMPT)
5802
+ })
5803
+ }),
6222
5804
  nativeStream: [
6223
5805
  threadStarted(NEW_THREAD_ID),
6224
5806
  agentMessage("m1", "Fresh start."),
@@ -6285,7 +5867,7 @@ var ConnectorHealthStore = class {
6285
5867
 
6286
5868
  // src/dispatcher.ts
6287
5869
  import { randomUUID } from "crypto";
6288
- import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10, statSync as statSync2 } from "fs";
5870
+ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10, statSync } from "fs";
6289
5871
  import { join as join13 } from "path";
6290
5872
 
6291
5873
  // src/summon.ts
@@ -6584,9 +6166,16 @@ function buildCompanionTurnRequest(params) {
6584
6166
  ...t.workspaceToolSurface ? { workspaceToolSurface: t.workspaceToolSurface } : {},
6585
6167
  // CT714: mount the turn-control surface ONLY when a real turn token backs
6586
6168
  // this turn — the surface admits `turn_token` auth exclusively, so a
6587
- // PAT-fallback bearer (older API / unresolved delegation) would be rejected
6588
- // there. Absent it, external adapters simply don't mount it that turn (the
6589
- // same graceful degrade as the rest of the OBO path).
6169
+ // PAT-fallback bearer would be rejected there. Absent it, external adapters
6170
+ // simply don't mount it that turn.
6171
+ //
6172
+ // CT1074: that fallback is now an OLD-API case only. The server mints a token
6173
+ // for every turn it dispatched, so "no token" no longer means "no human behind
6174
+ // the turn" — it means the API predates this change. The condition is right as
6175
+ // it stands and stays where the failure is honest (a rejected bearer is worse
6176
+ // than an unmounted server); what was wrong was upstream, where an undelegated
6177
+ // turn minted nothing and this silently dropped all five verbs on Codex and
6178
+ // opencode with no refusal anyone could see.
6590
6179
  ...params.turnToken ? { turnControlUrl: turnControlMcpUrl(params.baseUrl) } : {}
6591
6180
  },
6592
6181
  local: {
@@ -6616,10 +6205,10 @@ import { join as join9 } from "path";
6616
6205
  var PREFIX = "cabane-codex-instructions-";
6617
6206
  async function writeCodexInstructionsFile(contents) {
6618
6207
  const dir2 = await mkdtemp(join9(tmpdir(), PREFIX));
6619
- const path3 = join9(dir2, "instructions.md");
6620
- await writeFile(path3, contents, { encoding: "utf8", mode: 384 });
6208
+ const path = join9(dir2, "instructions.md");
6209
+ await writeFile(path, contents, { encoding: "utf8", mode: 384 });
6621
6210
  return {
6622
- path: path3,
6211
+ path,
6623
6212
  cleanup: async () => {
6624
6213
  await rm(dir2, { recursive: true, force: true });
6625
6214
  }
@@ -6639,10 +6228,10 @@ function pathFor3(workspaceId, conversationId, agentId) {
6639
6228
  return join10(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6640
6229
  }
6641
6230
  function readPrepared(workspaceId, conversationId, agentId) {
6642
- const path3 = pathFor3(workspaceId, conversationId, agentId);
6643
- if (!existsSync7(path3)) return null;
6231
+ const path = pathFor3(workspaceId, conversationId, agentId);
6232
+ if (!existsSync7(path)) return null;
6644
6233
  try {
6645
- const parsed = JSON.parse(readFileSync6(path3, "utf8"));
6234
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
6646
6235
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
6647
6236
  return {
6648
6237
  cwd: parsed.cwd,
@@ -6676,14 +6265,14 @@ function secretsPath() {
6676
6265
  }
6677
6266
  var secretStoreSchema = z13.record(z13.string(), z13.string());
6678
6267
  function loadSecretStore() {
6679
- const path3 = secretsPath();
6680
- if (!existsSync8(path3)) return makeStore({});
6268
+ const path = secretsPath();
6269
+ if (!existsSync8(path)) return makeStore({});
6681
6270
  let raw;
6682
6271
  try {
6683
- raw = readFileSync7(path3, "utf8");
6272
+ raw = readFileSync7(path, "utf8");
6684
6273
  } catch (err) {
6685
6274
  throw new ConfigError(
6686
- `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
6275
+ `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
6687
6276
  );
6688
6277
  }
6689
6278
  if (raw.trim().length === 0) return makeStore({});
@@ -6692,13 +6281,13 @@ function loadSecretStore() {
6692
6281
  parsed = JSON.parse(raw);
6693
6282
  } catch (err) {
6694
6283
  throw new ConfigError(
6695
- `${path3} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
6284
+ `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
6696
6285
  );
6697
6286
  }
6698
6287
  const result = secretStoreSchema.safeParse(parsed);
6699
6288
  if (!result.success) {
6700
6289
  throw new ConfigError(
6701
- `${path3} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
6290
+ `${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
6702
6291
  );
6703
6292
  }
6704
6293
  return makeStore(result.data);
@@ -7134,7 +6723,7 @@ function checkoutState(cwd) {
7134
6723
  if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
7135
6724
  let stat;
7136
6725
  try {
7137
- stat = statSync2(gitPath);
6726
+ stat = statSync(gitPath);
7138
6727
  } catch (error) {
7139
6728
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
7140
6729
  }
@@ -8565,8 +8154,8 @@ function sleep3(ms) {
8565
8154
  }
8566
8155
 
8567
8156
  // src/version.ts
8568
- import { createRequire as createRequire2 } from "module";
8569
- var pkg = createRequire2(import.meta.url)("../package.json");
8157
+ import { createRequire } from "module";
8158
+ var pkg = createRequire(import.meta.url)("../package.json");
8570
8159
  var COMPANION_VERSION = pkg.version;
8571
8160
 
8572
8161
  // src/supervisor.ts
@@ -9370,9 +8959,9 @@ var CompanionSupervisor = class {
9370
8959
  };
9371
8960
  function defaultReexec() {
9372
8961
  clearRuntimeState();
9373
- void import("child_process").then(({ spawn: spawn7 }) => {
8962
+ void import("child_process").then(({ spawn: spawn6 }) => {
9374
8963
  try {
9375
- const child = spawn7(process.execPath, process.argv.slice(1), {
8964
+ const child = spawn6(process.execPath, process.argv.slice(1), {
9376
8965
  stdio: "inherit",
9377
8966
  detached: false
9378
8967
  });
@@ -9456,8 +9045,8 @@ function recordCrash(rec2) {
9456
9045
  }
9457
9046
  function clearCrash() {
9458
9047
  try {
9459
- const path3 = crashMarkerPath();
9460
- if (existsSync11(path3)) rmSync7(path3, { force: true });
9048
+ const path = crashMarkerPath();
9049
+ if (existsSync11(path)) rmSync7(path, { force: true });
9461
9050
  } catch {
9462
9051
  }
9463
9052
  }
@@ -9956,11 +9545,11 @@ function printList(dir2) {
9956
9545
  "render one: cabane-companion transcript <file> (or `--last` for the newest)\n"
9957
9546
  );
9958
9547
  }
9959
- function peek(path3) {
9548
+ function peek(path) {
9960
9549
  let meta;
9961
9550
  let outcome;
9962
9551
  try {
9963
- for (const line of readFileSync10(path3, "utf8").split("\n")) {
9552
+ for (const line of readFileSync10(path, "utf8").split("\n")) {
9964
9553
  if (!line.trim()) continue;
9965
9554
  const o = safeParse(line);
9966
9555
  const t = str2(rec(o)?.type);
@@ -9990,13 +9579,13 @@ function resolveTarget(dir2, target) {
9990
9579
  ` + matches.slice(0, 10).map((m) => ` ${m}`).join("\n")
9991
9580
  );
9992
9581
  }
9993
- function renderFile(path3) {
9582
+ function renderFile(path) {
9994
9583
  let content;
9995
9584
  try {
9996
- content = readFileSync10(path3, "utf8");
9585
+ content = readFileSync10(path, "utf8");
9997
9586
  } catch (err) {
9998
9587
  throw new CompanionError(
9999
- `couldn't read ${path3}: ${err instanceof Error ? err.message : String(err)}`
9588
+ `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
10000
9589
  );
10001
9590
  }
10002
9591
  return renderTranscript(content.split("\n"));