@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/runtime.js CHANGED
@@ -3056,9 +3056,15 @@ var resolvedMcpServerSchema = z8.union([
3056
3056
  var resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
3057
3057
  var hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
3058
3058
  var turnRequestSchema = z8.object({
3059
- // Server-composed system prompt (core + capability prose + adapter addendum +
3060
- // charter). One string to the adapter.
3059
+ // Server-composed standing instructions: the core segment (body, tool naming, identity, agent
3060
+ // charter) and, after it, the conversation segment (workspace and room charters, skill indexes,
3061
+ // roster). One string to the adapter, current as of this dispatch.
3061
3062
  systemPrompt: z8.string(),
3063
+ // The same text as its two ordered segments, so an adapter that writes standing text into the
3064
+ // conversation (Codex's input preamble) can tell which half changed. `systemPrompt` is
3065
+ // `joinStanding(standing)`. Optional: absent from an API older than the split, where
3066
+ // `systemPrompt` is the core alone and the conversation material rides `prompt`.
3067
+ standing: z8.object({ core: z8.string(), conversation: z8.string() }).optional(),
3062
3068
  // Server-composed per-turn user text (anchor reminder + the triggering message).
3063
3069
  prompt: z8.string(),
3064
3070
  // The multi-block user-message body (text + vision).
@@ -3106,6 +3112,16 @@ var turnRequestSchema = z8.object({
3106
3112
  })
3107
3113
  });
3108
3114
 
3115
+ // packages/agent-runtime/src/standing.ts
3116
+ var STANDING_SEPARATOR = "\n\n---\n\n";
3117
+ function joinStanding(segments) {
3118
+ const conversation = segments.conversation.trim();
3119
+ return conversation.length > 0 ? `${segments.core}${STANDING_SEPARATOR}${conversation}` : segments.core;
3120
+ }
3121
+ function standingSegments(req) {
3122
+ return req.standing ?? { core: req.systemPrompt, conversation: "" };
3123
+ }
3124
+
3109
3125
  // packages/agent-runtime/src/conformance.ts
3110
3126
  import { z as z9 } from "zod";
3111
3127
  var conformanceFixtureSchema = z9.object({
@@ -3463,8 +3479,15 @@ var TURN_CONTROL_TOOLS = [
3463
3479
  "withdraw_ask"
3464
3480
  ];
3465
3481
  var TURN_CONTROL_TOOL_DESCRIPTIONS = {
3466
- 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.`,
3467
- 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.*\`).`,
3482
+ 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.
3483
+ - 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.
3484
+ - \`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.
3485
+ - 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.
3486
+ - 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.
3487
+ - 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.
3488
+ - 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.
3489
+ - 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\`.`,
3490
+ 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.*\`).`,
3468
3491
  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.`,
3469
3492
  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.`,
3470
3493
  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.`
@@ -3472,13 +3495,13 @@ var TURN_CONTROL_TOOL_DESCRIPTIONS = {
3472
3495
  var TURN_CONTROL_FIELD_DESCRIPTIONS = {
3473
3496
  ask: {
3474
3497
  targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
3475
- 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".`,
3476
- 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".`,
3477
- 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".`,
3498
+ 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\`.`,
3499
+ 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.`,
3500
+ 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.`,
3478
3501
  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.`,
3479
- "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".`,
3480
- "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".`,
3481
- "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".`,
3502
+ "questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
3503
+ "questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
3504
+ "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.`,
3482
3505
  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.",
3483
3506
  "replaces.askId": "The id of the open ask to withdraw.",
3484
3507
  "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.`
@@ -6114,7 +6137,14 @@ function fingerprintPrompt(prompt) {
6114
6137
  return `${prompt.length}.${(fnv >>> 0).toString(36)}.${(djb >>> 0).toString(36)}`;
6115
6138
  }
6116
6139
  function encodeSession3(state) {
6117
- return JSON.stringify(state);
6140
+ return JSON.stringify({
6141
+ threadId: state.threadId,
6142
+ cwd: state.cwd,
6143
+ promptFingerprint: state.promptFingerprint,
6144
+ conversationFingerprint: state.conversationFingerprint ?? null,
6145
+ compactions: state.compactions ?? null,
6146
+ usageBaseline: state.usageBaseline
6147
+ });
6118
6148
  }
6119
6149
  function decideResume3(stored, currentCwd) {
6120
6150
  if (!stored) return { fresh: true, reason: "no_session" };
@@ -6135,6 +6165,8 @@ function decideResume3(stored, currentCwd) {
6135
6165
  return {
6136
6166
  resume: state.threadId,
6137
6167
  promptFingerprint: typeof state.promptFingerprint === "string" ? state.promptFingerprint : null,
6168
+ conversationFingerprint: typeof state.conversationFingerprint === "string" ? state.conversationFingerprint : null,
6169
+ compactions: typeof state.compactions === "number" && Number.isInteger(state.compactions) && state.compactions >= 0 ? state.compactions : null,
6138
6170
  usageBaseline: readUsageBaseline(state.usageBaseline)
6139
6171
  };
6140
6172
  }
@@ -6167,6 +6199,7 @@ async function* decodeCodexStream(events, ctx) {
6167
6199
  let usage;
6168
6200
  let cumulativeUsage;
6169
6201
  let threadIdForSession = ctx.resumedThreadId;
6202
+ let lastState = ctx.storedState ?? null;
6170
6203
  let settled = false;
6171
6204
  function* flushInterim() {
6172
6205
  const sealed = sealHeld2(held, false);
@@ -6182,14 +6215,14 @@ async function* decodeCodexStream(events, ctx) {
6182
6215
  const state = encodeSession3({
6183
6216
  threadId,
6184
6217
  cwd: ctx.cwd ?? "",
6185
- promptFingerprint: ctx.promptFingerprint ?? null,
6186
- // The baseline this turn inherited, carried through unchanged. This early write records the
6187
- // thread id even if the turn dies mid-stream; it can't yet know what the turn will spend. The
6188
- // closing write records the new baseline.
6218
+ promptFingerprint: ctx.heldStanding?.promptFingerprint ?? null,
6219
+ conversationFingerprint: ctx.heldStanding?.conversationFingerprint ?? null,
6220
+ compactions: ctx.heldStanding?.compactions ?? null,
6189
6221
  usageBaseline: ctx.usageBaseline ?? null
6190
6222
  });
6191
6223
  if (threadId !== ctx.resumedThreadId || state !== ctx.storedState) {
6192
6224
  sessionEmitted = true;
6225
+ lastState = state;
6193
6226
  yield {
6194
6227
  type: "session",
6195
6228
  state,
@@ -6288,14 +6321,16 @@ async function* decodeCodexStream(events, ctx) {
6288
6321
  if (sealed) yield sealed;
6289
6322
  reason = "no_terminal";
6290
6323
  }
6291
- if (ok && cumulativeUsage && threadIdForSession) {
6324
+ if (ok && threadIdForSession) {
6292
6325
  const closing = encodeSession3({
6293
6326
  threadId: threadIdForSession,
6294
6327
  cwd: ctx.cwd ?? "",
6295
6328
  promptFingerprint: ctx.promptFingerprint ?? null,
6296
- usageBaseline: cumulativeUsage
6329
+ conversationFingerprint: ctx.conversationFingerprint ?? null,
6330
+ compactions: ctx.compactions ?? null,
6331
+ usageBaseline: cumulativeUsage ?? ctx.usageBaseline ?? null
6297
6332
  });
6298
- yield { type: "session", state: closing };
6333
+ if (closing !== lastState) yield { type: "session", state: closing };
6299
6334
  }
6300
6335
  const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
6301
6336
  yield {
@@ -6365,15 +6400,27 @@ var ENV_ENVELOPE_KEYS = [
6365
6400
  "CABANE_CONVERSATION_TITLE",
6366
6401
  "CABANE_HOST_ACCESS"
6367
6402
  ];
6368
- function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null, imagePaths = []) {
6403
+ function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadStanding = null, imagePaths = []) {
6369
6404
  const { policy, config } = req;
6370
6405
  const directory = req.local.cwd ?? "";
6371
6406
  const dialect = readCodexDialect(config.runtimeOptions);
6372
6407
  const baseInstructionsFile = !policy.hostFs && instructionsFile ? instructionsFile : null;
6373
6408
  const model = config.model ? parseCodexModel(config.model) : null;
6374
- const promptFingerprint = fingerprintPrompt(req.systemPrompt);
6375
- const threadHasThisPrompt = resumeThreadId !== null && threadPromptFingerprint !== null && threadPromptFingerprint === promptFingerprint;
6376
- const promptRidesInput = baseInstructionsFile === null && !threadHasThisPrompt;
6409
+ const segments = standingSegments(req);
6410
+ const conversationText = segments.conversation.trim();
6411
+ const fingerprints = {
6412
+ core: fingerprintPrompt(segments.core),
6413
+ conversation: conversationText.length > 0 ? fingerprintPrompt(conversationText) : null
6414
+ };
6415
+ const resumed = resumeThreadId !== null;
6416
+ const held = resumed ? threadStanding : null;
6417
+ const viaInput = baseInstructionsFile === null;
6418
+ const preamble = {
6419
+ core: viaInput && (held?.core ?? null) !== fingerprints.core,
6420
+ // Compared both ways: a held segment that is now empty differs from it, and nothing held
6421
+ // equals nothing to send.
6422
+ conversation: viaInput && (held?.conversation ?? null) !== fingerprints.conversation
6423
+ };
6377
6424
  return {
6378
6425
  resumeThreadId,
6379
6426
  directory,
@@ -6382,9 +6429,9 @@ function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromp
6382
6429
  skipGitRepoCheck: true,
6383
6430
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
6384
6431
  baseInstructionsFile,
6385
- input: withImages(buildInput(req, promptRidesInput), imagePaths),
6386
- promptRidesInput,
6387
- promptFingerprint,
6432
+ input: withImages(buildInput(req, segments, preamble, resumed), imagePaths),
6433
+ preamble,
6434
+ fingerprints,
6388
6435
  config: buildConfig(req, baseInstructionsFile),
6389
6436
  secretEnv: codexSecretEnv(req)
6390
6437
  };
@@ -6400,12 +6447,26 @@ function codexSecretEnv(req) {
6400
6447
  }
6401
6448
  return Object.keys(env).length > 0 ? env : null;
6402
6449
  }
6403
- function buildInput(req, promptRidesInput) {
6450
+ var 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.";
6451
+ var 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.";
6452
+ function buildInput(req, segments, preamble, resumed) {
6404
6453
  const userText = req.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
6405
6454
  const body = userText.trim().length > 0 ? userText : req.prompt;
6406
- if (!promptRidesInput) return body;
6407
- const system = req.systemPrompt.trim();
6408
- return system.length > 0 ? `${system}
6455
+ const standing = [];
6456
+ if (!resumed) {
6457
+ if (preamble.core) standing.push(joinStanding(segments).trim());
6458
+ } else {
6459
+ if (preamble.core) standing.push(`${CORE_REPLACES}
6460
+
6461
+ ${segments.core.trim()}`);
6462
+ if (preamble.conversation) {
6463
+ standing.push(`${CONVERSATION_REPLACES}
6464
+
6465
+ ${segments.conversation.trim()}`.trim());
6466
+ }
6467
+ }
6468
+ const lead = standing.filter((s) => s.length > 0);
6469
+ return lead.length > 0 ? `${lead.join("\n\n---\n\n")}
6409
6470
 
6410
6471
  ${body}` : body;
6411
6472
  }
@@ -6617,7 +6678,6 @@ function createCodexAdapter(deps = {}) {
6617
6678
  const directory = req.local.cwd ?? "";
6618
6679
  const decision = decideResume3(req.session, directory);
6619
6680
  const resumeThreadId = "resume" in decision ? decision.resume : null;
6620
- const threadPromptFingerprint = "resume" in decision ? decision.promptFingerprint : null;
6621
6681
  const threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
6622
6682
  if ("fresh" in decision && decision.reason === "cwd_mismatch") {
6623
6683
  deps.onWarn?.(
@@ -6634,6 +6694,32 @@ function createCodexAdapter(deps = {}) {
6634
6694
  degraded = false;
6635
6695
  }
6636
6696
  }
6697
+ let compactions = 0;
6698
+ let threadStanding = null;
6699
+ if ("resume" in decision) {
6700
+ compactions = deps.countCompactions ? await deps.countCompactions(decision.resume) : null;
6701
+ if (signal.aborted) return;
6702
+ if (compactions === null) {
6703
+ deps.onWarn?.(
6704
+ "codex adapter: the thread's compaction count could not be read \u2014 re-sending the standing instructions (ENG210)",
6705
+ { threadId: decision.resume }
6706
+ );
6707
+ } else if (decision.compactions === compactions) {
6708
+ threadStanding = {
6709
+ core: decision.promptFingerprint,
6710
+ conversation: decision.conversationFingerprint
6711
+ };
6712
+ } else if (decision.compactions !== null && compactions > decision.compactions) {
6713
+ deps.onWarn?.(
6714
+ "codex adapter: the thread compacted since the last turn \u2014 re-sending the standing instructions (ENG210)",
6715
+ {
6716
+ threadId: decision.resume,
6717
+ compactions,
6718
+ compactionsSince: compactions - decision.compactions
6719
+ }
6720
+ );
6721
+ }
6722
+ }
6637
6723
  let instructions = null;
6638
6724
  if (!req.policy.hostFs) {
6639
6725
  if (!deps.writeInstructionsFile) {
@@ -6678,7 +6764,7 @@ function createCodexAdapter(deps = {}) {
6678
6764
  req,
6679
6765
  resumeThreadId,
6680
6766
  instructions?.path ?? null,
6681
- threadPromptFingerprint,
6767
+ threadStanding,
6682
6768
  images?.paths ?? []
6683
6769
  );
6684
6770
  if (spec.model === null) {
@@ -6693,10 +6779,18 @@ function createCodexAdapter(deps = {}) {
6693
6779
  cwd: req.local.cwd,
6694
6780
  resumedThreadId: resumeThreadId,
6695
6781
  degraded,
6696
- // Which prompt the thread carries once this turn lands. If we sent one, it's that one — the
6697
- // later copy is what the model reads. If we withheld it, the thread carries what it did. Read
6698
- // off the spec, so the record can't disagree with what was sent.
6699
- promptFingerprint: spec.promptRidesInput ? spec.promptFingerprint : threadPromptFingerprint,
6782
+ // Which segments the thread carries once this turn lands. A segment sent is that one —
6783
+ // the later copy is what the model reads. One withheld is what the thread had. Read off
6784
+ // the spec, so the record can't disagree with what was sent.
6785
+ promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
6786
+ conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
6787
+ compactions,
6788
+ // What the thread held before this turn, for the write at `thread.started`.
6789
+ heldStanding: {
6790
+ promptFingerprint: threadStanding?.core ?? null,
6791
+ conversationFingerprint: threadStanding?.conversation ?? null,
6792
+ compactions: threadStanding ? compactions : null
6793
+ },
6700
6794
  storedState: req.session,
6701
6795
  usageBaseline: threadUsageBaseline,
6702
6796
  // Codex reports no model in-stream, so record what the thread was started with (null on the
@@ -6796,11 +6890,21 @@ var OP_FAILURE = {
6796
6890
  message: "files.read returned more than the result ceiling"
6797
6891
  }
6798
6892
  };
6799
- var sessionEvent3 = (threadId, cwd = DIR2, degraded = false, promptFingerprint = fingerprintPrompt(PROMPT), usageBaseline = null) => ({
6893
+ var startedSession = (threadId, cwd = DIR2, degraded = false) => ({
6800
6894
  type: "session",
6801
- state: encodeSession3({ threadId, cwd, promptFingerprint, usageBaseline }),
6895
+ state: encodeSession3({ threadId, cwd, promptFingerprint: null, usageBaseline: null }),
6802
6896
  ...degraded ? { degraded: true } : {}
6803
6897
  });
6898
+ var closingSession = (threadId, usageBaseline = null) => ({
6899
+ type: "session",
6900
+ state: encodeSession3({
6901
+ threadId,
6902
+ cwd: DIR2,
6903
+ promptFingerprint: fingerprintPrompt(PROMPT),
6904
+ usageBaseline,
6905
+ compactions: 0
6906
+ })
6907
+ });
6804
6908
  var CODEX_CONFORMANCE_FIXTURES = [
6805
6909
  {
6806
6910
  // A plain text reply: the held agent message seals as the terminal final at `turn.completed`;
@@ -6823,11 +6927,11 @@ var CODEX_CONFORMANCE_FIXTURES = [
6823
6927
  })
6824
6928
  ],
6825
6929
  expected: [
6826
- sessionEvent3(NEW_THREAD_ID),
6930
+ startedSession(NEW_THREAD_ID),
6827
6931
  { type: "text", body: "Hello there.", terminal: true },
6828
6932
  // The closing session write — the thread's ending totals, recorded as the baseline the next turn
6829
6933
  // subtracts. A fresh thread, so the `result` usage below is the frame verbatim.
6830
- sessionEvent3(NEW_THREAD_ID, DIR2, false, fingerprintPrompt(PROMPT), {
6934
+ closingSession(NEW_THREAD_ID, {
6831
6935
  inputTokens: 100,
6832
6936
  outputTokens: 50,
6833
6937
  cacheReadTokens: 20,
@@ -6874,7 +6978,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6874
6978
  turnCompleted()
6875
6979
  ],
6876
6980
  expected: [
6877
- sessionEvent3(NEW_THREAD_ID),
6981
+ startedSession(NEW_THREAD_ID),
6878
6982
  { type: "text", body: "Reading your notes.", terminal: false },
6879
6983
  {
6880
6984
  type: "tool",
@@ -6894,6 +6998,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6894
6998
  result: [{ type: "text", text: "sleep early" }]
6895
6999
  },
6896
7000
  { type: "text", body: "Here's the summary.", terminal: true },
7001
+ closingSession(NEW_THREAD_ID),
6897
7002
  { type: "result", ok: true }
6898
7003
  ]
6899
7004
  },
@@ -6927,7 +7032,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6927
7032
  turnCompleted()
6928
7033
  ],
6929
7034
  expected: [
6930
- sessionEvent3(NEW_THREAD_ID),
7035
+ startedSession(NEW_THREAD_ID),
6931
7036
  {
6932
7037
  type: "tool",
6933
7038
  id: "query-success",
@@ -6945,6 +7050,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6945
7050
  input: { op: "files.read", args: { path: "/ABOUT.md" } },
6946
7051
  result: OP_SUCCESS
6947
7052
  },
7053
+ closingSession(NEW_THREAD_ID),
6948
7054
  { type: "result", ok: true }
6949
7055
  ]
6950
7056
  },
@@ -6980,7 +7086,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6980
7086
  turnCompleted()
6981
7087
  ],
6982
7088
  expected: [
6983
- sessionEvent3(NEW_THREAD_ID),
7089
+ startedSession(NEW_THREAD_ID),
6984
7090
  {
6985
7091
  type: "tool",
6986
7092
  id: "query-failed",
@@ -6998,6 +7104,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6998
7104
  input: { op: "files.read", args: { path: "/ABOUT.md" } },
6999
7105
  result: OP_FAILURE
7000
7106
  },
7107
+ closingSession(NEW_THREAD_ID),
7001
7108
  { type: "result", ok: true }
7002
7109
  ]
7003
7110
  },
@@ -7021,7 +7128,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7021
7128
  turnCompleted()
7022
7129
  ],
7023
7130
  expected: [
7024
- sessionEvent3(NEW_THREAD_ID),
7131
+ startedSession(NEW_THREAD_ID),
7025
7132
  {
7026
7133
  type: "tool",
7027
7134
  id: "mcp-failed",
@@ -7039,6 +7146,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7039
7146
  input: { q: "receipt" },
7040
7147
  result: "connection closed"
7041
7148
  },
7149
+ closingSession(NEW_THREAD_ID),
7042
7150
  { type: "result", ok: true }
7043
7151
  ]
7044
7152
  },
@@ -7064,7 +7172,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7064
7172
  turnCompleted()
7065
7173
  ],
7066
7174
  expected: [
7067
- sessionEvent3(NEW_THREAD_ID),
7175
+ startedSession(NEW_THREAD_ID),
7068
7176
  {
7069
7177
  type: "tool",
7070
7178
  id: "mcp-image",
@@ -7082,6 +7190,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7082
7190
  input: {},
7083
7191
  result: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }]
7084
7192
  },
7193
+ closingSession(NEW_THREAD_ID),
7085
7194
  { type: "result", ok: true }
7086
7195
  ]
7087
7196
  },
@@ -7122,7 +7231,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7122
7231
  turnCompleted()
7123
7232
  ],
7124
7233
  expected: [
7125
- sessionEvent3(NEW_THREAD_ID),
7234
+ startedSession(NEW_THREAD_ID),
7126
7235
  { type: "tool", id: "t1", name: "list", phase: "start", summary: "", input: {} },
7127
7236
  {
7128
7237
  type: "tool",
@@ -7133,6 +7242,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7133
7242
  input: { path: "/product/tasks" },
7134
7243
  result: [{ type: "text", text: "CT481.md" }]
7135
7244
  },
7245
+ closingSession(NEW_THREAD_ID),
7136
7246
  { type: "result", ok: true }
7137
7247
  ]
7138
7248
  },
@@ -7150,7 +7260,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7150
7260
  turnCompleted()
7151
7261
  ],
7152
7262
  expected: [
7153
- sessionEvent3(NEW_THREAD_ID),
7263
+ startedSession(NEW_THREAD_ID),
7154
7264
  {
7155
7265
  type: "tool",
7156
7266
  id: "ws1",
@@ -7167,6 +7277,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7167
7277
  summary: "best pizza in nyc",
7168
7278
  input: { query: "best pizza in nyc" }
7169
7279
  },
7280
+ closingSession(NEW_THREAD_ID),
7170
7281
  { type: "result", ok: true }
7171
7282
  ]
7172
7283
  },
@@ -7182,7 +7293,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7182
7293
  turnCompleted()
7183
7294
  ],
7184
7295
  expected: [
7185
- sessionEvent3(NEW_THREAD_ID),
7296
+ startedSession(NEW_THREAD_ID),
7186
7297
  {
7187
7298
  type: "tool",
7188
7299
  id: "ws2",
@@ -7199,6 +7310,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7199
7310
  summary: "",
7200
7311
  input: { query: "" }
7201
7312
  },
7313
+ closingSession(NEW_THREAD_ID),
7202
7314
  { type: "result", ok: true }
7203
7315
  ]
7204
7316
  },
@@ -7229,7 +7341,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7229
7341
  turnCompleted()
7230
7342
  ],
7231
7343
  expected: [
7232
- sessionEvent3(NEW_THREAD_ID),
7344
+ startedSession(NEW_THREAD_ID),
7233
7345
  { type: "text", body: "Working on it.", terminal: false },
7234
7346
  {
7235
7347
  type: "tool",
@@ -7261,7 +7373,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7261
7373
  turnFailed("provider exploded")
7262
7374
  ],
7263
7375
  expected: [
7264
- sessionEvent3(NEW_THREAD_ID),
7376
+ startedSession(NEW_THREAD_ID),
7265
7377
  { type: "text", body: "Partial work.", terminal: false },
7266
7378
  { type: "runtime_notice", body: "provider exploded" },
7267
7379
  { type: "result", ok: false, reason: "error:provider exploded" }
@@ -7278,7 +7390,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7278
7390
  fatalError("stream died")
7279
7391
  ],
7280
7392
  expected: [
7281
- sessionEvent3(NEW_THREAD_ID),
7393
+ startedSession(NEW_THREAD_ID),
7282
7394
  { type: "text", body: "Starting.", terminal: false },
7283
7395
  { type: "runtime_notice", body: "stream died" },
7284
7396
  { type: "result", ok: false, reason: "error:stream died" }
@@ -7291,7 +7403,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7291
7403
  request: makeRequest3(),
7292
7404
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
7293
7405
  expected: [
7294
- sessionEvent3(NEW_THREAD_ID),
7406
+ startedSession(NEW_THREAD_ID),
7295
7407
  { type: "runtime_notice", body: "429 Too Many Requests" },
7296
7408
  { type: "result", ok: false, reason: "rate_limited" }
7297
7409
  ]
@@ -7301,7 +7413,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7301
7413
  request: makeRequest3(),
7302
7414
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("503 Service Unavailable")],
7303
7415
  expected: [
7304
- sessionEvent3(NEW_THREAD_ID),
7416
+ startedSession(NEW_THREAD_ID),
7305
7417
  { type: "runtime_notice", body: "503 Service Unavailable" },
7306
7418
  { type: "result", ok: false, reason: "server_error" }
7307
7419
  ]
@@ -7314,7 +7426,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7314
7426
  turnFailed("You've hit your session limit \xB7 resets 8:20pm (UTC)")
7315
7427
  ],
7316
7428
  expected: [
7317
- sessionEvent3(NEW_THREAD_ID),
7429
+ startedSession(NEW_THREAD_ID),
7318
7430
  {
7319
7431
  type: "runtime_notice",
7320
7432
  body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
@@ -7327,7 +7439,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7327
7439
  request: makeRequest3(),
7328
7440
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("401 Unauthorized: invalid api key")],
7329
7441
  expected: [
7330
- sessionEvent3(NEW_THREAD_ID),
7442
+ startedSession(NEW_THREAD_ID),
7331
7443
  { type: "runtime_notice", body: "401 Unauthorized: invalid api key" },
7332
7444
  { type: "result", ok: false, reason: "auth_expired" }
7333
7445
  ]
@@ -7339,7 +7451,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7339
7451
  request: makeRequest3(),
7340
7452
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
7341
7453
  expected: [
7342
- sessionEvent3(NEW_THREAD_ID),
7454
+ startedSession(NEW_THREAD_ID),
7343
7455
  { type: "runtime_notice", body: "something local broke" },
7344
7456
  { type: "result", ok: false, reason: "error:something local broke" }
7345
7457
  ]
@@ -7352,7 +7464,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7352
7464
  turnFailed(` ${BOUNDED_NOTICE2}${"y".repeat(20)} `)
7353
7465
  ],
7354
7466
  expected: [
7355
- sessionEvent3(NEW_THREAD_ID),
7467
+ startedSession(NEW_THREAD_ID),
7356
7468
  { type: "runtime_notice", body: BOUNDED_NOTICE2 },
7357
7469
  { type: "result", ok: false, reason: "usage_capped" }
7358
7470
  ]
@@ -7363,26 +7475,32 @@ var CODEX_CONFORMANCE_FIXTURES = [
7363
7475
  name: "empty failure text emits no runtime notice",
7364
7476
  request: makeRequest3(),
7365
7477
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
7366
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "error:unknown" }]
7478
+ expected: [
7479
+ startedSession(NEW_THREAD_ID),
7480
+ { type: "result", ok: false, reason: "error:unknown" }
7481
+ ]
7367
7482
  },
7368
7483
  {
7369
7484
  name: "whitespace-only failure text emits no runtime notice",
7370
7485
  request: makeRequest3(),
7371
7486
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed(" ")],
7372
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "error: " }]
7487
+ expected: [startedSession(NEW_THREAD_ID), { type: "result", ok: false, reason: "error: " }]
7373
7488
  },
7374
7489
  {
7375
7490
  name: "bare status token emits no runtime notice",
7376
7491
  request: makeRequest3(),
7377
7492
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429")],
7378
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "rate_limited" }]
7493
+ expected: [
7494
+ startedSession(NEW_THREAD_ID),
7495
+ { type: "result", ok: false, reason: "rate_limited" }
7496
+ ]
7379
7497
  },
7380
7498
  {
7381
7499
  name: "single-word provider sentence emits a runtime notice",
7382
7500
  request: makeRequest3(),
7383
7501
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("Unavailable.")],
7384
7502
  expected: [
7385
- sessionEvent3(NEW_THREAD_ID),
7503
+ startedSession(NEW_THREAD_ID),
7386
7504
  { type: "runtime_notice", body: "Unavailable." },
7387
7505
  { type: "result", ok: false, reason: "error:Unavailable." }
7388
7506
  ]
@@ -7410,7 +7528,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7410
7528
  turnCompleted()
7411
7529
  ],
7412
7530
  expected: [
7413
- sessionEvent3(NEW_THREAD_ID),
7531
+ startedSession(NEW_THREAD_ID),
7414
7532
  {
7415
7533
  type: "result",
7416
7534
  ok: false,
@@ -7430,8 +7548,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
7430
7548
  turnCompleted()
7431
7549
  ],
7432
7550
  expected: [
7433
- sessionEvent3(NEW_THREAD_ID),
7551
+ startedSession(NEW_THREAD_ID),
7434
7552
  { type: "text", body: "All good now.", terminal: true },
7553
+ closingSession(NEW_THREAD_ID),
7435
7554
  { type: "result", ok: true }
7436
7555
  ]
7437
7556
  },
@@ -7443,7 +7562,11 @@ var CODEX_CONFORMANCE_FIXTURES = [
7443
7562
  name: "zero-content success (empty runtime result)",
7444
7563
  request: makeRequest3(),
7445
7564
  nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
7446
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: true }]
7565
+ expected: [
7566
+ startedSession(NEW_THREAD_ID),
7567
+ closingSession(NEW_THREAD_ID),
7568
+ { type: "result", ok: true }
7569
+ ]
7447
7570
  },
7448
7571
  {
7449
7572
  // Empty-final: a clean turn that ended on a tool call with no closing message.
@@ -7468,7 +7591,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7468
7591
  turnCompleted()
7469
7592
  ],
7470
7593
  expected: [
7471
- sessionEvent3(NEW_THREAD_ID),
7594
+ startedSession(NEW_THREAD_ID),
7472
7595
  {
7473
7596
  type: "tool",
7474
7597
  id: "t1",
@@ -7486,6 +7609,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7486
7609
  input: { changes: [{ path: "a.md", kind: "add" }] },
7487
7610
  result: [{ path: "a.md", kind: "add" }]
7488
7611
  },
7612
+ closingSession(NEW_THREAD_ID),
7489
7613
  { type: "result", ok: true }
7490
7614
  ]
7491
7615
  },
@@ -7501,9 +7625,10 @@ var CODEX_CONFORMANCE_FIXTURES = [
7501
7625
  turnCompleted()
7502
7626
  ],
7503
7627
  expected: [
7504
- sessionEvent3(NEW_THREAD_ID),
7628
+ startedSession(NEW_THREAD_ID),
7505
7629
  { type: "thinking", text: "Let me check the notes." },
7506
7630
  { type: "text", body: "All set.", terminal: true },
7631
+ closingSession(NEW_THREAD_ID),
7507
7632
  { type: "result", ok: true }
7508
7633
  ]
7509
7634
  },
@@ -7521,8 +7646,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
7521
7646
  }),
7522
7647
  nativeStream: [threadStarted(NEW_THREAD_ID), agentMessage("m1", "Done."), turnCompleted()],
7523
7648
  expected: [
7524
- sessionEvent3(NEW_THREAD_ID),
7649
+ startedSession(NEW_THREAD_ID),
7525
7650
  { type: "text", body: "Done.", terminal: true },
7651
+ closingSession(NEW_THREAD_ID),
7526
7652
  {
7527
7653
  type: "result",
7528
7654
  ok: true,
@@ -7544,7 +7670,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
7544
7670
  threadId: "th1",
7545
7671
  cwd: DIR2,
7546
7672
  promptFingerprint: fingerprintPrompt(PROMPT),
7547
- usageBaseline: null
7673
+ usageBaseline: null,
7674
+ compactions: 0
7548
7675
  })
7549
7676
  }),
7550
7677
  nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
@@ -7566,8 +7693,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
7566
7693
  }),
7567
7694
  nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
7568
7695
  expected: [
7569
- sessionEvent3("th1"),
7696
+ startedSession("th1"),
7570
7697
  { type: "text", body: "Back again.", terminal: true },
7698
+ closingSession("th1"),
7571
7699
  { type: "result", ok: true }
7572
7700
  ]
7573
7701
  },
@@ -7589,8 +7717,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
7589
7717
  }),
7590
7718
  nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
7591
7719
  expected: [
7592
- sessionEvent3("th1"),
7720
+ startedSession("th1"),
7593
7721
  { type: "text", body: "Back again.", terminal: true },
7722
+ closingSession("th1"),
7594
7723
  { type: "result", ok: true }
7595
7724
  ]
7596
7725
  },
@@ -7616,8 +7745,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
7616
7745
  turnCompleted()
7617
7746
  ],
7618
7747
  expected: [
7619
- sessionEvent3(NEW_THREAD_ID, DIR2, true),
7748
+ startedSession(NEW_THREAD_ID, DIR2, true),
7620
7749
  { type: "text", body: "Fresh start.", terminal: true },
7750
+ closingSession(NEW_THREAD_ID),
7621
7751
  { type: "result", ok: true }
7622
7752
  ]
7623
7753
  }
@@ -7699,7 +7829,7 @@ var ConnectorHealthStore = class {
7699
7829
  // src/dispatcher.ts
7700
7830
  import { randomUUID as randomUUID4 } from "crypto";
7701
7831
  import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
7702
- import { join as join18 } from "path";
7832
+ import { join as join19 } from "path";
7703
7833
 
7704
7834
  // src/runner/launch.ts
7705
7835
  import { createInterface } from "readline";
@@ -9097,6 +9227,7 @@ function buildCompanionTurnRequest(params) {
9097
9227
  const { turnContext: t } = params;
9098
9228
  return {
9099
9229
  systemPrompt: t.systemPrompt,
9230
+ ...t.standing ? { standing: t.standing } : {},
9100
9231
  prompt: t.prompt,
9101
9232
  content: t.content,
9102
9233
  config: t.config,
@@ -9208,6 +9339,62 @@ function message(err) {
9208
9339
  return err instanceof Error ? err.message : String(err);
9209
9340
  }
9210
9341
 
9342
+ // src/codex-compactions.ts
9343
+ import { createReadStream } from "fs";
9344
+ import { readdir } from "fs/promises";
9345
+ import { homedir as homedir6 } from "os";
9346
+ import { join as join16 } from "path";
9347
+ import { createInterface as createInterface2 } from "readline";
9348
+ var DAY_MS = 864e5;
9349
+ var UUID_V7 = /^([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9350
+ var RECORD_HEAD_CHARS = 200;
9351
+ function createCodexCompactionCounter(codexHome = process.env.CODEX_HOME || join16(homedir6(), ".codex")) {
9352
+ return async (threadId) => {
9353
+ try {
9354
+ const rollout = await findRollout(codexHome, threadId);
9355
+ return rollout ? await countCompactions(rollout) : null;
9356
+ } catch {
9357
+ return null;
9358
+ }
9359
+ };
9360
+ }
9361
+ async function findRollout(codexHome, threadId) {
9362
+ const id = UUID_V7.exec(threadId);
9363
+ if (!id) return null;
9364
+ const createdAt = parseInt(`${id[1]}${id[2]}`, 16);
9365
+ const suffix = `-${threadId}.jsonl`;
9366
+ for (const offset of [0, -DAY_MS, DAY_MS]) {
9367
+ const day = new Date(createdAt + offset);
9368
+ const dir2 = join16(
9369
+ codexHome,
9370
+ "sessions",
9371
+ String(day.getUTCFullYear()),
9372
+ String(day.getUTCMonth() + 1).padStart(2, "0"),
9373
+ String(day.getUTCDate()).padStart(2, "0")
9374
+ );
9375
+ let names;
9376
+ try {
9377
+ names = await readdir(dir2);
9378
+ } catch {
9379
+ continue;
9380
+ }
9381
+ const name = names.find((n) => n.startsWith("rollout-") && n.endsWith(suffix));
9382
+ if (name) return join16(dir2, name);
9383
+ }
9384
+ return null;
9385
+ }
9386
+ async function countCompactions(rollout) {
9387
+ const lines2 = createInterface2({
9388
+ input: createReadStream(rollout, { encoding: "utf8" }),
9389
+ crlfDelay: Infinity
9390
+ });
9391
+ let count = 0;
9392
+ for await (const line of lines2) {
9393
+ if (line.slice(0, RECORD_HEAD_CHARS).includes('"type":"compacted"')) count += 1;
9394
+ }
9395
+ return count;
9396
+ }
9397
+
9211
9398
  // src/turn-seq-floor.ts
9212
9399
  var SeqFloorUnavailable = class extends Error {
9213
9400
  constructor(detail) {
@@ -9305,15 +9492,15 @@ function reportFailure(config, body) {
9305
9492
 
9306
9493
  // src/prepared.ts
9307
9494
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
9308
- import { join as join16 } from "path";
9495
+ import { join as join17 } from "path";
9309
9496
  function dirFor(workspaceId) {
9310
- return join16(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
9497
+ return join17(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
9311
9498
  }
9312
9499
  function conversationDir(workspaceId, conversationId) {
9313
- return join16(dirFor(workspaceId), encodeURIComponent(conversationId));
9500
+ return join17(dirFor(workspaceId), encodeURIComponent(conversationId));
9314
9501
  }
9315
9502
  function pathFor2(workspaceId, conversationId, agentId) {
9316
- return join16(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
9503
+ return join17(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
9317
9504
  }
9318
9505
  function readPrepared(workspaceId, conversationId, agentId) {
9319
9506
  const path = pathFor2(workspaceId, conversationId, agentId);
@@ -9345,9 +9532,9 @@ function clearPrepared(workspaceId, conversationId, agentId) {
9345
9532
 
9346
9533
  // src/transcript-writer.ts
9347
9534
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
9348
- import { basename as basename3, dirname as dirname7, join as join17 } from "path";
9535
+ import { basename as basename3, dirname as dirname7, join as join18 } from "path";
9349
9536
  function transcriptsDir() {
9350
- return join17(runnerStateDir(), "transcripts");
9537
+ return join18(runnerStateDir(), "transcripts");
9351
9538
  }
9352
9539
  var RETAIN = 200;
9353
9540
  var ANOMALY_RETAIN = 50;
@@ -9357,7 +9544,7 @@ var TranscriptWriter = class {
9357
9544
  onWarn;
9358
9545
  constructor(dir2, meta, onWarn) {
9359
9546
  this.onWarn = onWarn;
9360
- this.path = join17(dir2, fileName(meta));
9547
+ this.path = join18(dir2, fileName(meta));
9361
9548
  try {
9362
9549
  mkdirSync8(dir2, { recursive: true });
9363
9550
  try {
@@ -9386,10 +9573,10 @@ var TranscriptWriter = class {
9386
9573
  preserveAnomaly() {
9387
9574
  if (this.broken) return;
9388
9575
  try {
9389
- const dir2 = join17(dirname7(this.path), "anomalies");
9576
+ const dir2 = join18(dirname7(this.path), "anomalies");
9390
9577
  mkdirSync8(dir2, { recursive: true, mode: 448 });
9391
9578
  chmodSync4(dir2, 448);
9392
- const target = join17(dir2, basename3(this.path));
9579
+ const target = join18(dir2, basename3(this.path));
9393
9580
  copyFileSync(this.path, target);
9394
9581
  chmodSync4(target, 384);
9395
9582
  pruneOld(dir2, ANOMALY_RETAIN);
@@ -9435,7 +9622,7 @@ function pruneOld(dir2, retain) {
9435
9622
  const drop = files.sort().slice(0, files.length - retain);
9436
9623
  for (const f of drop) {
9437
9624
  try {
9438
- rmSync6(join17(dir2, f), { force: true });
9625
+ rmSync6(join18(dir2, f), { force: true });
9439
9626
  } catch {
9440
9627
  }
9441
9628
  }
@@ -10344,6 +10531,8 @@ var TurnExecution = class {
10344
10531
  turnId: this.turnId,
10345
10532
  warn: (meta, msg) => turnLog.warn(meta, msg)
10346
10533
  }),
10534
+ // Whether a resumed thread compacted since its standing preamble was sent.
10535
+ countCompactions: createCodexCompactionCounter(),
10347
10536
  ...codexPath ? { transport: createSdkCodexTransport({ codexPathOverride: codexPath }) } : {}
10348
10537
  })
10349
10538
  );
@@ -10404,6 +10593,9 @@ var TurnExecution = class {
10404
10593
  );
10405
10594
  return {
10406
10595
  systemPrompt: context.systemPrompt,
10596
+ // Replaced even when absent, so the segments can't outlive the
10597
+ // `systemPrompt` they were split from.
10598
+ standing: context.standing,
10407
10599
  prompt: context.prompt,
10408
10600
  content: context.content
10409
10601
  };
@@ -10815,7 +11007,7 @@ function checkoutState(cwd) {
10815
11007
  if (entries.length === 0) {
10816
11008
  return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
10817
11009
  }
10818
- const gitPath = join18(cwd, ".git");
11010
+ const gitPath = join19(cwd, ".git");
10819
11011
  if (!existsSync12(gitPath)) return { ok: true, reason: "usable" };
10820
11012
  let stat;
10821
11013
  try {
@@ -10823,7 +11015,7 @@ function checkoutState(cwd) {
10823
11015
  } catch (error) {
10824
11016
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
10825
11017
  }
10826
- if (stat.isDirectory() && !existsSync12(join18(gitPath, "HEAD")))
11018
+ if (stat.isDirectory() && !existsSync12(join19(gitPath, "HEAD")))
10827
11019
  return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
10828
11020
  return { ok: true, reason: "usable" };
10829
11021
  }
@@ -11030,7 +11222,7 @@ function createLocalProcessProvider(opts = {}) {
11030
11222
  import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
11031
11223
  import { constants as constants3, tmpdir as tmpdir3 } from "os";
11032
11224
  import {
11033
- createReadStream,
11225
+ createReadStream as createReadStream2,
11034
11226
  createWriteStream as createWriteStream2,
11035
11227
  existsSync as existsSync15,
11036
11228
  mkdirSync as mkdirSync10,
@@ -11041,7 +11233,7 @@ import {
11041
11233
  statSync as statSync4,
11042
11234
  writeFileSync as writeFileSync6
11043
11235
  } from "fs";
11044
- import { join as join21 } from "path";
11236
+ import { join as join22 } from "path";
11045
11237
  import { Writable } from "stream";
11046
11238
 
11047
11239
  // src/providers/docker-cli.ts
@@ -11235,7 +11427,7 @@ import {
11235
11427
  renameSync as renameSync5,
11236
11428
  rmSync as rmSync8
11237
11429
  } from "fs";
11238
- import { join as join19 } from "path";
11430
+ import { join as join20 } from "path";
11239
11431
  function routes(t) {
11240
11432
  const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
11241
11433
  const agent = `${conv}/participants/agents/${t.agentId}`;
@@ -11281,7 +11473,7 @@ function publishStaged(from, to, move = atomicMove) {
11281
11473
  const staged = readdirSync3(from).filter((n) => n.endsWith(".json")).map((name) => {
11282
11474
  let entry = {};
11283
11475
  try {
11284
- entry = JSON.parse(readFileSync9(join19(from, name), "utf8"));
11476
+ entry = JSON.parse(readFileSync9(join20(from, name), "utf8"));
11285
11477
  } catch {
11286
11478
  }
11287
11479
  return { name, entry };
@@ -11294,7 +11486,7 @@ function publishStaged(from, to, move = atomicMove) {
11294
11486
  let published = 0;
11295
11487
  for (const { name } of staged) {
11296
11488
  try {
11297
- move(join19(from, name), join19(to, name));
11489
+ move(join20(from, name), join20(to, name));
11298
11490
  } catch {
11299
11491
  return { published, complete: false };
11300
11492
  }
@@ -11307,7 +11499,7 @@ function publishStaged(from, to, move = atomicMove) {
11307
11499
  // src/providers/runner-base.ts
11308
11500
  import { createHash as createHash3 } from "crypto";
11309
11501
  import { existsSync as existsSync14, realpathSync as realpathSync4 } from "fs";
11310
- import { dirname as dirname8, join as join20, sep as sep3 } from "path";
11502
+ import { dirname as dirname8, join as join21, sep as sep3 } from "path";
11311
11503
  import { fileURLToPath as fileURLToPath5 } from "url";
11312
11504
  var COMPANION_MOUNT = "/opt/cabane/companion";
11313
11505
  var BASE_DOCKERFILE = `FROM node:22-slim
@@ -11355,11 +11547,11 @@ async function ensureBaseImage(docker, image, log) {
11355
11547
  function ownInstall(moduleFile = fileURLToPath5(import.meta.url)) {
11356
11548
  let dir2 = dirname8(moduleFile);
11357
11549
  for (let i = 0; i < 4; i++) {
11358
- if (existsSync14(join20(dir2, "dist", "cli.js")) && existsSync14(join20(dir2, "package.json"))) break;
11550
+ if (existsSync14(join21(dir2, "dist", "cli.js")) && existsSync14(join21(dir2, "package.json"))) break;
11359
11551
  dir2 = dirname8(dir2);
11360
11552
  }
11361
- if (!existsSync14(join20(dir2, "dist", "cli.js"))) return null;
11362
- const sdk = join20(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
11553
+ if (!existsSync14(join21(dir2, "dist", "cli.js"))) return null;
11554
+ const sdk = join21(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
11363
11555
  if (!existsSync14(sdk)) return null;
11364
11556
  const root = realpathSync4(dir2);
11365
11557
  return realpathSync4(sdk).startsWith(root + sep3) ? root : null;
@@ -11498,39 +11690,39 @@ function networkCreateError(stderr) {
11498
11690
  }
11499
11691
  function sessionArchivePath(deviceDir, attemptId) {
11500
11692
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
11501
- return join21(deviceDir, "attempts", attemptId, "session.tar");
11693
+ return join22(deviceDir, "attempts", attemptId, "session.tar");
11502
11694
  }
11503
11695
  function pruneOtherAttempts(deviceDir, pair, keep) {
11504
- const root = join21(deviceDir, "attempts");
11696
+ const root = join22(deviceDir, "attempts");
11505
11697
  if (!existsSync15(root)) return;
11506
11698
  for (const id of readdirSync4(root)) {
11507
11699
  if (id === keep || !UUID_RE.test(id)) continue;
11508
11700
  let pairOf = "";
11509
11701
  try {
11510
- pairOf = readFileSync10(join21(root, id, "pair"), "utf8");
11702
+ pairOf = readFileSync10(join22(root, id, "pair"), "utf8");
11511
11703
  } catch {
11512
11704
  continue;
11513
11705
  }
11514
11706
  if (pairOf === `${pair.conversationId}/${pair.agentId}`) {
11515
- rmSync9(join21(root, id), { recursive: true, force: true });
11707
+ rmSync9(join22(root, id), { recursive: true, force: true });
11516
11708
  }
11517
11709
  }
11518
11710
  }
11519
11711
  var IDLE_SESSION_ARCHIVE_MS = 30 * 24 * 60 * 6e4;
11520
11712
  function pruneIdleSessionArchives(deviceDir, now = Date.now(), maxAgeMs = IDLE_SESSION_ARCHIVE_MS) {
11521
- const root = join21(deviceDir, "attempts");
11713
+ const root = join22(deviceDir, "attempts");
11522
11714
  if (!existsSync15(root)) return [];
11523
11715
  const removed = [];
11524
11716
  for (const id of readdirSync4(root)) {
11525
11717
  if (!UUID_RE.test(id)) continue;
11526
11718
  let kept;
11527
11719
  try {
11528
- kept = statSync4(join21(root, id)).mtimeMs;
11720
+ kept = statSync4(join22(root, id)).mtimeMs;
11529
11721
  } catch {
11530
11722
  continue;
11531
11723
  }
11532
11724
  if (now - kept < maxAgeMs) continue;
11533
- rmSync9(join21(root, id), { recursive: true, force: true });
11725
+ rmSync9(join22(root, id), { recursive: true, force: true });
11534
11726
  removed.push(id);
11535
11727
  }
11536
11728
  return removed;
@@ -11731,11 +11923,11 @@ function createDockerProvider(opts) {
11731
11923
  await must(["network", "disconnect", "-f", other, name], "moving the container");
11732
11924
  if (others.includes(sharedNetwork)) await retireSharedNetwork();
11733
11925
  };
11734
- const socketRoot = opts.socketRoot ?? join21(
11926
+ const socketRoot = opts.socketRoot ?? join22(
11735
11927
  process.env.XDG_RUNTIME_DIR && existsSync15(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir3(),
11736
11928
  `cabane-runners-${opts.deviceKey}`
11737
11929
  );
11738
- const socketDir = (name) => join21(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
11930
+ const socketDir = (name) => join22(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
11739
11931
  const create = async (name, attempt) => {
11740
11932
  mkdirSync10(socketRoot, { recursive: true, mode: 448 });
11741
11933
  mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
@@ -11877,7 +12069,7 @@ function createDockerProvider(opts) {
11877
12069
  }
11878
12070
  };
11879
12071
  const removeIn = (name, from, names) => names.length ? execIn(name, ["sh", "-c", 'cd "$1" && shift && rm -f -- "$@"', "sh", from, ...names]) : Promise.resolve(null);
11880
- const pendingPath = (name) => join21(opts.deviceDir, "runners-pending", `${name}.json`);
12072
+ const pendingPath = (name) => join22(opts.deviceDir, "runners-pending", `${name}.json`);
11881
12073
  const readPending = (name) => {
11882
12074
  try {
11883
12075
  const ids = JSON.parse(readFileSync10(pendingPath(name), "utf8"));
@@ -11891,12 +12083,12 @@ function createDockerProvider(opts) {
11891
12083
  rmSync9(pendingPath(name), { force: true });
11892
12084
  return;
11893
12085
  }
11894
- mkdirSync10(join21(opts.deviceDir, "runners-pending"), { recursive: true });
12086
+ mkdirSync10(join22(opts.deviceDir, "runners-pending"), { recursive: true });
11895
12087
  writeFileSync6(pendingPath(name), JSON.stringify([...new Set(ids)]));
11896
12088
  };
11897
12089
  const keepSession = async (name, attempt, attemptId) => {
11898
12090
  const archive = sessionArchivePath(opts.deviceDir, attemptId);
11899
- mkdirSync10(join21(archive, ".."), { recursive: true });
12091
+ mkdirSync10(join22(archive, ".."), { recursive: true });
11900
12092
  const partial = `${archive}.${process.pid}.partial`;
11901
12093
  const file = new CappedFile(partial, sessionCap);
11902
12094
  const r = await execIn(
@@ -11913,7 +12105,7 @@ function createDockerProvider(opts) {
11913
12105
  );
11914
12106
  if (r.code === 0 && !file.over) {
11915
12107
  renameSync6(partial, archive);
11916
- writeFileSync6(join21(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
12108
+ writeFileSync6(join22(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
11917
12109
  pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
11918
12110
  await writeMarker(name, attemptId);
11919
12111
  log.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
@@ -11927,7 +12119,7 @@ function createDockerProvider(opts) {
11927
12119
  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"
11928
12120
  );
11929
12121
  };
11930
- const stagingDir = (name) => join21(opts.deviceDir, "runners-pending", `${name}.outbox`);
12122
+ const stagingDir = (name) => join22(opts.deviceDir, "runners-pending", `${name}.outbox`);
11931
12123
  const ensureRunning = async (name) => {
11932
12124
  const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
11933
12125
  if (seen.code !== 0) return false;
@@ -11935,7 +12127,7 @@ function createDockerProvider(opts) {
11935
12127
  return (await docker.run(["start", name])).code === 0;
11936
12128
  };
11937
12129
  const dropArchive = (attemptId) => {
11938
- rmSync9(join21(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
12130
+ rmSync9(join22(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
11939
12131
  recursive: true,
11940
12132
  force: true
11941
12133
  });
@@ -12004,7 +12196,7 @@ function createDockerProvider(opts) {
12004
12196
  if (seen.stdout.trim().split(" ")[0] !== "true")
12005
12197
  await must(["start", name], "starting the container");
12006
12198
  const git = await serveGitCredentials({
12007
- socketPath: join21(socketDir(name), "git.sock"),
12199
+ socketPath: join22(socketDir(name), "git.sock"),
12008
12200
  hosts: opts.gitHosts,
12009
12201
  fill: opts.gitFill,
12010
12202
  log
@@ -12040,7 +12232,7 @@ function createDockerProvider(opts) {
12040
12232
  const archive = sessionArchivePath(opts.deviceDir, attemptId);
12041
12233
  if (existsSync15(archive)) {
12042
12234
  const r = await execIn(name, ["tar", "-x", "-C", CONTAINER_HOME, "-f", "-"], {
12043
- input: createReadStream(archive)
12235
+ input: createReadStream2(archive)
12044
12236
  });
12045
12237
  if (r.code !== 0) {
12046
12238
  log.warn(
@@ -12153,15 +12345,15 @@ function createDockerProvider(opts) {
12153
12345
  const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
12154
12346
  if (transcripts) {
12155
12347
  const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
12156
- if (kept.length) mkdirSync10(join21(opts.hostStateDir, "transcripts"), { recursive: true });
12157
- for (const f of kept) writeFileSync6(join21(opts.hostStateDir, "transcripts", f.name), f.data);
12348
+ if (kept.length) mkdirSync10(join22(opts.hostStateDir, "transcripts"), { recursive: true });
12349
+ for (const f of kept) writeFileSync6(join22(opts.hostStateDir, "transcripts", f.name), f.data);
12158
12350
  await removeIn(
12159
12351
  name,
12160
12352
  `${CONTAINER_STATE}/transcripts`,
12161
12353
  kept.map((f) => f.name)
12162
12354
  );
12163
12355
  }
12164
- const outboxRel = join21("outbox", encodeURIComponent(attempt.agentId));
12356
+ const outboxRel = join22("outbox", encodeURIComponent(attempt.agentId));
12165
12357
  const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
12166
12358
  let unpublished = false;
12167
12359
  if (outbox) {
@@ -12187,7 +12379,7 @@ function createDockerProvider(opts) {
12187
12379
  const staged = stagingDir(name);
12188
12380
  mkdirSync10(staged, { recursive: true });
12189
12381
  writeFileSync6(
12190
- join21(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
12382
+ join22(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
12191
12383
  `${JSON.stringify(entry)}
12192
12384
  `
12193
12385
  );
@@ -12200,7 +12392,7 @@ function createDockerProvider(opts) {
12200
12392
  if (outbox.complete) {
12201
12393
  const published = publishStaged(
12202
12394
  stagingDir(name),
12203
- join21(opts.hostStateDir, outboxRel),
12395
+ join22(opts.hostStateDir, outboxRel),
12204
12396
  opts.moveFile
12205
12397
  );
12206
12398
  queued = published.published;
@@ -12415,8 +12607,8 @@ import {
12415
12607
  } from "fs";
12416
12608
  import { createRequire as createRequire2 } from "module";
12417
12609
  import { isIP } from "net";
12418
- import { homedir as homedir6, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo2 } from "os";
12419
- import { dirname as dirname9, join as join22, sep as sep4 } from "path";
12610
+ import { homedir as homedir7, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo2 } from "os";
12611
+ import { dirname as dirname9, join as join23, sep as sep4 } from "path";
12420
12612
  import { fileURLToPath as fileURLToPath6 } from "url";
12421
12613
  import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
12422
12614
  function accountHome() {
@@ -12513,7 +12705,7 @@ function companionStoreWatch(log) {
12513
12705
  return new CompanionStoreWatch({
12514
12706
  log,
12515
12707
  roots: () => {
12516
- const home = homedir6();
12708
+ const home = homedir7();
12517
12709
  return storeWalkRoots([.../* @__PURE__ */ new Set([home, accountHome() ?? home])], cabaneDir());
12518
12710
  }
12519
12711
  });
@@ -12612,8 +12804,8 @@ function emptyHarnessVersions() {
12612
12804
  }
12613
12805
 
12614
12806
  // src/harness-copy.ts
12615
- import { homedir as homedir7 } from "os";
12616
- import { join as join23 } from "path";
12807
+ import { homedir as homedir8 } from "os";
12808
+ import { join as join24 } from "path";
12617
12809
  var HARNESS_LABELS = {
12618
12810
  "claude-code": "Claude Code",
12619
12811
  codex: "Codex",
@@ -12627,7 +12819,7 @@ function failureText(runtime, reason) {
12627
12819
  case "spawn":
12628
12820
  return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
12629
12821
  case "config":
12630
- return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join23(homedir7(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
12822
+ return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join24(homedir8(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
12631
12823
  case "unreadable":
12632
12824
  return `Couldn't check whether ${label} is signed in: it answered in a way Cabane couldn't read. Try again, and if it keeps happening, update Cabane.`;
12633
12825
  }
@@ -12967,7 +13159,7 @@ import {
12967
13159
  rmSync as rmSync11,
12968
13160
  writeFileSync as writeFileSync8
12969
13161
  } from "fs";
12970
- import { join as join24 } from "path";
13162
+ import { join as join25 } from "path";
12971
13163
  var MAX_ENTRIES = 2e3;
12972
13164
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
12973
13165
  var Outbox = class {
@@ -12980,10 +13172,10 @@ var Outbox = class {
12980
13172
  // Resolved lazily (per call) so tests that swap HOME between
12981
13173
  // cases route writes at the right tmpdir.
12982
13174
  dir() {
12983
- return join24(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
13175
+ return join25(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
12984
13176
  }
12985
13177
  fileFor(turnId, seq) {
12986
- return join24(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
13178
+ return join25(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
12987
13179
  }
12988
13180
  // Persist a commit for later draining. Atomic (temp file + rename) so a
12989
13181
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -13025,7 +13217,7 @@ var Outbox = class {
13025
13217
  const entries = [];
13026
13218
  for (const name of names) {
13027
13219
  if (!name.endsWith(".json")) continue;
13028
- const full = join24(dir2, name);
13220
+ const full = join25(dir2, name);
13029
13221
  try {
13030
13222
  const parsed = JSON.parse(readFileSync12(full, "utf8"));
13031
13223
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -13249,7 +13441,7 @@ var SseSubscriber = class {
13249
13441
  // src/updater.ts
13250
13442
  import { spawn as spawn8 } from "child_process";
13251
13443
  import { copyFileSync as copyFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync16, renameSync as renameSync11, rmSync as rmSync14 } from "fs";
13252
- import { dirname as dirname13, join as join28 } from "path";
13444
+ import { dirname as dirname13, join as join29 } from "path";
13253
13445
 
13254
13446
  // src/boot-rule.ts
13255
13447
  import {
@@ -13261,7 +13453,7 @@ import {
13261
13453
  symlinkSync,
13262
13454
  writeFileSync as writeFileSync9
13263
13455
  } from "fs";
13264
- import { dirname as dirname10, join as join25 } from "path";
13456
+ import { dirname as dirname10, join as join26 } from "path";
13265
13457
  var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
13266
13458
  var MAX_STAGE_ATTEMPTS = 3;
13267
13459
  function superseded(pending, ...running) {
@@ -13294,26 +13486,26 @@ import {
13294
13486
  rmSync as rmSync13,
13295
13487
  symlinkSync as symlinkSync2
13296
13488
  } from "fs";
13297
- import { dirname as dirname11, join as join26 } from "path";
13489
+ import { dirname as dirname11, join as join27 } from "path";
13298
13490
  import { fileURLToPath as fileURLToPath7 } from "url";
13299
13491
  function versionsDir() {
13300
- return join26(cabaneDir(), "versions");
13492
+ return join27(cabaneDir(), "versions");
13301
13493
  }
13302
13494
  function currentLink() {
13303
- return join26(cabaneDir(), "current");
13495
+ return join27(cabaneDir(), "current");
13304
13496
  }
13305
13497
  function bootGuardPath() {
13306
- return join26(cabaneDir(), "boot-guard.mjs");
13498
+ return join27(cabaneDir(), "boot-guard.mjs");
13307
13499
  }
13308
13500
  function versionPackageRoot(version) {
13309
- return join26(versionsDir(), version, "node_modules", "@cabane", "companion");
13501
+ return join27(versionsDir(), version, "node_modules", "@cabane", "companion");
13310
13502
  }
13311
13503
  function ownPackageRoot(moduleUrl = import.meta.url) {
13312
13504
  return dirname11(dirname11(fileURLToPath7(moduleUrl)));
13313
13505
  }
13314
13506
  function packageVersion(root) {
13315
13507
  try {
13316
- const pkg2 = JSON.parse(readFileSync14(join26(root, "package.json"), "utf8"));
13508
+ const pkg2 = JSON.parse(readFileSync14(join27(root, "package.json"), "utf8"));
13317
13509
  return typeof pkg2.version === "string" ? pkg2.version : null;
13318
13510
  } catch {
13319
13511
  return null;
@@ -13322,7 +13514,7 @@ function packageVersion(root) {
13322
13514
  function currentRoot() {
13323
13515
  try {
13324
13516
  const root = realpathSync7(currentLink());
13325
- return existsSync17(join26(root, "dist", "cli.js")) ? root : null;
13517
+ return existsSync17(join27(root, "dist", "cli.js")) ? root : null;
13326
13518
  } catch {
13327
13519
  return null;
13328
13520
  }
@@ -13345,7 +13537,7 @@ function pruneVersions(keep) {
13345
13537
  const removed = [];
13346
13538
  for (const name of readdirSync6(dir2)) {
13347
13539
  if (keep.includes(name)) continue;
13348
- rmSync13(join26(dir2, name), { recursive: true, force: true });
13540
+ rmSync13(join27(dir2, name), { recursive: true, force: true });
13349
13541
  removed.push(name);
13350
13542
  }
13351
13543
  return removed;
@@ -13364,7 +13556,7 @@ function compareVersions(a, b) {
13364
13556
 
13365
13557
  // src/update-state.ts
13366
13558
  import { mkdirSync as mkdirSync15, readFileSync as readFileSync15, renameSync as renameSync10, writeFileSync as writeFileSync10 } from "fs";
13367
- import { dirname as dirname12, join as join27 } from "path";
13559
+ import { dirname as dirname12, join as join28 } from "path";
13368
13560
  import { z as z18 } from "zod";
13369
13561
  var resultSchema = z18.object({
13370
13562
  kind: z18.enum(["updated", "rolled_back", "failed"]),
@@ -13388,7 +13580,7 @@ var stateSchema = z18.object({
13388
13580
  failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
13389
13581
  });
13390
13582
  function updateStatePath(dir2 = cabaneDir()) {
13391
- return join27(dir2, "update.json");
13583
+ return join28(dir2, "update.json");
13392
13584
  }
13393
13585
  function readUpdateState(path = updateStatePath()) {
13394
13586
  try {
@@ -13437,7 +13629,7 @@ function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
13437
13629
  return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
13438
13630
  }
13439
13631
  function writeBootGuard(root = ownPackageRoot()) {
13440
- const built = join28(root, "dist", "boot-guard.js");
13632
+ const built = join29(root, "dist", "boot-guard.js");
13441
13633
  if (!existsSync18(built)) return false;
13442
13634
  const target = bootGuardPath();
13443
13635
  mkdirSync16(dirname13(target), { recursive: true });
@@ -13477,17 +13669,17 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
13477
13669
  async function stageVersion(version, harnesses, deps = {}) {
13478
13670
  const run = deps.run ?? runProcess;
13479
13671
  const node = deps.execPath ?? process.execPath;
13480
- const target = join28(versionsDir(), version);
13672
+ const target = join29(versionsDir(), version);
13481
13673
  const placed = versionPackageRoot(version);
13482
13674
  if (existsSync18(placed)) {
13483
13675
  if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
13484
13676
  return { ok: true, root: placed };
13485
13677
  }
13486
- const npm = join28(dirname13(node), "npm");
13678
+ const npm = join29(dirname13(node), "npm");
13487
13679
  if (!existsSync18(npm))
13488
13680
  return { ok: false, reason: `there is no npm beside Node at ${dirname13(node)}` };
13489
13681
  mkdirSync16(versionsDir(), { recursive: true });
13490
- const staging = join28(versionsDir(), `.${version}.staging-${process.pid}`);
13682
+ const staging = join29(versionsDir(), `.${version}.staging-${process.pid}`);
13491
13683
  rmSync14(staging, { recursive: true, force: true });
13492
13684
  const installed = await run(
13493
13685
  npm,
@@ -13516,7 +13708,7 @@ async function stageVersion(version, harnesses, deps = {}) {
13516
13708
  return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
13517
13709
  return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
13518
13710
  }
13519
- const root = join28(staging, "node_modules", "@cabane", "companion");
13711
+ const root = join29(staging, "node_modules", "@cabane", "companion");
13520
13712
  const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
13521
13713
  if (verdict) {
13522
13714
  rmSync14(staging, { recursive: true, force: true });
@@ -13528,7 +13720,7 @@ async function stageVersion(version, harnesses, deps = {}) {
13528
13720
  }
13529
13721
  async function verifyInstall(root, version, harnesses, deps) {
13530
13722
  if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
13531
- const answered = await deps.run(deps.execPath, [join28(root, "dist", "bin.js"), "--version"], {
13723
+ const answered = await deps.run(deps.execPath, [join29(root, "dist", "bin.js"), "--version"], {
13532
13724
  timeoutMs: 6e4,
13533
13725
  env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
13534
13726
  });
@@ -13536,7 +13728,7 @@ async function verifyInstall(root, version, harnesses, deps) {
13536
13728
  return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
13537
13729
  }
13538
13730
  if (harnesses.length === 0) return null;
13539
- const probed = await deps.run(deps.execPath, [join28(root, "dist", "postinstall.js"), "--json"], {
13731
+ const probed = await deps.run(deps.execPath, [join29(root, "dist", "postinstall.js"), "--json"], {
13540
13732
  timeoutMs: 6e4
13541
13733
  });
13542
13734
  let resolutions = {};
@@ -15337,9 +15529,9 @@ function handleUncaught(log, err, origin) {
15337
15529
 
15338
15530
  // src/crash-marker.ts
15339
15531
  import { existsSync as existsSync19, mkdirSync as mkdirSync17, readFileSync as readFileSync16, rmSync as rmSync15, writeFileSync as writeFileSync11 } from "fs";
15340
- import { join as join29 } from "path";
15532
+ import { join as join30 } from "path";
15341
15533
  function crashMarkerPath() {
15342
- return join29(cabaneDir(), "last-error.json");
15534
+ return join30(cabaneDir(), "last-error.json");
15343
15535
  }
15344
15536
  function recordCrash(rec) {
15345
15537
  try {
@@ -15359,7 +15551,7 @@ function clearCrash() {
15359
15551
  // src/stray-companion.ts
15360
15552
  import { spawnSync } from "child_process";
15361
15553
  import { readFileSync as readFileSync17 } from "fs";
15362
- import { join as join30 } from "path";
15554
+ import { join as join31 } from "path";
15363
15555
  var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
15364
15556
  var START = /\sstart(\s|$)/;
15365
15557
  function companionStarts(psOutput) {
@@ -15376,7 +15568,7 @@ function companionStarts(psOutput) {
15376
15568
  }
15377
15569
  function stateDirOf(env) {
15378
15570
  const home = env.CABANE_COMPANION_HOME || env.HOME;
15379
- return home ? join30(home, ".cabane") : null;
15571
+ return home ? join31(home, ".cabane") : null;
15380
15572
  }
15381
15573
  function envFromPsE(output, command) {
15382
15574
  const line = output.replace(/\n$/, "");