@cabane/companion 0.6.169 → 0.6.171

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 +368 -161
  2. package/dist/runtime.js +339 -147
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2084,9 +2084,15 @@ var init_turn_request = __esm({
2084
2084
  resolvedMcpServersSchema = z9.record(z9.string(), resolvedMcpServerSchema);
2085
2085
  hostInjectedServersSchema = z9.record(z9.string(), z9.unknown());
2086
2086
  turnRequestSchema = z9.object({
2087
- // Server-composed system prompt (core + capability prose + adapter addendum +
2088
- // charter). One string to the adapter.
2087
+ // Server-composed standing instructions: the core segment (body, tool naming, identity, agent
2088
+ // charter) and, after it, the conversation segment (workspace and room charters, skill indexes,
2089
+ // roster). One string to the adapter, current as of this dispatch.
2089
2090
  systemPrompt: z9.string(),
2091
+ // The same text as its two ordered segments, so an adapter that writes standing text into the
2092
+ // conversation (Codex's input preamble) can tell which half changed. `systemPrompt` is
2093
+ // `joinStanding(standing)`. Optional: absent from an API older than the split, where
2094
+ // `systemPrompt` is the core alone and the conversation material rides `prompt`.
2095
+ standing: z9.object({ core: z9.string(), conversation: z9.string() }).optional(),
2090
2096
  // Server-composed per-turn user text (anchor reminder + the triggering message).
2091
2097
  prompt: z9.string(),
2092
2098
  // The multi-block user-message body (text + vision).
@@ -2136,6 +2142,22 @@ var init_turn_request = __esm({
2136
2142
  }
2137
2143
  });
2138
2144
 
2145
+ // packages/agent-runtime/src/standing.ts
2146
+ function joinStanding(segments) {
2147
+ const conversation = segments.conversation.trim();
2148
+ return conversation.length > 0 ? `${segments.core}${STANDING_SEPARATOR}${conversation}` : segments.core;
2149
+ }
2150
+ function standingSegments(req) {
2151
+ return req.standing ?? { core: req.systemPrompt, conversation: "" };
2152
+ }
2153
+ var STANDING_SEPARATOR;
2154
+ var init_standing = __esm({
2155
+ "packages/agent-runtime/src/standing.ts"() {
2156
+ "use strict";
2157
+ STANDING_SEPARATOR = "\n\n---\n\n";
2158
+ }
2159
+ });
2160
+
2139
2161
  // packages/agent-runtime/src/conformance.ts
2140
2162
  import { z as z10 } from "zod";
2141
2163
  var conformanceFixtureSchema;
@@ -2520,8 +2542,15 @@ var init_descriptions = __esm({
2520
2542
  "withdraw_ask"
2521
2543
  ];
2522
2544
  TURN_CONTROL_TOOL_DESCRIPTIONS = {
2523
- ask: `Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you can't proceed without a person's input (a decision only they can make, a missing fact). Pass \`targetUserId\`. Two forms: a SINGLE question \u2014 a \`headline\` plus a short \`question\` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a \`questions\` array of 1\u2013${MAX_ASK_ITEMS} items, each \`{ headline, body?, options? }\`. Provide EITHER \`question\` (single) or \`questions\` (array), never both. **Several decisions landing together are ONE call with \`questions\`** \u2014 never cram the extras into prose, drop all but one, or fire an ask each: a second \`ask\` is a second inbox item competing with the first, never an edit to it. Each question keeps the same form: a one-sentence \`headline\`, a short \`body\` frame (NOT a report \u2014 your status, links and detail go in your REPLY; inline markdown only, no lists or headings), and ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} \`options\` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. Your final reply carries the CONTEXT, the ask carries the QUESTION(S). Your question is RECORDED when you call this and delivered when your turn ends \u2014 beside the reply that frames it, or earlier on a long turn, so they can answer while you work. Ask as soon as you know it, and write it to stand alone: in every headline, body and option, say "I" for yourself and name anyone else who acts \u2014 never "you", "we" or "they". The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. A person can DISMISS a question instead of answering it \u2014 it ends, you are not told, nothing wakes you. Check an ask's status before re-raising a vanished one. \`replaces\` withdraws a stale ask of yours (or anyone's) in this conversation in the same write, with the reason shown to the person. Targets a human only; to bring a peer AGENT in, name them by \`@handle\` in your reply.`,
2524
- cancel_wake: `Stand down your own wake \u2014 the inverse of \`wake_me\`. No arguments: it cancels the wake you have armed in this conversation, NOW, when you call it. Reach for it when the thing you armed a wake to check has already happened, or the work it was watching is over \u2014 an armed wake you no longer need fires into a turn with nothing to do, and a supervision loop with no off switch is one you can only end by leaving it running. Whether you have one armed is printed in your turn context ("Wake armed: \u2026"); calling this with nothing armed is a clean no-op, no error. Each call acts as it arrives: \`cancel_wake\` then \`wake_me\` leaves the NEW wake armed; \`wake_me\` then \`cancel_wake\` leaves nothing armed. It reaches only your own wake in this conversation \u2014 never a peer's, and never a recurring schedule (those are \`schedules.*\`).`,
2545
+ ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster; to bring in an agent, name it with \`@handle\` in your reply.
2546
+ - Form: one question (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} \`{ headline, body?, options? }\`; never both. Decisions that arise together go in ONE call, in the order you want them answered. A second \`ask\` is a separate item, never an edit; raise one only for a genuinely new question.
2547
+ - \`headline\`: one sentence ending in \`?\`. Body (\`question\` or each item's \`body\`): a sentence or two of framing, not a report; inline Markdown only, no lists or headings. \`options\`: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers (always for a yes/no go-ahead), each a distinct course of action you can carry out without asking back.
2548
+ - Voice, in headline, body and options: "I" for yourself, never your own name; name everyone else who decides or acts, as on your roster. Never "you", "your", "we", "us", "our" or their contractions, nor "he", "she" or "they" for a named actor. Each option reads correctly alone. A quotation stays accurate: say who said it.
2549
+ - It must stand alone, since it can arrive first: the reply carries the findings, the ask the question; don't repeat the reply in it.
2550
+ - Recorded when you call this; delivered when your turn ends, or after 3 minutes if it is still running. Ask as soon as you know. The answer reaches you on a later turn.
2551
+ - The person sends one response; unanswered questions end as Unanswered. A person can dismiss an ask; you aren't told and nothing wakes you. Check an ask's status before raising it again.
2552
+ - To replace a stale ask (yours or anyone's here), pass \`replaces: { askId, reason }\`: both land in one write and the person sees the reason. To withdraw without a replacement, use \`withdraw_ask\`.`,
2553
+ cancel_wake: `Stand down your own wake \u2014 the inverse of \`wake_me\`. No arguments: it cancels the wake you have armed in this conversation, NOW, when you call it. Reach for it when the thing you armed a wake to check has already happened, or the work it was watching is over \u2014 an armed wake you no longer need fires into a turn with nothing to do, and a supervision loop with no off switch is one you can only end by leaving it running. Whether you have one armed is printed in your turn context ("Wake armed: \u2026"); a wake you armed that is no longer printed there has fired or been dismissed. Calling this with nothing armed is a clean no-op, no error. Each call acts as it arrives: \`cancel_wake\` then \`wake_me\` leaves the NEW wake armed; \`wake_me\` then \`cancel_wake\` leaves nothing armed. It reaches only your own wake in this conversation \u2014 never a peer's, and never a recurring schedule (those are \`schedules.*\`).`,
2525
2554
  skip_turn: `End your current turn WITHOUT posting a reply. Call this when you've been woken to read a message that genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or something another participant clearly owns. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
2526
2555
  wake_me: `Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for "wait until X": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an \`ask\`. Pass EXACTLY ONE of \`afterSeconds\` (a relative delay \u2014 \`300\` for five minutes) or \`at\` (an absolute ISO-8601 timestamp WITH a zone, e.g. \`2026-07-16T09:00:00-07:00\` \u2014 YOU compute it; the system never parses natural-language time). \`note\` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check ("check whether CT441 merged yet"). The wake arms NOW, when you call this, and the delay counts from this call; a later \`wake_me\` in the same turn replaces it, and \`cancel_wake\` stands it down. A wake that comes due while your turn is still running waits and is delivered after your reply. This is the ONLY sanctioned way to schedule your own continuation; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days \u2014 a wake that can't be armed returns an error here, in this call, so you can recompute; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an \`ask\` to the human instead.`,
2527
2556
  withdraw_ask: `Withdraw an open ask \u2014 yours or anyone's in this conversation \u2014 because the question is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't your ask, to the agent who asked it. If there is a current question to put in its place, don't call this \u2014 call \`ask\` with \`replaces\` so the two land as one and the person never sees neither or both. Never withdraw to hurry an answer; to act on a deadline, arm a \`wake_me\` and decide then.`
@@ -2529,13 +2558,13 @@ var init_descriptions = __esm({
2529
2558
  TURN_CONTROL_FIELD_DESCRIPTIONS = {
2530
2559
  ask: {
2531
2560
  targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
2532
- question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
2533
- headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
2534
- options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must stand alone: "I" for yourself, anyone else responsible for its action by name \u2014 never "you", "we" or "they".`,
2561
+ question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
2562
+ headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the person reads first in For you and on the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged.`,
2563
+ options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
2535
2564
  questions: `MULTI-question form: 1\u2013${MAX_ASK_ITEMS} questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`,
2536
- "questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
2537
- "questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters. NOT a report. Say "I" for yourself; name anyone else described, never "you", "we" or "they".`,
2538
- "questions.options": `Optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must stand alone: "I" for yourself, anyone else responsible for its action by name \u2014 never "you", "we" or "they".`,
2565
+ "questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
2566
+ "questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
2567
+ "questions.options": `Optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
2539
2568
  replaces: "Withdraw a stale ask in this conversation as part of raising this one. Both land in a single write, so the person goes from the old question to the new one and never sees neither or both.",
2540
2569
  "replaces.askId": "The id of the open ask to withdraw.",
2541
2570
  "replaces.reason": `Why the old question is no longer the question \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
@@ -5374,7 +5403,14 @@ function fingerprintPrompt(prompt) {
5374
5403
  return `${prompt.length}.${(fnv >>> 0).toString(36)}.${(djb >>> 0).toString(36)}`;
5375
5404
  }
5376
5405
  function encodeSession3(state) {
5377
- return JSON.stringify(state);
5406
+ return JSON.stringify({
5407
+ threadId: state.threadId,
5408
+ cwd: state.cwd,
5409
+ promptFingerprint: state.promptFingerprint,
5410
+ conversationFingerprint: state.conversationFingerprint ?? null,
5411
+ compactions: state.compactions ?? null,
5412
+ usageBaseline: state.usageBaseline
5413
+ });
5378
5414
  }
5379
5415
  function decideResume3(stored, currentCwd) {
5380
5416
  if (!stored) return { fresh: true, reason: "no_session" };
@@ -5395,6 +5431,8 @@ function decideResume3(stored, currentCwd) {
5395
5431
  return {
5396
5432
  resume: state.threadId,
5397
5433
  promptFingerprint: typeof state.promptFingerprint === "string" ? state.promptFingerprint : null,
5434
+ conversationFingerprint: typeof state.conversationFingerprint === "string" ? state.conversationFingerprint : null,
5435
+ compactions: typeof state.compactions === "number" && Number.isInteger(state.compactions) && state.compactions >= 0 ? state.compactions : null,
5398
5436
  usageBaseline: readUsageBaseline(state.usageBaseline)
5399
5437
  };
5400
5438
  }
@@ -5432,6 +5470,7 @@ async function* decodeCodexStream(events, ctx2) {
5432
5470
  let usage;
5433
5471
  let cumulativeUsage;
5434
5472
  let threadIdForSession = ctx2.resumedThreadId;
5473
+ let lastState = ctx2.storedState ?? null;
5435
5474
  let settled = false;
5436
5475
  function* flushInterim() {
5437
5476
  const sealed = sealHeld2(held, false);
@@ -5447,14 +5486,14 @@ async function* decodeCodexStream(events, ctx2) {
5447
5486
  const state = encodeSession3({
5448
5487
  threadId,
5449
5488
  cwd: ctx2.cwd ?? "",
5450
- promptFingerprint: ctx2.promptFingerprint ?? null,
5451
- // The baseline this turn inherited, carried through unchanged. This early write records the
5452
- // thread id even if the turn dies mid-stream; it can't yet know what the turn will spend. The
5453
- // closing write records the new baseline.
5489
+ promptFingerprint: ctx2.heldStanding?.promptFingerprint ?? null,
5490
+ conversationFingerprint: ctx2.heldStanding?.conversationFingerprint ?? null,
5491
+ compactions: ctx2.heldStanding?.compactions ?? null,
5454
5492
  usageBaseline: ctx2.usageBaseline ?? null
5455
5493
  });
5456
5494
  if (threadId !== ctx2.resumedThreadId || state !== ctx2.storedState) {
5457
5495
  sessionEmitted = true;
5496
+ lastState = state;
5458
5497
  yield {
5459
5498
  type: "session",
5460
5499
  state,
@@ -5553,14 +5592,16 @@ async function* decodeCodexStream(events, ctx2) {
5553
5592
  if (sealed) yield sealed;
5554
5593
  reason = "no_terminal";
5555
5594
  }
5556
- if (ok && cumulativeUsage && threadIdForSession) {
5595
+ if (ok && threadIdForSession) {
5557
5596
  const closing = encodeSession3({
5558
5597
  threadId: threadIdForSession,
5559
5598
  cwd: ctx2.cwd ?? "",
5560
5599
  promptFingerprint: ctx2.promptFingerprint ?? null,
5561
- usageBaseline: cumulativeUsage
5600
+ conversationFingerprint: ctx2.conversationFingerprint ?? null,
5601
+ compactions: ctx2.compactions ?? null,
5602
+ usageBaseline: cumulativeUsage ?? ctx2.usageBaseline ?? null
5562
5603
  });
5563
- yield { type: "session", state: closing };
5604
+ if (closing !== lastState) yield { type: "session", state: closing };
5564
5605
  }
5565
5606
  const resolvedConfig = ctx2.resolvedReasoningEffort ? { reasoningEffort: ctx2.resolvedReasoningEffort } : void 0;
5566
5607
  yield {
@@ -5640,15 +5681,27 @@ var init_model3 = __esm({
5640
5681
  });
5641
5682
 
5642
5683
  // packages/agent-runtime/src/codex/run-spec.ts
5643
- function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null, imagePaths = []) {
5684
+ function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadStanding = null, imagePaths = []) {
5644
5685
  const { policy, config } = req;
5645
5686
  const directory = req.local.cwd ?? "";
5646
5687
  const dialect = readCodexDialect(config.runtimeOptions);
5647
5688
  const baseInstructionsFile = !policy.hostFs && instructionsFile ? instructionsFile : null;
5648
5689
  const model = config.model ? parseCodexModel(config.model) : null;
5649
- const promptFingerprint = fingerprintPrompt(req.systemPrompt);
5650
- const threadHasThisPrompt = resumeThreadId !== null && threadPromptFingerprint !== null && threadPromptFingerprint === promptFingerprint;
5651
- const promptRidesInput = baseInstructionsFile === null && !threadHasThisPrompt;
5690
+ const segments = standingSegments(req);
5691
+ const conversationText = segments.conversation.trim();
5692
+ const fingerprints = {
5693
+ core: fingerprintPrompt(segments.core),
5694
+ conversation: conversationText.length > 0 ? fingerprintPrompt(conversationText) : null
5695
+ };
5696
+ const resumed = resumeThreadId !== null;
5697
+ const held = resumed ? threadStanding : null;
5698
+ const viaInput = baseInstructionsFile === null;
5699
+ const preamble = {
5700
+ core: viaInput && (held?.core ?? null) !== fingerprints.core,
5701
+ // Compared both ways: a held segment that is now empty differs from it, and nothing held
5702
+ // equals nothing to send.
5703
+ conversation: viaInput && (held?.conversation ?? null) !== fingerprints.conversation
5704
+ };
5652
5705
  return {
5653
5706
  resumeThreadId,
5654
5707
  directory,
@@ -5657,9 +5710,9 @@ function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromp
5657
5710
  skipGitRepoCheck: true,
5658
5711
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
5659
5712
  baseInstructionsFile,
5660
- input: withImages(buildInput(req, promptRidesInput), imagePaths),
5661
- promptRidesInput,
5662
- promptFingerprint,
5713
+ input: withImages(buildInput(req, segments, preamble, resumed), imagePaths),
5714
+ preamble,
5715
+ fingerprints,
5663
5716
  config: buildConfig(req, baseInstructionsFile),
5664
5717
  secretEnv: codexSecretEnv(req)
5665
5718
  };
@@ -5675,12 +5728,24 @@ function codexSecretEnv(req) {
5675
5728
  }
5676
5729
  return Object.keys(env).length > 0 ? env : null;
5677
5730
  }
5678
- function buildInput(req, promptRidesInput) {
5731
+ function buildInput(req, segments, preamble, resumed) {
5679
5732
  const userText = req.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
5680
5733
  const body = userText.trim().length > 0 ? userText : req.prompt;
5681
- if (!promptRidesInput) return body;
5682
- const system = req.systemPrompt.trim();
5683
- return system.length > 0 ? `${system}
5734
+ const standing = [];
5735
+ if (!resumed) {
5736
+ if (preamble.core) standing.push(joinStanding(segments).trim());
5737
+ } else {
5738
+ if (preamble.core) standing.push(`${CORE_REPLACES}
5739
+
5740
+ ${segments.core.trim()}`);
5741
+ if (preamble.conversation) {
5742
+ standing.push(`${CONVERSATION_REPLACES}
5743
+
5744
+ ${segments.conversation.trim()}`.trim());
5745
+ }
5746
+ }
5747
+ const lead = standing.filter((s) => s.length > 0);
5748
+ return lead.length > 0 ? `${lead.join("\n\n---\n\n")}
5684
5749
 
5685
5750
  ${body}` : body;
5686
5751
  }
@@ -5766,10 +5831,11 @@ function buildConfig(req, baseInstructionsFile) {
5766
5831
  function isStringRecord2(v) {
5767
5832
  return !!v && typeof v === "object" && Object.values(v).every((x) => typeof x === "string");
5768
5833
  }
5769
- var CABANE_MCP_SERVER3, TURN_CONTROL_MCP_SERVER2, ACTIVE_CONVERSATION_HEADER3, ENV_ENVELOPE_KEYS;
5834
+ var CABANE_MCP_SERVER3, TURN_CONTROL_MCP_SERVER2, ACTIVE_CONVERSATION_HEADER3, ENV_ENVELOPE_KEYS, CORE_REPLACES, CONVERSATION_REPLACES;
5770
5835
  var init_run_spec2 = __esm({
5771
5836
  "packages/agent-runtime/src/codex/run-spec.ts"() {
5772
5837
  "use strict";
5838
+ init_standing();
5773
5839
  init_policy3();
5774
5840
  init_model3();
5775
5841
  init_session3();
@@ -5784,6 +5850,8 @@ var init_run_spec2 = __esm({
5784
5850
  "CABANE_CONVERSATION_TITLE",
5785
5851
  "CABANE_HOST_ACCESS"
5786
5852
  ];
5853
+ CORE_REPLACES = "Your standing instructions changed since an earlier copy in this conversation, or that copy may have been compacted away. This copy replaces every earlier one: where they differ, this one holds, and anything an earlier copy said that this one does not no longer applies.";
5854
+ CONVERSATION_REPLACES = "This conversation's standing context \u2014 its charters, skill indexes and roster \u2014 changed since an earlier copy in this conversation, or that copy may have been compacted away. This copy replaces every earlier one, including charters and skills an earlier turn carried in its own text: where they differ, this one holds, and anything an earlier copy said that this one does not no longer applies.";
5787
5855
  }
5788
5856
  });
5789
5857
 
@@ -5922,7 +5990,6 @@ function createCodexAdapter(deps = {}) {
5922
5990
  const directory = req.local.cwd ?? "";
5923
5991
  const decision = decideResume3(req.session, directory);
5924
5992
  const resumeThreadId = "resume" in decision ? decision.resume : null;
5925
- const threadPromptFingerprint = "resume" in decision ? decision.promptFingerprint : null;
5926
5993
  const threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
5927
5994
  if ("fresh" in decision && decision.reason === "cwd_mismatch") {
5928
5995
  deps.onWarn?.(
@@ -5939,6 +6006,32 @@ function createCodexAdapter(deps = {}) {
5939
6006
  degraded = false;
5940
6007
  }
5941
6008
  }
6009
+ let compactions = 0;
6010
+ let threadStanding = null;
6011
+ if ("resume" in decision) {
6012
+ compactions = deps.countCompactions ? await deps.countCompactions(decision.resume) : null;
6013
+ if (signal.aborted) return;
6014
+ if (compactions === null) {
6015
+ deps.onWarn?.(
6016
+ "codex adapter: the thread's compaction count could not be read \u2014 re-sending the standing instructions (ENG210)",
6017
+ { threadId: decision.resume }
6018
+ );
6019
+ } else if (decision.compactions === compactions) {
6020
+ threadStanding = {
6021
+ core: decision.promptFingerprint,
6022
+ conversation: decision.conversationFingerprint
6023
+ };
6024
+ } else if (decision.compactions !== null && compactions > decision.compactions) {
6025
+ deps.onWarn?.(
6026
+ "codex adapter: the thread compacted since the last turn \u2014 re-sending the standing instructions (ENG210)",
6027
+ {
6028
+ threadId: decision.resume,
6029
+ compactions,
6030
+ compactionsSince: compactions - decision.compactions
6031
+ }
6032
+ );
6033
+ }
6034
+ }
5942
6035
  let instructions = null;
5943
6036
  if (!req.policy.hostFs) {
5944
6037
  if (!deps.writeInstructionsFile) {
@@ -5983,7 +6076,7 @@ function createCodexAdapter(deps = {}) {
5983
6076
  req,
5984
6077
  resumeThreadId,
5985
6078
  instructions?.path ?? null,
5986
- threadPromptFingerprint,
6079
+ threadStanding,
5987
6080
  images?.paths ?? []
5988
6081
  );
5989
6082
  if (spec.model === null) {
@@ -5998,10 +6091,18 @@ function createCodexAdapter(deps = {}) {
5998
6091
  cwd: req.local.cwd,
5999
6092
  resumedThreadId: resumeThreadId,
6000
6093
  degraded,
6001
- // Which prompt the thread carries once this turn lands. If we sent one, it's that one — the
6002
- // later copy is what the model reads. If we withheld it, the thread carries what it did. Read
6003
- // off the spec, so the record can't disagree with what was sent.
6004
- promptFingerprint: spec.promptRidesInput ? spec.promptFingerprint : threadPromptFingerprint,
6094
+ // Which segments the thread carries once this turn lands. A segment sent is that one —
6095
+ // the later copy is what the model reads. One withheld is what the thread had. Read off
6096
+ // the spec, so the record can't disagree with what was sent.
6097
+ promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
6098
+ conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
6099
+ compactions,
6100
+ // What the thread held before this turn, for the write at `thread.started`.
6101
+ heldStanding: {
6102
+ promptFingerprint: threadStanding?.core ?? null,
6103
+ conversationFingerprint: threadStanding?.conversation ?? null,
6104
+ compactions: threadStanding ? compactions : null
6105
+ },
6005
6106
  storedState: req.session,
6006
6107
  usageBaseline: threadUsageBaseline,
6007
6108
  // Codex reports no model in-stream, so record what the thread was started with (null on the
@@ -6072,7 +6173,7 @@ function makeRequest3(overrides2 = {}) {
6072
6173
  ...overrides2
6073
6174
  };
6074
6175
  }
6075
- var ABORT_SENTINEL3, NEW_THREAD_ID, BOUNDED_NOTICE2, COMPANION_POLICY2, DIR2, PROMPT, threadStarted, agentMessage, reasoning, toolFrame, turnCompleted, turnFailed, fatalError, errorItem, OP_SUCCESS, OP_FAILURE, sessionEvent3, CODEX_CONFORMANCE_FIXTURES;
6176
+ var ABORT_SENTINEL3, NEW_THREAD_ID, BOUNDED_NOTICE2, COMPANION_POLICY2, DIR2, PROMPT, threadStarted, agentMessage, reasoning, toolFrame, turnCompleted, turnFailed, fatalError, errorItem, OP_SUCCESS, OP_FAILURE, startedSession, closingSession, CODEX_CONFORMANCE_FIXTURES;
6076
6177
  var init_conformance4 = __esm({
6077
6178
  "packages/agent-runtime/src/codex/conformance.ts"() {
6078
6179
  "use strict";
@@ -6127,11 +6228,21 @@ var init_conformance4 = __esm({
6127
6228
  message: "files.read returned more than the result ceiling"
6128
6229
  }
6129
6230
  };
6130
- sessionEvent3 = (threadId, cwd = DIR2, degraded = false, promptFingerprint = fingerprintPrompt(PROMPT), usageBaseline = null) => ({
6231
+ startedSession = (threadId, cwd = DIR2, degraded = false) => ({
6131
6232
  type: "session",
6132
- state: encodeSession3({ threadId, cwd, promptFingerprint, usageBaseline }),
6233
+ state: encodeSession3({ threadId, cwd, promptFingerprint: null, usageBaseline: null }),
6133
6234
  ...degraded ? { degraded: true } : {}
6134
6235
  });
6236
+ closingSession = (threadId, usageBaseline = null) => ({
6237
+ type: "session",
6238
+ state: encodeSession3({
6239
+ threadId,
6240
+ cwd: DIR2,
6241
+ promptFingerprint: fingerprintPrompt(PROMPT),
6242
+ usageBaseline,
6243
+ compactions: 0
6244
+ })
6245
+ });
6135
6246
  CODEX_CONFORMANCE_FIXTURES = [
6136
6247
  {
6137
6248
  // A plain text reply: the held agent message seals as the terminal final at `turn.completed`;
@@ -6154,11 +6265,11 @@ var init_conformance4 = __esm({
6154
6265
  })
6155
6266
  ],
6156
6267
  expected: [
6157
- sessionEvent3(NEW_THREAD_ID),
6268
+ startedSession(NEW_THREAD_ID),
6158
6269
  { type: "text", body: "Hello there.", terminal: true },
6159
6270
  // The closing session write — the thread's ending totals, recorded as the baseline the next turn
6160
6271
  // subtracts. A fresh thread, so the `result` usage below is the frame verbatim.
6161
- sessionEvent3(NEW_THREAD_ID, DIR2, false, fingerprintPrompt(PROMPT), {
6272
+ closingSession(NEW_THREAD_ID, {
6162
6273
  inputTokens: 100,
6163
6274
  outputTokens: 50,
6164
6275
  cacheReadTokens: 20,
@@ -6205,7 +6316,7 @@ var init_conformance4 = __esm({
6205
6316
  turnCompleted()
6206
6317
  ],
6207
6318
  expected: [
6208
- sessionEvent3(NEW_THREAD_ID),
6319
+ startedSession(NEW_THREAD_ID),
6209
6320
  { type: "text", body: "Reading your notes.", terminal: false },
6210
6321
  {
6211
6322
  type: "tool",
@@ -6225,6 +6336,7 @@ var init_conformance4 = __esm({
6225
6336
  result: [{ type: "text", text: "sleep early" }]
6226
6337
  },
6227
6338
  { type: "text", body: "Here's the summary.", terminal: true },
6339
+ closingSession(NEW_THREAD_ID),
6228
6340
  { type: "result", ok: true }
6229
6341
  ]
6230
6342
  },
@@ -6258,7 +6370,7 @@ var init_conformance4 = __esm({
6258
6370
  turnCompleted()
6259
6371
  ],
6260
6372
  expected: [
6261
- sessionEvent3(NEW_THREAD_ID),
6373
+ startedSession(NEW_THREAD_ID),
6262
6374
  {
6263
6375
  type: "tool",
6264
6376
  id: "query-success",
@@ -6276,6 +6388,7 @@ var init_conformance4 = __esm({
6276
6388
  input: { op: "files.read", args: { path: "/ABOUT.md" } },
6277
6389
  result: OP_SUCCESS
6278
6390
  },
6391
+ closingSession(NEW_THREAD_ID),
6279
6392
  { type: "result", ok: true }
6280
6393
  ]
6281
6394
  },
@@ -6311,7 +6424,7 @@ var init_conformance4 = __esm({
6311
6424
  turnCompleted()
6312
6425
  ],
6313
6426
  expected: [
6314
- sessionEvent3(NEW_THREAD_ID),
6427
+ startedSession(NEW_THREAD_ID),
6315
6428
  {
6316
6429
  type: "tool",
6317
6430
  id: "query-failed",
@@ -6329,6 +6442,7 @@ var init_conformance4 = __esm({
6329
6442
  input: { op: "files.read", args: { path: "/ABOUT.md" } },
6330
6443
  result: OP_FAILURE
6331
6444
  },
6445
+ closingSession(NEW_THREAD_ID),
6332
6446
  { type: "result", ok: true }
6333
6447
  ]
6334
6448
  },
@@ -6352,7 +6466,7 @@ var init_conformance4 = __esm({
6352
6466
  turnCompleted()
6353
6467
  ],
6354
6468
  expected: [
6355
- sessionEvent3(NEW_THREAD_ID),
6469
+ startedSession(NEW_THREAD_ID),
6356
6470
  {
6357
6471
  type: "tool",
6358
6472
  id: "mcp-failed",
@@ -6370,6 +6484,7 @@ var init_conformance4 = __esm({
6370
6484
  input: { q: "receipt" },
6371
6485
  result: "connection closed"
6372
6486
  },
6487
+ closingSession(NEW_THREAD_ID),
6373
6488
  { type: "result", ok: true }
6374
6489
  ]
6375
6490
  },
@@ -6395,7 +6510,7 @@ var init_conformance4 = __esm({
6395
6510
  turnCompleted()
6396
6511
  ],
6397
6512
  expected: [
6398
- sessionEvent3(NEW_THREAD_ID),
6513
+ startedSession(NEW_THREAD_ID),
6399
6514
  {
6400
6515
  type: "tool",
6401
6516
  id: "mcp-image",
@@ -6413,6 +6528,7 @@ var init_conformance4 = __esm({
6413
6528
  input: {},
6414
6529
  result: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }]
6415
6530
  },
6531
+ closingSession(NEW_THREAD_ID),
6416
6532
  { type: "result", ok: true }
6417
6533
  ]
6418
6534
  },
@@ -6453,7 +6569,7 @@ var init_conformance4 = __esm({
6453
6569
  turnCompleted()
6454
6570
  ],
6455
6571
  expected: [
6456
- sessionEvent3(NEW_THREAD_ID),
6572
+ startedSession(NEW_THREAD_ID),
6457
6573
  { type: "tool", id: "t1", name: "list", phase: "start", summary: "", input: {} },
6458
6574
  {
6459
6575
  type: "tool",
@@ -6464,6 +6580,7 @@ var init_conformance4 = __esm({
6464
6580
  input: { path: "/product/tasks" },
6465
6581
  result: [{ type: "text", text: "CT481.md" }]
6466
6582
  },
6583
+ closingSession(NEW_THREAD_ID),
6467
6584
  { type: "result", ok: true }
6468
6585
  ]
6469
6586
  },
@@ -6481,7 +6598,7 @@ var init_conformance4 = __esm({
6481
6598
  turnCompleted()
6482
6599
  ],
6483
6600
  expected: [
6484
- sessionEvent3(NEW_THREAD_ID),
6601
+ startedSession(NEW_THREAD_ID),
6485
6602
  {
6486
6603
  type: "tool",
6487
6604
  id: "ws1",
@@ -6498,6 +6615,7 @@ var init_conformance4 = __esm({
6498
6615
  summary: "best pizza in nyc",
6499
6616
  input: { query: "best pizza in nyc" }
6500
6617
  },
6618
+ closingSession(NEW_THREAD_ID),
6501
6619
  { type: "result", ok: true }
6502
6620
  ]
6503
6621
  },
@@ -6513,7 +6631,7 @@ var init_conformance4 = __esm({
6513
6631
  turnCompleted()
6514
6632
  ],
6515
6633
  expected: [
6516
- sessionEvent3(NEW_THREAD_ID),
6634
+ startedSession(NEW_THREAD_ID),
6517
6635
  {
6518
6636
  type: "tool",
6519
6637
  id: "ws2",
@@ -6530,6 +6648,7 @@ var init_conformance4 = __esm({
6530
6648
  summary: "",
6531
6649
  input: { query: "" }
6532
6650
  },
6651
+ closingSession(NEW_THREAD_ID),
6533
6652
  { type: "result", ok: true }
6534
6653
  ]
6535
6654
  },
@@ -6560,7 +6679,7 @@ var init_conformance4 = __esm({
6560
6679
  turnCompleted()
6561
6680
  ],
6562
6681
  expected: [
6563
- sessionEvent3(NEW_THREAD_ID),
6682
+ startedSession(NEW_THREAD_ID),
6564
6683
  { type: "text", body: "Working on it.", terminal: false },
6565
6684
  {
6566
6685
  type: "tool",
@@ -6592,7 +6711,7 @@ var init_conformance4 = __esm({
6592
6711
  turnFailed("provider exploded")
6593
6712
  ],
6594
6713
  expected: [
6595
- sessionEvent3(NEW_THREAD_ID),
6714
+ startedSession(NEW_THREAD_ID),
6596
6715
  { type: "text", body: "Partial work.", terminal: false },
6597
6716
  { type: "runtime_notice", body: "provider exploded" },
6598
6717
  { type: "result", ok: false, reason: "error:provider exploded" }
@@ -6609,7 +6728,7 @@ var init_conformance4 = __esm({
6609
6728
  fatalError("stream died")
6610
6729
  ],
6611
6730
  expected: [
6612
- sessionEvent3(NEW_THREAD_ID),
6731
+ startedSession(NEW_THREAD_ID),
6613
6732
  { type: "text", body: "Starting.", terminal: false },
6614
6733
  { type: "runtime_notice", body: "stream died" },
6615
6734
  { type: "result", ok: false, reason: "error:stream died" }
@@ -6622,7 +6741,7 @@ var init_conformance4 = __esm({
6622
6741
  request: makeRequest3(),
6623
6742
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
6624
6743
  expected: [
6625
- sessionEvent3(NEW_THREAD_ID),
6744
+ startedSession(NEW_THREAD_ID),
6626
6745
  { type: "runtime_notice", body: "429 Too Many Requests" },
6627
6746
  { type: "result", ok: false, reason: "rate_limited" }
6628
6747
  ]
@@ -6632,7 +6751,7 @@ var init_conformance4 = __esm({
6632
6751
  request: makeRequest3(),
6633
6752
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("503 Service Unavailable")],
6634
6753
  expected: [
6635
- sessionEvent3(NEW_THREAD_ID),
6754
+ startedSession(NEW_THREAD_ID),
6636
6755
  { type: "runtime_notice", body: "503 Service Unavailable" },
6637
6756
  { type: "result", ok: false, reason: "server_error" }
6638
6757
  ]
@@ -6645,7 +6764,7 @@ var init_conformance4 = __esm({
6645
6764
  turnFailed("You've hit your session limit \xB7 resets 8:20pm (UTC)")
6646
6765
  ],
6647
6766
  expected: [
6648
- sessionEvent3(NEW_THREAD_ID),
6767
+ startedSession(NEW_THREAD_ID),
6649
6768
  {
6650
6769
  type: "runtime_notice",
6651
6770
  body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
@@ -6658,7 +6777,7 @@ var init_conformance4 = __esm({
6658
6777
  request: makeRequest3(),
6659
6778
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("401 Unauthorized: invalid api key")],
6660
6779
  expected: [
6661
- sessionEvent3(NEW_THREAD_ID),
6780
+ startedSession(NEW_THREAD_ID),
6662
6781
  { type: "runtime_notice", body: "401 Unauthorized: invalid api key" },
6663
6782
  { type: "result", ok: false, reason: "auth_expired" }
6664
6783
  ]
@@ -6670,7 +6789,7 @@ var init_conformance4 = __esm({
6670
6789
  request: makeRequest3(),
6671
6790
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
6672
6791
  expected: [
6673
- sessionEvent3(NEW_THREAD_ID),
6792
+ startedSession(NEW_THREAD_ID),
6674
6793
  { type: "runtime_notice", body: "something local broke" },
6675
6794
  { type: "result", ok: false, reason: "error:something local broke" }
6676
6795
  ]
@@ -6683,7 +6802,7 @@ var init_conformance4 = __esm({
6683
6802
  turnFailed(` ${BOUNDED_NOTICE2}${"y".repeat(20)} `)
6684
6803
  ],
6685
6804
  expected: [
6686
- sessionEvent3(NEW_THREAD_ID),
6805
+ startedSession(NEW_THREAD_ID),
6687
6806
  { type: "runtime_notice", body: BOUNDED_NOTICE2 },
6688
6807
  { type: "result", ok: false, reason: "usage_capped" }
6689
6808
  ]
@@ -6694,26 +6813,32 @@ var init_conformance4 = __esm({
6694
6813
  name: "empty failure text emits no runtime notice",
6695
6814
  request: makeRequest3(),
6696
6815
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
6697
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "error:unknown" }]
6816
+ expected: [
6817
+ startedSession(NEW_THREAD_ID),
6818
+ { type: "result", ok: false, reason: "error:unknown" }
6819
+ ]
6698
6820
  },
6699
6821
  {
6700
6822
  name: "whitespace-only failure text emits no runtime notice",
6701
6823
  request: makeRequest3(),
6702
6824
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed(" ")],
6703
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "error: " }]
6825
+ expected: [startedSession(NEW_THREAD_ID), { type: "result", ok: false, reason: "error: " }]
6704
6826
  },
6705
6827
  {
6706
6828
  name: "bare status token emits no runtime notice",
6707
6829
  request: makeRequest3(),
6708
6830
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429")],
6709
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "rate_limited" }]
6831
+ expected: [
6832
+ startedSession(NEW_THREAD_ID),
6833
+ { type: "result", ok: false, reason: "rate_limited" }
6834
+ ]
6710
6835
  },
6711
6836
  {
6712
6837
  name: "single-word provider sentence emits a runtime notice",
6713
6838
  request: makeRequest3(),
6714
6839
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("Unavailable.")],
6715
6840
  expected: [
6716
- sessionEvent3(NEW_THREAD_ID),
6841
+ startedSession(NEW_THREAD_ID),
6717
6842
  { type: "runtime_notice", body: "Unavailable." },
6718
6843
  { type: "result", ok: false, reason: "error:Unavailable." }
6719
6844
  ]
@@ -6741,7 +6866,7 @@ var init_conformance4 = __esm({
6741
6866
  turnCompleted()
6742
6867
  ],
6743
6868
  expected: [
6744
- sessionEvent3(NEW_THREAD_ID),
6869
+ startedSession(NEW_THREAD_ID),
6745
6870
  {
6746
6871
  type: "result",
6747
6872
  ok: false,
@@ -6761,8 +6886,9 @@ var init_conformance4 = __esm({
6761
6886
  turnCompleted()
6762
6887
  ],
6763
6888
  expected: [
6764
- sessionEvent3(NEW_THREAD_ID),
6889
+ startedSession(NEW_THREAD_ID),
6765
6890
  { type: "text", body: "All good now.", terminal: true },
6891
+ closingSession(NEW_THREAD_ID),
6766
6892
  { type: "result", ok: true }
6767
6893
  ]
6768
6894
  },
@@ -6774,7 +6900,11 @@ var init_conformance4 = __esm({
6774
6900
  name: "zero-content success (empty runtime result)",
6775
6901
  request: makeRequest3(),
6776
6902
  nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
6777
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: true }]
6903
+ expected: [
6904
+ startedSession(NEW_THREAD_ID),
6905
+ closingSession(NEW_THREAD_ID),
6906
+ { type: "result", ok: true }
6907
+ ]
6778
6908
  },
6779
6909
  {
6780
6910
  // Empty-final: a clean turn that ended on a tool call with no closing message.
@@ -6799,7 +6929,7 @@ var init_conformance4 = __esm({
6799
6929
  turnCompleted()
6800
6930
  ],
6801
6931
  expected: [
6802
- sessionEvent3(NEW_THREAD_ID),
6932
+ startedSession(NEW_THREAD_ID),
6803
6933
  {
6804
6934
  type: "tool",
6805
6935
  id: "t1",
@@ -6817,6 +6947,7 @@ var init_conformance4 = __esm({
6817
6947
  input: { changes: [{ path: "a.md", kind: "add" }] },
6818
6948
  result: [{ path: "a.md", kind: "add" }]
6819
6949
  },
6950
+ closingSession(NEW_THREAD_ID),
6820
6951
  { type: "result", ok: true }
6821
6952
  ]
6822
6953
  },
@@ -6832,9 +6963,10 @@ var init_conformance4 = __esm({
6832
6963
  turnCompleted()
6833
6964
  ],
6834
6965
  expected: [
6835
- sessionEvent3(NEW_THREAD_ID),
6966
+ startedSession(NEW_THREAD_ID),
6836
6967
  { type: "thinking", text: "Let me check the notes." },
6837
6968
  { type: "text", body: "All set.", terminal: true },
6969
+ closingSession(NEW_THREAD_ID),
6838
6970
  { type: "result", ok: true }
6839
6971
  ]
6840
6972
  },
@@ -6852,8 +6984,9 @@ var init_conformance4 = __esm({
6852
6984
  }),
6853
6985
  nativeStream: [threadStarted(NEW_THREAD_ID), agentMessage("m1", "Done."), turnCompleted()],
6854
6986
  expected: [
6855
- sessionEvent3(NEW_THREAD_ID),
6987
+ startedSession(NEW_THREAD_ID),
6856
6988
  { type: "text", body: "Done.", terminal: true },
6989
+ closingSession(NEW_THREAD_ID),
6857
6990
  {
6858
6991
  type: "result",
6859
6992
  ok: true,
@@ -6875,7 +7008,8 @@ var init_conformance4 = __esm({
6875
7008
  threadId: "th1",
6876
7009
  cwd: DIR2,
6877
7010
  promptFingerprint: fingerprintPrompt(PROMPT),
6878
- usageBaseline: null
7011
+ usageBaseline: null,
7012
+ compactions: 0
6879
7013
  })
6880
7014
  }),
6881
7015
  nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
@@ -6897,8 +7031,9 @@ var init_conformance4 = __esm({
6897
7031
  }),
6898
7032
  nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
6899
7033
  expected: [
6900
- sessionEvent3("th1"),
7034
+ startedSession("th1"),
6901
7035
  { type: "text", body: "Back again.", terminal: true },
7036
+ closingSession("th1"),
6902
7037
  { type: "result", ok: true }
6903
7038
  ]
6904
7039
  },
@@ -6920,8 +7055,9 @@ var init_conformance4 = __esm({
6920
7055
  }),
6921
7056
  nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
6922
7057
  expected: [
6923
- sessionEvent3("th1"),
7058
+ startedSession("th1"),
6924
7059
  { type: "text", body: "Back again.", terminal: true },
7060
+ closingSession("th1"),
6925
7061
  { type: "result", ok: true }
6926
7062
  ]
6927
7063
  },
@@ -6947,8 +7083,9 @@ var init_conformance4 = __esm({
6947
7083
  turnCompleted()
6948
7084
  ],
6949
7085
  expected: [
6950
- sessionEvent3(NEW_THREAD_ID, DIR2, true),
7086
+ startedSession(NEW_THREAD_ID, DIR2, true),
6951
7087
  { type: "text", body: "Fresh start.", terminal: true },
7088
+ closingSession(NEW_THREAD_ID),
6952
7089
  { type: "result", ok: true }
6953
7090
  ]
6954
7091
  }
@@ -6976,6 +7113,7 @@ var init_src = __esm({
6976
7113
  init_empty_result();
6977
7114
  init_failure();
6978
7115
  init_turn_request();
7116
+ init_standing();
6979
7117
  init_conformance();
6980
7118
  init_transcript();
6981
7119
  init_transcript_sink();
@@ -8529,6 +8667,7 @@ function buildCompanionTurnRequest(params) {
8529
8667
  const { turnContext: t } = params;
8530
8668
  return {
8531
8669
  systemPrompt: t.systemPrompt,
8670
+ ...t.standing ? { standing: t.standing } : {},
8532
8671
  prompt: t.prompt,
8533
8672
  content: t.content,
8534
8673
  config: t.config,
@@ -8658,6 +8797,68 @@ var init_codex_images = __esm({
8658
8797
  }
8659
8798
  });
8660
8799
 
8800
+ // src/codex-compactions.ts
8801
+ import { createReadStream } from "fs";
8802
+ import { readdir } from "fs/promises";
8803
+ import { homedir as homedir8 } from "os";
8804
+ import { join as join23 } from "path";
8805
+ import { createInterface as createInterface2 } from "readline";
8806
+ function createCodexCompactionCounter(codexHome = process.env.CODEX_HOME || join23(homedir8(), ".codex")) {
8807
+ return async (threadId) => {
8808
+ try {
8809
+ const rollout = await findRollout(codexHome, threadId);
8810
+ return rollout ? await countCompactions(rollout) : null;
8811
+ } catch {
8812
+ return null;
8813
+ }
8814
+ };
8815
+ }
8816
+ async function findRollout(codexHome, threadId) {
8817
+ const id = UUID_V7.exec(threadId);
8818
+ if (!id) return null;
8819
+ const createdAt = parseInt(`${id[1]}${id[2]}`, 16);
8820
+ const suffix = `-${threadId}.jsonl`;
8821
+ for (const offset of [0, -DAY_MS, DAY_MS]) {
8822
+ const day = new Date(createdAt + offset);
8823
+ const dir2 = join23(
8824
+ codexHome,
8825
+ "sessions",
8826
+ String(day.getUTCFullYear()),
8827
+ String(day.getUTCMonth() + 1).padStart(2, "0"),
8828
+ String(day.getUTCDate()).padStart(2, "0")
8829
+ );
8830
+ let names;
8831
+ try {
8832
+ names = await readdir(dir2);
8833
+ } catch {
8834
+ continue;
8835
+ }
8836
+ const name = names.find((n) => n.startsWith("rollout-") && n.endsWith(suffix));
8837
+ if (name) return join23(dir2, name);
8838
+ }
8839
+ return null;
8840
+ }
8841
+ async function countCompactions(rollout) {
8842
+ const lines2 = createInterface2({
8843
+ input: createReadStream(rollout, { encoding: "utf8" }),
8844
+ crlfDelay: Infinity
8845
+ });
8846
+ let count = 0;
8847
+ for await (const line of lines2) {
8848
+ if (line.slice(0, RECORD_HEAD_CHARS).includes('"type":"compacted"')) count += 1;
8849
+ }
8850
+ return count;
8851
+ }
8852
+ var DAY_MS, UUID_V7, RECORD_HEAD_CHARS;
8853
+ var init_codex_compactions = __esm({
8854
+ "src/codex-compactions.ts"() {
8855
+ "use strict";
8856
+ DAY_MS = 864e5;
8857
+ UUID_V7 = /^([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8858
+ RECORD_HEAD_CHARS = 200;
8859
+ }
8860
+ });
8861
+
8661
8862
  // src/turn-seq-floor.ts
8662
8863
  function resolveSeqFloor(sources, ctx2) {
8663
8864
  const { serverFloor, outboxFloor } = sources;
@@ -8766,15 +8967,15 @@ var init_prepare_failure = __esm({
8766
8967
 
8767
8968
  // src/prepared.ts
8768
8969
  import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, rmSync as rmSync9, writeFileSync as writeFileSync8, existsSync as existsSync13 } from "fs";
8769
- import { join as join23 } from "path";
8970
+ import { join as join24 } from "path";
8770
8971
  function dirFor(workspaceId) {
8771
- return join23(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8972
+ return join24(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8772
8973
  }
8773
8974
  function conversationDir(workspaceId, conversationId) {
8774
- return join23(dirFor(workspaceId), encodeURIComponent(conversationId));
8975
+ return join24(dirFor(workspaceId), encodeURIComponent(conversationId));
8775
8976
  }
8776
8977
  function pathFor2(workspaceId, conversationId, agentId) {
8777
- return join23(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8978
+ return join24(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8778
8979
  }
8779
8980
  function readPrepared(workspaceId, conversationId, agentId) {
8780
8981
  const path = pathFor2(workspaceId, conversationId, agentId);
@@ -8812,9 +9013,9 @@ var init_prepared = __esm({
8812
9013
 
8813
9014
  // src/transcript-writer.ts
8814
9015
  import { appendFileSync, chmodSync as chmodSync5, copyFileSync as copyFileSync2, mkdirSync as mkdirSync14, readdirSync as readdirSync2, rmSync as rmSync10 } from "fs";
8815
- import { basename as basename3, dirname as dirname12, join as join24 } from "path";
9016
+ import { basename as basename3, dirname as dirname12, join as join25 } from "path";
8816
9017
  function transcriptsDir() {
8817
- return join24(runnerStateDir(), "transcripts");
9018
+ return join25(runnerStateDir(), "transcripts");
8818
9019
  }
8819
9020
  function fileName(meta) {
8820
9021
  const d = new Date(meta.ts);
@@ -8840,7 +9041,7 @@ function pruneOld(dir2, retain) {
8840
9041
  const drop = files.sort().slice(0, files.length - retain);
8841
9042
  for (const f of drop) {
8842
9043
  try {
8843
- rmSync10(join24(dir2, f), { force: true });
9044
+ rmSync10(join25(dir2, f), { force: true });
8844
9045
  } catch {
8845
9046
  }
8846
9047
  }
@@ -8858,7 +9059,7 @@ var init_transcript_writer = __esm({
8858
9059
  onWarn;
8859
9060
  constructor(dir2, meta, onWarn) {
8860
9061
  this.onWarn = onWarn;
8861
- this.path = join24(dir2, fileName(meta));
9062
+ this.path = join25(dir2, fileName(meta));
8862
9063
  try {
8863
9064
  mkdirSync14(dir2, { recursive: true });
8864
9065
  try {
@@ -8887,10 +9088,10 @@ var init_transcript_writer = __esm({
8887
9088
  preserveAnomaly() {
8888
9089
  if (this.broken) return;
8889
9090
  try {
8890
- const dir2 = join24(dirname12(this.path), "anomalies");
9091
+ const dir2 = join25(dirname12(this.path), "anomalies");
8891
9092
  mkdirSync14(dir2, { recursive: true, mode: 448 });
8892
9093
  chmodSync5(dir2, 448);
8893
- const target = join24(dir2, basename3(this.path));
9094
+ const target = join25(dir2, basename3(this.path));
8894
9095
  copyFileSync2(this.path, target);
8895
9096
  chmodSync5(target, 384);
8896
9097
  pruneOld(dir2, ANOMALY_RETAIN);
@@ -9190,6 +9391,7 @@ var init_turn_execution = __esm({
9190
9391
  init_bundled_binaries();
9191
9392
  init_codex_instructions();
9192
9393
  init_codex_images();
9394
+ init_codex_compactions();
9193
9395
  init_config();
9194
9396
  init_api_error_shape();
9195
9397
  init_errors();
@@ -9875,6 +10077,8 @@ var init_turn_execution = __esm({
9875
10077
  turnId: this.turnId,
9876
10078
  warn: (meta, msg) => turnLog.warn(meta, msg)
9877
10079
  }),
10080
+ // Whether a resumed thread compacted since its standing preamble was sent.
10081
+ countCompactions: createCodexCompactionCounter(),
9878
10082
  ...codexPath ? { transport: createSdkCodexTransport({ codexPathOverride: codexPath }) } : {}
9879
10083
  })
9880
10084
  );
@@ -9935,6 +10139,9 @@ var init_turn_execution = __esm({
9935
10139
  );
9936
10140
  return {
9937
10141
  systemPrompt: context.systemPrompt,
10142
+ // Replaced even when absent, so the segments can't outlive the
10143
+ // `systemPrompt` they were split from.
10144
+ standing: context.standing,
9938
10145
  prompt: context.prompt,
9939
10146
  content: context.content
9940
10147
  };
@@ -10327,7 +10534,7 @@ var init_turn_execution = __esm({
10327
10534
  // src/dispatcher.ts
10328
10535
  import { randomUUID as randomUUID4 } from "crypto";
10329
10536
  import { existsSync as existsSync15, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
10330
- import { join as join25 } from "path";
10537
+ import { join as join26 } from "path";
10331
10538
  function checkoutState(cwd) {
10332
10539
  if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
10333
10540
  if (!existsSync15(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
@@ -10340,7 +10547,7 @@ function checkoutState(cwd) {
10340
10547
  if (entries.length === 0) {
10341
10548
  return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
10342
10549
  }
10343
- const gitPath = join25(cwd, ".git");
10550
+ const gitPath = join26(cwd, ".git");
10344
10551
  if (!existsSync15(gitPath)) return { ok: true, reason: "usable" };
10345
10552
  let stat;
10346
10553
  try {
@@ -10348,7 +10555,7 @@ function checkoutState(cwd) {
10348
10555
  } catch (error) {
10349
10556
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
10350
10557
  }
10351
- if (stat.isDirectory() && !existsSync15(join25(gitPath, "HEAD")))
10558
+ if (stat.isDirectory() && !existsSync15(join26(gitPath, "HEAD")))
10352
10559
  return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
10353
10560
  return { ok: true, reason: "usable" };
10354
10561
  }
@@ -10726,8 +10933,8 @@ import {
10726
10933
  } from "fs";
10727
10934
  import { createRequire as createRequire3 } from "module";
10728
10935
  import { isIP } from "net";
10729
- import { homedir as homedir8, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo3 } from "os";
10730
- import { dirname as dirname14, join as join29, sep as sep5 } from "path";
10936
+ import { homedir as homedir9, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo3 } from "os";
10937
+ import { dirname as dirname14, join as join30, sep as sep5 } from "path";
10731
10938
  import { fileURLToPath as fileURLToPath7 } from "url";
10732
10939
  import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
10733
10940
  function accountHome() {
@@ -10761,7 +10968,7 @@ async function resolvedAddresses(baseUrl) {
10761
10968
  function companionPackageRoot(modulePath) {
10762
10969
  for (let dir2 = dirname14(modulePath); ; dir2 = dirname14(dir2)) {
10763
10970
  try {
10764
- const manifest = JSON.parse(readFileSync16(join29(dir2, "package.json"), "utf8"));
10971
+ const manifest = JSON.parse(readFileSync16(join30(dir2, "package.json"), "utf8"));
10765
10972
  if (manifest.name === "@cabane/companion") return dir2;
10766
10973
  } catch {
10767
10974
  }
@@ -10838,22 +11045,22 @@ function companionStoreDenies(input) {
10838
11045
  const always = [
10839
11046
  cabaneDir2,
10840
11047
  ...homes.flatMap((home) => [
10841
- join29(home, ".cabane"),
10842
- join29(home, "*", ".cabane"),
10843
- join29(home, "*", "*", ".cabane")
11048
+ join30(home, ".cabane"),
11049
+ join30(home, "*", ".cabane"),
11050
+ join30(home, "*", "*", ".cabane")
10844
11051
  ]),
10845
11052
  // A glob anchored at the root has no directory to start from.
10846
- ...dirname14(parent) === parent ? [] : [join29(parent, "*", ".cabane")]
11053
+ ...dirname14(parent) === parent ? [] : [join30(parent, "*", ".cabane")]
10847
11054
  ];
10848
11055
  const companionHome = dirname14(cabaneDir2);
10849
- const otherLogins = foundStores.map((store) => dirname14(store)).filter((home) => home !== companionHome).flatMap((home) => [join29(home, ".claude"), join29(home, ".codex")]).filter((path) => !input.ownState.some((own) => isAtOrUnder(own, path)));
11056
+ const otherLogins = foundStores.map((store) => dirname14(store)).filter((home) => home !== companionHome).flatMap((home) => [join30(home, ".claude"), join30(home, ".codex")]).filter((path) => !input.ownState.some((own) => isAtOrUnder(own, path)));
10850
11057
  return [.../* @__PURE__ */ new Set([...always, ...foundStores, ...otherLogins])];
10851
11058
  }
10852
11059
  function companionStoreWatch(log2) {
10853
11060
  return new CompanionStoreWatch({
10854
11061
  log: log2,
10855
11062
  roots: () => {
10856
- const home = homedir8();
11063
+ const home = homedir9();
10857
11064
  return storeWalkRoots([.../* @__PURE__ */ new Set([home, accountHome() ?? home])], cabaneDir());
10858
11065
  }
10859
11066
  });
@@ -10863,7 +11070,7 @@ function isAtOrUnder(path, dir2) {
10863
11070
  }
10864
11071
  function expandHome(path, home) {
10865
11072
  if (path === "~") return home;
10866
- return path.startsWith("~/") ? join29(home, path.slice(2)) : path;
11073
+ return path.startsWith("~/") ? join30(home, path.slice(2)) : path;
10867
11074
  }
10868
11075
  function stripPort(pattern) {
10869
11076
  return pattern.startsWith("[") ? pattern.replace(/^\[([^\]]+)\](:\d+)?$/, "$1") : pattern.replace(/:\d+$/, "");
@@ -10881,8 +11088,8 @@ function patternsOverlap(a, b) {
10881
11088
  }
10882
11089
  function harnessSandboxConfig(input) {
10883
11090
  const { policy } = input;
10884
- const claudeDir = input.claudeConfigDir ?? join29(input.home, ".claude");
10885
- const codexDir = input.codexHome ?? join29(input.home, ".codex");
11091
+ const claudeDir = input.claudeConfigDir ?? join30(input.home, ".claude");
11092
+ const codexDir = input.codexHome ?? join30(input.home, ".codex");
10886
11093
  const homes = [.../* @__PURE__ */ new Set([input.home, input.accountHome ?? input.home])];
10887
11094
  const expand = (paths) => (paths ?? []).map((p) => expandHome(p, input.home));
10888
11095
  const cabane2 = cabaneHostEntries(input.baseUrl, input.resolved, input.ownAddresses);
@@ -10899,7 +11106,7 @@ function harnessSandboxConfig(input) {
10899
11106
  foundStores: input.foundStores,
10900
11107
  ownState: [claudeDir, codexDir]
10901
11108
  }),
10902
- ...homes.flatMap((home) => SECRET_PATHS_UNDER_HOME.map((p) => join29(home, p))),
11109
+ ...homes.flatMap((home) => SECRET_PATHS_UNDER_HOME.map((p) => join30(home, p))),
10903
11110
  ...expand(policy.filesystem?.denyRead)
10904
11111
  ];
10905
11112
  const strict = policy.filesystem?.mode === "strict";
@@ -10907,7 +11114,7 @@ function harnessSandboxConfig(input) {
10907
11114
  const allowRead = strict ? [
10908
11115
  ...input.cwd ? [input.cwd] : [],
10909
11116
  claudeDir,
10910
- join29(input.home, ".claude.json"),
11117
+ join30(input.home, ".claude.json"),
10911
11118
  codexDir,
10912
11119
  ...input.runtimePaths ?? [],
10913
11120
  ...expand(policy.filesystem?.allowRead)
@@ -10947,7 +11154,7 @@ function harnessSandboxConfig(input) {
10947
11154
  ...input.cwd ? [input.cwd] : [],
10948
11155
  input.tmp,
10949
11156
  claudeDir,
10950
- join29(input.home, ".claude.json"),
11157
+ join30(input.home, ".claude.json"),
10951
11158
  codexDir,
10952
11159
  ...expand(policy.filesystem?.allowWrite)
10953
11160
  ],
@@ -10998,7 +11205,7 @@ async function prepareHarnessSandbox(input) {
10998
11205
  baseUrl: input.baseUrl,
10999
11206
  resolved: await resolvedAddresses(input.baseUrl),
11000
11207
  ownAddresses: ownAddresses(),
11001
- home: homedir8(),
11208
+ home: homedir9(),
11002
11209
  accountHome: accountHome(),
11003
11210
  tmp: tmpdir4(),
11004
11211
  cabaneDir: cabaneDir(),
@@ -11049,9 +11256,9 @@ async function prepareHarnessSandbox(input) {
11049
11256
  } finally {
11050
11257
  for (const name of held) delete process.env[name];
11051
11258
  }
11052
- mkdirSync17(join29(cabaneDir(), "sandbox"), { recursive: true });
11053
- const dir2 = mkdtempSync(join29(cabaneDir(), "sandbox", "turn-"));
11054
- const script = join29(dir2, "exec-sandboxed");
11259
+ mkdirSync17(join30(cabaneDir(), "sandbox"), { recursive: true });
11260
+ const dir2 = mkdtempSync(join30(cabaneDir(), "sandbox", "turn-"));
11261
+ const script = join30(dir2, "exec-sandboxed");
11055
11262
  writeFileSync10(
11056
11263
  script,
11057
11264
  [
@@ -11067,7 +11274,7 @@ async function prepareHarnessSandbox(input) {
11067
11274
  return {
11068
11275
  wrap: (command, args) => ({ command: script, args: [command, ...args] }),
11069
11276
  executableFor: (real) => {
11070
- const shim = join29(dir2, `exec-${++shims}`);
11277
+ const shim = join30(dir2, `exec-${++shims}`);
11071
11278
  writeFileSync10(shim, `#!/bin/bash
11072
11279
  exec ${shellQuote(script)} ${shellQuote(real)} "$@"
11073
11280
  `);
@@ -11160,7 +11367,7 @@ import {
11160
11367
  rmSync as rmSync15,
11161
11368
  writeFileSync as writeFileSync11
11162
11369
  } from "fs";
11163
- import { join as join30 } from "path";
11370
+ import { join as join31 } from "path";
11164
11371
  var MAX_ENTRIES, MAX_AGE_MS, Outbox;
11165
11372
  var init_outbox = __esm({
11166
11373
  "src/outbox.ts"() {
@@ -11178,10 +11385,10 @@ var init_outbox = __esm({
11178
11385
  // Resolved lazily (per call) so tests that swap HOME between
11179
11386
  // cases route writes at the right tmpdir.
11180
11387
  dir() {
11181
- return join30(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
11388
+ return join31(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
11182
11389
  }
11183
11390
  fileFor(turnId, seq) {
11184
- return join30(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
11391
+ return join31(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
11185
11392
  }
11186
11393
  // Persist a commit for later draining. Atomic (temp file + rename) so a
11187
11394
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -11223,7 +11430,7 @@ var init_outbox = __esm({
11223
11430
  const entries = [];
11224
11431
  for (const name of names) {
11225
11432
  if (!name.endsWith(".json")) continue;
11226
- const full = join30(dir2, name);
11433
+ const full = join31(dir2, name);
11227
11434
  try {
11228
11435
  const parsed = JSON.parse(readFileSync17(full, "utf8"));
11229
11436
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -11352,7 +11559,7 @@ var init_image_manifest = __esm({
11352
11559
 
11353
11560
  // src/runner/exec-shim.ts
11354
11561
  import { chmodSync as chmodSync8, mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync2, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "fs";
11355
- import { join as join34 } from "path";
11562
+ import { join as join35 } from "path";
11356
11563
  function shellQuote2(value) {
11357
11564
  return `'${value.replace(/'/g, `'\\''`)}'`;
11358
11565
  }
@@ -11362,10 +11569,10 @@ function createShimDir() {
11362
11569
  return {
11363
11570
  write(argv) {
11364
11571
  if (!dir2) {
11365
- mkdirSync20(join34(cabaneDir(), "runner"), { recursive: true });
11366
- dir2 = mkdtempSync2(join34(cabaneDir(), "runner", "turn-"));
11572
+ mkdirSync20(join35(cabaneDir(), "runner"), { recursive: true });
11573
+ dir2 = mkdtempSync2(join35(cabaneDir(), "runner", "turn-"));
11367
11574
  }
11368
- const path = join34(dir2, `exec-${++count}`);
11575
+ const path = join35(dir2, `exec-${++count}`);
11369
11576
  writeFileSync13(path, `#!/bin/sh
11370
11577
  exec ${argv.map(shellQuote2).join(" ")} "$@"
11371
11578
  `);
@@ -11386,7 +11593,7 @@ var init_exec_shim = __esm({
11386
11593
  });
11387
11594
 
11388
11595
  // src/runner/launcher-channel.ts
11389
- import { createInterface as createInterface2 } from "readline";
11596
+ import { createInterface as createInterface3 } from "readline";
11390
11597
  function openLauncherChannel(stdin, emit, onLauncherGone) {
11391
11598
  const waiting = /* @__PURE__ */ new Map();
11392
11599
  const replies = /* @__PURE__ */ new Map();
@@ -11394,7 +11601,7 @@ function openLauncherChannel(stdin, emit, onLauncherGone) {
11394
11601
  let resolveFirst;
11395
11602
  let gotFirst = false;
11396
11603
  const first = new Promise((resolve2) => resolveFirst = resolve2);
11397
- const lines2 = createInterface2({ input: stdin, crlfDelay: Infinity });
11604
+ const lines2 = createInterface3({ input: stdin, crlfDelay: Infinity });
11398
11605
  lines2.on("line", (line) => {
11399
11606
  if (!gotFirst) {
11400
11607
  gotFirst = true;
@@ -15754,7 +15961,7 @@ function createLocalProcessProvider(opts = {}) {
15754
15961
  import { createHash as createHash5, randomUUID as randomUUID5 } from "crypto";
15755
15962
  import { constants as constants3, tmpdir as tmpdir3 } from "os";
15756
15963
  import {
15757
- createReadStream,
15964
+ createReadStream as createReadStream2,
15758
15965
  createWriteStream as createWriteStream2,
15759
15966
  existsSync as existsSync18,
15760
15967
  mkdirSync as mkdirSync16,
@@ -15765,7 +15972,7 @@ import {
15765
15972
  statSync as statSync4,
15766
15973
  writeFileSync as writeFileSync9
15767
15974
  } from "fs";
15768
- import { join as join28 } from "path";
15975
+ import { join as join29 } from "path";
15769
15976
  import { Writable } from "stream";
15770
15977
 
15771
15978
  // src/providers/docker-cli.ts
@@ -15838,7 +16045,7 @@ import {
15838
16045
  renameSync as renameSync10,
15839
16046
  rmSync as rmSync12
15840
16047
  } from "fs";
15841
- import { join as join26 } from "path";
16048
+ import { join as join27 } from "path";
15842
16049
  function routes(t) {
15843
16050
  const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
15844
16051
  const agent = `${conv}/participants/agents/${t.agentId}`;
@@ -15884,7 +16091,7 @@ function publishStaged(from, to, move = atomicMove) {
15884
16091
  const staged = readdirSync4(from).filter((n) => n.endsWith(".json")).map((name) => {
15885
16092
  let entry = {};
15886
16093
  try {
15887
- entry = JSON.parse(readFileSync14(join26(from, name), "utf8"));
16094
+ entry = JSON.parse(readFileSync14(join27(from, name), "utf8"));
15888
16095
  } catch {
15889
16096
  }
15890
16097
  return { name, entry };
@@ -15897,7 +16104,7 @@ function publishStaged(from, to, move = atomicMove) {
15897
16104
  let published = 0;
15898
16105
  for (const { name } of staged) {
15899
16106
  try {
15900
- move(join26(from, name), join26(to, name));
16107
+ move(join27(from, name), join27(to, name));
15901
16108
  } catch {
15902
16109
  return { published, complete: false };
15903
16110
  }
@@ -15910,7 +16117,7 @@ function publishStaged(from, to, move = atomicMove) {
15910
16117
  // src/providers/runner-base.ts
15911
16118
  import { createHash as createHash4 } from "crypto";
15912
16119
  import { existsSync as existsSync17, realpathSync as realpathSync6 } from "fs";
15913
- import { dirname as dirname13, join as join27, sep as sep4 } from "path";
16120
+ import { dirname as dirname13, join as join28, sep as sep4 } from "path";
15914
16121
  import { fileURLToPath as fileURLToPath6 } from "url";
15915
16122
  var COMPANION_MOUNT = "/opt/cabane/companion";
15916
16123
  var BASE_DOCKERFILE = `FROM node:22-slim
@@ -15958,11 +16165,11 @@ async function ensureBaseImage(docker, image, log2) {
15958
16165
  function ownInstall(moduleFile = fileURLToPath6(import.meta.url)) {
15959
16166
  let dir2 = dirname13(moduleFile);
15960
16167
  for (let i = 0; i < 4; i++) {
15961
- if (existsSync17(join27(dir2, "dist", "cli.js")) && existsSync17(join27(dir2, "package.json"))) break;
16168
+ if (existsSync17(join28(dir2, "dist", "cli.js")) && existsSync17(join28(dir2, "package.json"))) break;
15962
16169
  dir2 = dirname13(dir2);
15963
16170
  }
15964
- if (!existsSync17(join27(dir2, "dist", "cli.js"))) return null;
15965
- const sdk = join27(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
16171
+ if (!existsSync17(join28(dir2, "dist", "cli.js"))) return null;
16172
+ const sdk = join28(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
15966
16173
  if (!existsSync17(sdk)) return null;
15967
16174
  const root = realpathSync6(dir2);
15968
16175
  return realpathSync6(sdk).startsWith(root + sep4) ? root : null;
@@ -16101,39 +16308,39 @@ function networkCreateError(stderr) {
16101
16308
  }
16102
16309
  function sessionArchivePath(deviceDir, attemptId) {
16103
16310
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
16104
- return join28(deviceDir, "attempts", attemptId, "session.tar");
16311
+ return join29(deviceDir, "attempts", attemptId, "session.tar");
16105
16312
  }
16106
16313
  function pruneOtherAttempts(deviceDir, pair2, keep) {
16107
- const root = join28(deviceDir, "attempts");
16314
+ const root = join29(deviceDir, "attempts");
16108
16315
  if (!existsSync18(root)) return;
16109
16316
  for (const id of readdirSync5(root)) {
16110
16317
  if (id === keep || !UUID_RE.test(id)) continue;
16111
16318
  let pairOf = "";
16112
16319
  try {
16113
- pairOf = readFileSync15(join28(root, id, "pair"), "utf8");
16320
+ pairOf = readFileSync15(join29(root, id, "pair"), "utf8");
16114
16321
  } catch {
16115
16322
  continue;
16116
16323
  }
16117
16324
  if (pairOf === `${pair2.conversationId}/${pair2.agentId}`) {
16118
- rmSync13(join28(root, id), { recursive: true, force: true });
16325
+ rmSync13(join29(root, id), { recursive: true, force: true });
16119
16326
  }
16120
16327
  }
16121
16328
  }
16122
16329
  var IDLE_SESSION_ARCHIVE_MS = 30 * 24 * 60 * 6e4;
16123
16330
  function pruneIdleSessionArchives(deviceDir, now = Date.now(), maxAgeMs = IDLE_SESSION_ARCHIVE_MS) {
16124
- const root = join28(deviceDir, "attempts");
16331
+ const root = join29(deviceDir, "attempts");
16125
16332
  if (!existsSync18(root)) return [];
16126
16333
  const removed = [];
16127
16334
  for (const id of readdirSync5(root)) {
16128
16335
  if (!UUID_RE.test(id)) continue;
16129
16336
  let kept;
16130
16337
  try {
16131
- kept = statSync4(join28(root, id)).mtimeMs;
16338
+ kept = statSync4(join29(root, id)).mtimeMs;
16132
16339
  } catch {
16133
16340
  continue;
16134
16341
  }
16135
16342
  if (now - kept < maxAgeMs) continue;
16136
- rmSync13(join28(root, id), { recursive: true, force: true });
16343
+ rmSync13(join29(root, id), { recursive: true, force: true });
16137
16344
  removed.push(id);
16138
16345
  }
16139
16346
  return removed;
@@ -16334,11 +16541,11 @@ function createDockerProvider(opts) {
16334
16541
  await must(["network", "disconnect", "-f", other, name], "moving the container");
16335
16542
  if (others.includes(sharedNetwork)) await retireSharedNetwork();
16336
16543
  };
16337
- const socketRoot = opts.socketRoot ?? join28(
16544
+ const socketRoot = opts.socketRoot ?? join29(
16338
16545
  process.env.XDG_RUNTIME_DIR && existsSync18(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir3(),
16339
16546
  `cabane-runners-${opts.deviceKey}`
16340
16547
  );
16341
- const socketDir = (name) => join28(socketRoot, createHash5("sha256").update(name).digest("hex").slice(0, 12));
16548
+ const socketDir = (name) => join29(socketRoot, createHash5("sha256").update(name).digest("hex").slice(0, 12));
16342
16549
  const create = async (name, attempt) => {
16343
16550
  mkdirSync16(socketRoot, { recursive: true, mode: 448 });
16344
16551
  mkdirSync16(socketDir(name), { recursive: true, mode: 457 });
@@ -16480,7 +16687,7 @@ function createDockerProvider(opts) {
16480
16687
  }
16481
16688
  };
16482
16689
  const removeIn = (name, from, names) => names.length ? execIn(name, ["sh", "-c", 'cd "$1" && shift && rm -f -- "$@"', "sh", from, ...names]) : Promise.resolve(null);
16483
- const pendingPath = (name) => join28(opts.deviceDir, "runners-pending", `${name}.json`);
16690
+ const pendingPath = (name) => join29(opts.deviceDir, "runners-pending", `${name}.json`);
16484
16691
  const readPending = (name) => {
16485
16692
  try {
16486
16693
  const ids = JSON.parse(readFileSync15(pendingPath(name), "utf8"));
@@ -16494,12 +16701,12 @@ function createDockerProvider(opts) {
16494
16701
  rmSync13(pendingPath(name), { force: true });
16495
16702
  return;
16496
16703
  }
16497
- mkdirSync16(join28(opts.deviceDir, "runners-pending"), { recursive: true });
16704
+ mkdirSync16(join29(opts.deviceDir, "runners-pending"), { recursive: true });
16498
16705
  writeFileSync9(pendingPath(name), JSON.stringify([...new Set(ids)]));
16499
16706
  };
16500
16707
  const keepSession = async (name, attempt, attemptId) => {
16501
16708
  const archive = sessionArchivePath(opts.deviceDir, attemptId);
16502
- mkdirSync16(join28(archive, ".."), { recursive: true });
16709
+ mkdirSync16(join29(archive, ".."), { recursive: true });
16503
16710
  const partial = `${archive}.${process.pid}.partial`;
16504
16711
  const file = new CappedFile(partial, sessionCap);
16505
16712
  const r = await execIn(
@@ -16516,7 +16723,7 @@ function createDockerProvider(opts) {
16516
16723
  );
16517
16724
  if (r.code === 0 && !file.over) {
16518
16725
  renameSync11(partial, archive);
16519
- writeFileSync9(join28(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
16726
+ writeFileSync9(join29(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
16520
16727
  pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
16521
16728
  await writeMarker(name, attemptId);
16522
16729
  log2.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
@@ -16530,7 +16737,7 @@ function createDockerProvider(opts) {
16530
16737
  file.over ? "runners: the session files are over the size cap and weren't kept; the next new container starts the session over" : "runners: couldn't keep the session files; the next new container starts the session over"
16531
16738
  );
16532
16739
  };
16533
- const stagingDir = (name) => join28(opts.deviceDir, "runners-pending", `${name}.outbox`);
16740
+ const stagingDir = (name) => join29(opts.deviceDir, "runners-pending", `${name}.outbox`);
16534
16741
  const ensureRunning = async (name) => {
16535
16742
  const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
16536
16743
  if (seen.code !== 0) return false;
@@ -16538,7 +16745,7 @@ function createDockerProvider(opts) {
16538
16745
  return (await docker.run(["start", name])).code === 0;
16539
16746
  };
16540
16747
  const dropArchive = (attemptId) => {
16541
- rmSync13(join28(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
16748
+ rmSync13(join29(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
16542
16749
  recursive: true,
16543
16750
  force: true
16544
16751
  });
@@ -16607,7 +16814,7 @@ function createDockerProvider(opts) {
16607
16814
  if (seen.stdout.trim().split(" ")[0] !== "true")
16608
16815
  await must(["start", name], "starting the container");
16609
16816
  const git = await serveGitCredentials({
16610
- socketPath: join28(socketDir(name), "git.sock"),
16817
+ socketPath: join29(socketDir(name), "git.sock"),
16611
16818
  hosts: opts.gitHosts,
16612
16819
  fill: opts.gitFill,
16613
16820
  log: log2
@@ -16643,7 +16850,7 @@ function createDockerProvider(opts) {
16643
16850
  const archive = sessionArchivePath(opts.deviceDir, attemptId);
16644
16851
  if (existsSync18(archive)) {
16645
16852
  const r = await execIn(name, ["tar", "-x", "-C", CONTAINER_HOME, "-f", "-"], {
16646
- input: createReadStream(archive)
16853
+ input: createReadStream2(archive)
16647
16854
  });
16648
16855
  if (r.code !== 0) {
16649
16856
  log2.warn(
@@ -16756,15 +16963,15 @@ function createDockerProvider(opts) {
16756
16963
  const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
16757
16964
  if (transcripts) {
16758
16965
  const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
16759
- if (kept.length) mkdirSync16(join28(opts.hostStateDir, "transcripts"), { recursive: true });
16760
- for (const f of kept) writeFileSync9(join28(opts.hostStateDir, "transcripts", f.name), f.data);
16966
+ if (kept.length) mkdirSync16(join29(opts.hostStateDir, "transcripts"), { recursive: true });
16967
+ for (const f of kept) writeFileSync9(join29(opts.hostStateDir, "transcripts", f.name), f.data);
16761
16968
  await removeIn(
16762
16969
  name,
16763
16970
  `${CONTAINER_STATE}/transcripts`,
16764
16971
  kept.map((f) => f.name)
16765
16972
  );
16766
16973
  }
16767
- const outboxRel = join28("outbox", encodeURIComponent(attempt.agentId));
16974
+ const outboxRel = join29("outbox", encodeURIComponent(attempt.agentId));
16768
16975
  const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
16769
16976
  let unpublished = false;
16770
16977
  if (outbox) {
@@ -16790,7 +16997,7 @@ function createDockerProvider(opts) {
16790
16997
  const staged = stagingDir(name);
16791
16998
  mkdirSync16(staged, { recursive: true });
16792
16999
  writeFileSync9(
16793
- join28(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
17000
+ join29(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
16794
17001
  `${JSON.stringify(entry)}
16795
17002
  `
16796
17003
  );
@@ -16803,7 +17010,7 @@ function createDockerProvider(opts) {
16803
17010
  if (outbox.complete) {
16804
17011
  const published = publishStaged(
16805
17012
  stagingDir(name),
16806
- join28(opts.hostStateDir, outboxRel),
17013
+ join29(opts.hostStateDir, outboxRel),
16807
17014
  opts.moveFile
16808
17015
  );
16809
17016
  queued = published.published;
@@ -18778,9 +18985,9 @@ function handleUncaught(log2, err, origin) {
18778
18985
  // src/crash-marker.ts
18779
18986
  init_config();
18780
18987
  import { existsSync as existsSync20, mkdirSync as mkdirSync19, readFileSync as readFileSync18, rmSync as rmSync16, writeFileSync as writeFileSync12 } from "fs";
18781
- import { join as join31 } from "path";
18988
+ import { join as join32 } from "path";
18782
18989
  function crashMarkerPath() {
18783
- return join31(cabaneDir(), "last-error.json");
18990
+ return join32(cabaneDir(), "last-error.json");
18784
18991
  }
18785
18992
  function recordCrash(rec2) {
18786
18993
  try {
@@ -19093,8 +19300,8 @@ async function runAttached(runtime) {
19093
19300
  init_install_kind();
19094
19301
  init_cabane_command();
19095
19302
  import { readdirSync as readdirSync7, realpathSync as realpathSync8, existsSync as existsSync21 } from "fs";
19096
- import { homedir as homedir9 } from "os";
19097
- import { dirname as dirname15, join as join32 } from "path";
19303
+ import { homedir as homedir10 } from "os";
19304
+ import { dirname as dirname15, join as join33 } from "path";
19098
19305
  init_config();
19099
19306
  init_sandbox_policy();
19100
19307
  init_secrets();
@@ -19163,7 +19370,7 @@ function versionLine() {
19163
19370
  const install = companionInstall();
19164
19371
  const own = installedCommandPath({ install });
19165
19372
  if (install === "script") {
19166
- return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join32(homedir9(), ".cabane"))})`;
19373
+ return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join33(homedir10(), ".cabane"))})`;
19167
19374
  }
19168
19375
  if (own)
19169
19376
  return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname15(dirname15(own)))})`;
@@ -19189,8 +19396,8 @@ function otherInstall(install, own) {
19189
19396
  }
19190
19397
  };
19191
19398
  if (install === "npm") {
19192
- const script = join32(
19193
- homedir9(),
19399
+ const script = join33(
19400
+ homedir10(),
19194
19401
  ".cabane",
19195
19402
  "node",
19196
19403
  "lib",
@@ -19198,11 +19405,11 @@ function otherInstall(install, own) {
19198
19405
  "@cabane",
19199
19406
  "companion"
19200
19407
  );
19201
- return existsSync21(script) ? join32(homedir9(), ".cabane") : null;
19408
+ return existsSync21(script) ? join33(homedir10(), ".cabane") : null;
19202
19409
  }
19203
19410
  const found = firstOnPath("cabane-companion", process.env.PATH);
19204
19411
  if (!found) return null;
19205
- const ownAlias = own ? real(join32(dirname15(own), "cabane-companion")) : null;
19412
+ const ownAlias = own ? real(join33(dirname15(own), "cabane-companion")) : null;
19206
19413
  return real(found) === ownAlias ? null : dirname15(found);
19207
19414
  }
19208
19415
  async function liveState() {
@@ -19429,7 +19636,7 @@ init_errors();
19429
19636
  init_src();
19430
19637
  init_transcript_writer();
19431
19638
  import { existsSync as existsSync22, readFileSync as readFileSync19, readdirSync as readdirSync8 } from "fs";
19432
- import { isAbsolute, join as join33 } from "path";
19639
+ import { isAbsolute, join as join34 } from "path";
19433
19640
  async function transcript(opts = {}) {
19434
19641
  const dir2 = transcriptsDir();
19435
19642
  if (opts.follow) {
@@ -19446,7 +19653,7 @@ async function transcript(opts = {}) {
19446
19653
  process.stdout.write(emptyMessage(dir2));
19447
19654
  return;
19448
19655
  }
19449
- process.stdout.write(renderFile(join33(dir2, newest)) + "\n");
19656
+ process.stdout.write(renderFile(join34(dir2, newest)) + "\n");
19450
19657
  return;
19451
19658
  }
19452
19659
  printList(dir2);
@@ -19529,7 +19736,7 @@ function isComplete(content) {
19529
19736
  async function followTranscripts(dir2) {
19530
19737
  const follower = new TranscriptFollower({
19531
19738
  listFiles: () => listFiles(dir2),
19532
- read: (f) => readFileSync19(join33(dir2, f), "utf8"),
19739
+ read: (f) => readFileSync19(join34(dir2, f), "utf8"),
19533
19740
  write: (s) => process.stdout.write(s),
19534
19741
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
19535
19742
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -19569,7 +19776,7 @@ function printList(dir2) {
19569
19776
 
19570
19777
  `);
19571
19778
  for (const f of files.slice(0, 20)) {
19572
- const { meta, outcome } = peek(join33(dir2, f));
19779
+ const { meta, outcome } = peek(join34(dir2, f));
19573
19780
  const when = fmtTime(rec(meta)?.ts);
19574
19781
  const ws = str2(rec(meta)?.workspaceSlug);
19575
19782
  const o = rec(outcome);
@@ -19607,10 +19814,10 @@ function resolveTarget(dir2, target) {
19607
19814
  if (existsSync22(target)) return target;
19608
19815
  throw new CompanionError(`no transcript at ${target}.`);
19609
19816
  }
19610
- const exact = join33(dir2, target);
19817
+ const exact = join34(dir2, target);
19611
19818
  if (existsSync22(exact)) return exact;
19612
19819
  const matches = listFiles(dir2).filter((f) => f.includes(target));
19613
- if (matches.length === 1) return join33(dir2, matches[0]);
19820
+ if (matches.length === 1) return join34(dir2, matches[0]);
19614
19821
  if (matches.length === 0) {
19615
19822
  throw new CompanionError(
19616
19823
  `no transcript matching "${target}" in ${dir2}. Run \`${cabane("transcript")}\` to list them.`