@cabane/companion 0.6.14 → 0.6.16

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 +115 -34
  2. package/dist/runtime.js +115 -34
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4918,6 +4918,7 @@ function parseCodexModel(model) {
4918
4918
  var CABANE_MCP_SERVER3 = "cabane";
4919
4919
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
4920
4920
  var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
4921
+ var ENV_ENVELOPE_KEYS = ["CABANE_ENV_TIER", "CABANE_ENV_KEY", "CABANE_ENV_BINDING"];
4921
4922
  function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4922
4923
  const { policy, config } = req;
4923
4924
  const directory = req.local.cwd ?? "";
@@ -5004,6 +5005,12 @@ function buildConfig(req, baseInstructionsFile) {
5004
5005
  }
5005
5006
  const policy = codexToolPolicy(req.policy);
5006
5007
  const tmpDir = req.local.env?.TMPDIR;
5008
+ const shellEnv = {};
5009
+ if (tmpDir) shellEnv.TMPDIR = tmpDir;
5010
+ for (const key of ENV_ENVELOPE_KEYS) {
5011
+ const value = req.local.env?.[key];
5012
+ if (value !== void 0) shellEnv[key] = value;
5013
+ }
5007
5014
  const gateLockDir = req.local.env?.CABANE_GATE_LOCK_DIR;
5008
5015
  return {
5009
5016
  mcp_servers,
@@ -5012,7 +5019,7 @@ function buildConfig(req, baseInstructionsFile) {
5012
5019
  // becomes Codex's base instructions, and Codex's own environment preamble goes
5013
5020
  // with the harness prompt it belonged to.
5014
5021
  ...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
5015
- ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
5022
+ ...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {},
5016
5023
  ...policy.permissionProfile ? {
5017
5024
  // CT733: named permission profiles are Codex's split-filesystem path.
5018
5025
  // `:root = read` preserves coding-mode host reads; the one explicit
@@ -7568,43 +7575,92 @@ ${reason}`,
7568
7575
  `runtime_unavailable:${err.runtime}`
7569
7576
  );
7570
7577
  }
7578
+ let turnReceiptPath = null;
7579
+ const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
7580
+ const closeTurnReceipt = (ok, reason) => {
7581
+ if (!turnReceiptPath) return;
7582
+ const target = turnReceiptPath;
7583
+ turnReceiptPath = null;
7584
+ try {
7585
+ appendFileSync2(
7586
+ target,
7587
+ `${JSON.stringify({
7588
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7589
+ event: "settled",
7590
+ turnId,
7591
+ agentId: payload.agentId,
7592
+ conversationId: payload.conversationId,
7593
+ ok,
7594
+ reason
7595
+ })}
7596
+ `,
7597
+ { mode: 384 }
7598
+ );
7599
+ } catch (error) {
7600
+ turnLog.warn(
7601
+ { err: error instanceof Error ? error.message : String(error) },
7602
+ "dispatcher: turn-settled diagnostic write failed"
7603
+ );
7604
+ }
7605
+ };
7571
7606
  if (prepareHook && hookEnv?.CABANE_TASK_ID) {
7572
- const proof = await proveWorkspaceTools(request, adapter.name, {
7573
- ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7574
- harnessFingerprint: turnContext.runtime
7575
- });
7576
7607
  const checkout = checkoutState(effectiveCwd);
7577
- turnLog[proof.ok && checkout.ok ? "info" : "error"](
7578
- { workspaceProof: proof, checkout: effectiveCwd ?? null, checkoutState: checkout },
7579
- `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout ${checkout.ok ? "usable" : checkout.reason}`
7580
- );
7581
- if (effectiveCwd && checkout.ok) {
7608
+ if (!effectiveCwd || !checkout.ok) {
7609
+ const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7610
+ turnLog.error({ checkout: effectiveCwd ?? null, checkoutState: checkout }, reason);
7582
7611
  try {
7583
- const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7584
- mkdirSync10(diagnosticDir, { recursive: true });
7585
- appendFileSync2(
7586
- join13(diagnosticDir, "readiness.jsonl"),
7587
- `${JSON.stringify({
7588
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7589
- taskId: hookEnv.CABANE_TASK_ID,
7590
- binding: hookEnv.CABANE_TASK_BINDING ?? null,
7591
- checkout: effectiveCwd,
7592
- classification: proof.ok ? "ready" : "workspace_tools_missing",
7593
- failedCapability: proof.failedCapability,
7594
- workspaceTools: proof
7595
- })}
7596
- `,
7597
- { mode: 384 }
7598
- );
7599
- } catch (error) {
7612
+ await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7613
+ body: `**Couldn't prepare your environment.** ${reason}`,
7614
+ kind: "final",
7615
+ turnId,
7616
+ parentMessageId: payload.messageId
7617
+ });
7618
+ } catch (postErr) {
7600
7619
  turnLog.warn(
7601
- { err: error instanceof Error ? error.message : String(error) },
7602
- "dispatcher: workspace-proof diagnostic write failed"
7620
+ { err: postErr instanceof Error ? postErr.message : String(postErr) },
7621
+ "dispatcher: checkout-missing notice post failed"
7603
7622
  );
7604
7623
  }
7624
+ return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7605
7625
  }
7606
- if (!checkout.ok) {
7607
- const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7626
+ const receiptPath = join13(effectiveCwd, ".git", "cabane", "readiness.jsonl");
7627
+ const receiptLine = (fields) => `${JSON.stringify({
7628
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7629
+ taskId: hookEnv.CABANE_TASK_ID,
7630
+ binding: hookEnv.CABANE_TASK_BINDING ?? null,
7631
+ checkout: effectiveCwd,
7632
+ // CT1022: the two fields the environment reaper reads — which turn this
7633
+ // is (so its settle can be matched among interleaved agents) and how long
7634
+ // it may legitimately run (so an unclosed receipt expires on this turn's
7635
+ // real deadline, not the reaper's guess).
7636
+ turnId,
7637
+ totalTimeoutMs,
7638
+ // CT1062: WHOSE turn. A task env is shared — between agents, and between
7639
+ // an agent's own sequential conversations — so a reader asking "is a turn
7640
+ // of THIS agent, other than mine, running here?" (the host's run-lock
7641
+ // does, before it lets a second conversation into the tree) can only
7642
+ // answer it if the line says who. An unattributed open line has to count
7643
+ // for everyone, which refuses work that should have been admitted.
7644
+ agentId: payload.agentId,
7645
+ conversationId: payload.conversationId,
7646
+ ...fields
7647
+ })}
7648
+ `;
7649
+ try {
7650
+ mkdirSync10(join13(effectiveCwd, ".git", "cabane"), { recursive: true });
7651
+ appendFileSync2(
7652
+ receiptPath,
7653
+ // `starting` is the honest classification before the proof has run. The
7654
+ // line the proof appends below carries the same `turnId`, so a reader
7655
+ // replaying the file sees one turn, not two.
7656
+ receiptLine({ classification: "starting" }),
7657
+ { mode: 384 }
7658
+ );
7659
+ turnReceiptPath = receiptPath;
7660
+ } catch (error) {
7661
+ const detail = error instanceof Error ? error.message : String(error);
7662
+ const reason = `turn_receipt_unwritable: ${detail}; task=${hookEnv.CABANE_TASK_ID}; checkout=${effectiveCwd}; recovery=restore write access to the checkout's .git/cabane, then re-dispatch`;
7663
+ turnLog.error({ err: detail, receiptPath }, reason);
7608
7664
  try {
7609
7665
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7610
7666
  body: `**Couldn't prepare your environment.** ${reason}`,
@@ -7615,13 +7671,38 @@ ${reason}`,
7615
7671
  } catch (postErr) {
7616
7672
  turnLog.warn(
7617
7673
  { err: postErr instanceof Error ? postErr.message : String(postErr) },
7618
- "dispatcher: checkout-missing notice post failed"
7674
+ "dispatcher: turn-receipt failure notice post failed"
7619
7675
  );
7620
7676
  }
7621
7677
  return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7622
7678
  }
7679
+ const proof = await proveWorkspaceTools(request, adapter.name, {
7680
+ ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7681
+ harnessFingerprint: turnContext.runtime
7682
+ });
7683
+ turnLog[proof.ok ? "info" : "error"](
7684
+ { workspaceProof: proof, checkout: effectiveCwd },
7685
+ `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout usable`
7686
+ );
7687
+ try {
7688
+ appendFileSync2(
7689
+ receiptPath,
7690
+ receiptLine({
7691
+ classification: proof.ok ? "ready" : "workspace_tools_missing",
7692
+ failedCapability: proof.failedCapability,
7693
+ workspaceTools: proof
7694
+ }),
7695
+ { mode: 384 }
7696
+ );
7697
+ } catch (error) {
7698
+ turnLog.warn(
7699
+ { err: error instanceof Error ? error.message : String(error) },
7700
+ "dispatcher: workspace-proof diagnostic write failed (the turn receipt is open)"
7701
+ );
7702
+ }
7623
7703
  if (!proof.ok) {
7624
- const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd ?? "unknown"}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7704
+ const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7705
+ closeTurnReceipt(false, reason);
7625
7706
  try {
7626
7707
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7627
7708
  body: `**Couldn't prepare your environment.** ${reason}`,
@@ -7733,7 +7814,6 @@ ${reason}`,
7733
7814
  }
7734
7815
  };
7735
7816
  const idleTimeoutMs = this.opts.idleTimeoutMs ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
7736
- const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
7737
7817
  const fireTimeout = (reason) => {
7738
7818
  if (abortController.signal.aborted) return;
7739
7819
  timeoutReason = reason;
@@ -7835,6 +7915,7 @@ ${reason}`,
7835
7915
  } finally {
7836
7916
  if (idleTimer) clearTimeout(idleTimer);
7837
7917
  clearTimeout(totalTimer);
7918
+ closeTurnReceipt(okResult, resultReason ?? null);
7838
7919
  const userCancelled = abortController.signal.aborted && timeoutReason === null;
7839
7920
  if (timeoutReason !== null) {
7840
7921
  resultReason = timeoutReason;
package/dist/runtime.js CHANGED
@@ -4571,6 +4571,7 @@ function parseCodexModel(model) {
4571
4571
  var CABANE_MCP_SERVER3 = "cabane";
4572
4572
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
4573
4573
  var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
4574
+ var ENV_ENVELOPE_KEYS = ["CABANE_ENV_TIER", "CABANE_ENV_KEY", "CABANE_ENV_BINDING"];
4574
4575
  function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4575
4576
  const { policy, config } = req;
4576
4577
  const directory = req.local.cwd ?? "";
@@ -4657,6 +4658,12 @@ function buildConfig(req, baseInstructionsFile) {
4657
4658
  }
4658
4659
  const policy = codexToolPolicy(req.policy);
4659
4660
  const tmpDir = req.local.env?.TMPDIR;
4661
+ const shellEnv = {};
4662
+ if (tmpDir) shellEnv.TMPDIR = tmpDir;
4663
+ for (const key of ENV_ENVELOPE_KEYS) {
4664
+ const value = req.local.env?.[key];
4665
+ if (value !== void 0) shellEnv[key] = value;
4666
+ }
4660
4667
  const gateLockDir = req.local.env?.CABANE_GATE_LOCK_DIR;
4661
4668
  return {
4662
4669
  mcp_servers,
@@ -4665,7 +4672,7 @@ function buildConfig(req, baseInstructionsFile) {
4665
4672
  // becomes Codex's base instructions, and Codex's own environment preamble goes
4666
4673
  // with the harness prompt it belonged to.
4667
4674
  ...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
4668
- ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
4675
+ ...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {},
4669
4676
  ...policy.permissionProfile ? {
4670
4677
  // CT733: named permission profiles are Codex's split-filesystem path.
4671
4678
  // `:root = read` preserves coding-mode host reads; the one explicit
@@ -7221,43 +7228,92 @@ ${reason}`,
7221
7228
  `runtime_unavailable:${err.runtime}`
7222
7229
  );
7223
7230
  }
7231
+ let turnReceiptPath = null;
7232
+ const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
7233
+ const closeTurnReceipt = (ok, reason) => {
7234
+ if (!turnReceiptPath) return;
7235
+ const target = turnReceiptPath;
7236
+ turnReceiptPath = null;
7237
+ try {
7238
+ appendFileSync2(
7239
+ target,
7240
+ `${JSON.stringify({
7241
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7242
+ event: "settled",
7243
+ turnId,
7244
+ agentId: payload.agentId,
7245
+ conversationId: payload.conversationId,
7246
+ ok,
7247
+ reason
7248
+ })}
7249
+ `,
7250
+ { mode: 384 }
7251
+ );
7252
+ } catch (error) {
7253
+ turnLog.warn(
7254
+ { err: error instanceof Error ? error.message : String(error) },
7255
+ "dispatcher: turn-settled diagnostic write failed"
7256
+ );
7257
+ }
7258
+ };
7224
7259
  if (prepareHook && hookEnv?.CABANE_TASK_ID) {
7225
- const proof = await proveWorkspaceTools(request, adapter.name, {
7226
- ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7227
- harnessFingerprint: turnContext.runtime
7228
- });
7229
7260
  const checkout = checkoutState(effectiveCwd);
7230
- turnLog[proof.ok && checkout.ok ? "info" : "error"](
7231
- { workspaceProof: proof, checkout: effectiveCwd ?? null, checkoutState: checkout },
7232
- `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout ${checkout.ok ? "usable" : checkout.reason}`
7233
- );
7234
- if (effectiveCwd && checkout.ok) {
7261
+ if (!effectiveCwd || !checkout.ok) {
7262
+ const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7263
+ turnLog.error({ checkout: effectiveCwd ?? null, checkoutState: checkout }, reason);
7235
7264
  try {
7236
- const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7237
- mkdirSync9(diagnosticDir, { recursive: true });
7238
- appendFileSync2(
7239
- join13(diagnosticDir, "readiness.jsonl"),
7240
- `${JSON.stringify({
7241
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7242
- taskId: hookEnv.CABANE_TASK_ID,
7243
- binding: hookEnv.CABANE_TASK_BINDING ?? null,
7244
- checkout: effectiveCwd,
7245
- classification: proof.ok ? "ready" : "workspace_tools_missing",
7246
- failedCapability: proof.failedCapability,
7247
- workspaceTools: proof
7248
- })}
7249
- `,
7250
- { mode: 384 }
7251
- );
7252
- } catch (error) {
7265
+ await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7266
+ body: `**Couldn't prepare your environment.** ${reason}`,
7267
+ kind: "final",
7268
+ turnId,
7269
+ parentMessageId: payload.messageId
7270
+ });
7271
+ } catch (postErr) {
7253
7272
  turnLog.warn(
7254
- { err: error instanceof Error ? error.message : String(error) },
7255
- "dispatcher: workspace-proof diagnostic write failed"
7273
+ { err: postErr instanceof Error ? postErr.message : String(postErr) },
7274
+ "dispatcher: checkout-missing notice post failed"
7256
7275
  );
7257
7276
  }
7277
+ return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7258
7278
  }
7259
- if (!checkout.ok) {
7260
- const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7279
+ const receiptPath = join13(effectiveCwd, ".git", "cabane", "readiness.jsonl");
7280
+ const receiptLine = (fields) => `${JSON.stringify({
7281
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7282
+ taskId: hookEnv.CABANE_TASK_ID,
7283
+ binding: hookEnv.CABANE_TASK_BINDING ?? null,
7284
+ checkout: effectiveCwd,
7285
+ // CT1022: the two fields the environment reaper reads — which turn this
7286
+ // is (so its settle can be matched among interleaved agents) and how long
7287
+ // it may legitimately run (so an unclosed receipt expires on this turn's
7288
+ // real deadline, not the reaper's guess).
7289
+ turnId,
7290
+ totalTimeoutMs,
7291
+ // CT1062: WHOSE turn. A task env is shared — between agents, and between
7292
+ // an agent's own sequential conversations — so a reader asking "is a turn
7293
+ // of THIS agent, other than mine, running here?" (the host's run-lock
7294
+ // does, before it lets a second conversation into the tree) can only
7295
+ // answer it if the line says who. An unattributed open line has to count
7296
+ // for everyone, which refuses work that should have been admitted.
7297
+ agentId: payload.agentId,
7298
+ conversationId: payload.conversationId,
7299
+ ...fields
7300
+ })}
7301
+ `;
7302
+ try {
7303
+ mkdirSync9(join13(effectiveCwd, ".git", "cabane"), { recursive: true });
7304
+ appendFileSync2(
7305
+ receiptPath,
7306
+ // `starting` is the honest classification before the proof has run. The
7307
+ // line the proof appends below carries the same `turnId`, so a reader
7308
+ // replaying the file sees one turn, not two.
7309
+ receiptLine({ classification: "starting" }),
7310
+ { mode: 384 }
7311
+ );
7312
+ turnReceiptPath = receiptPath;
7313
+ } catch (error) {
7314
+ const detail = error instanceof Error ? error.message : String(error);
7315
+ const reason = `turn_receipt_unwritable: ${detail}; task=${hookEnv.CABANE_TASK_ID}; checkout=${effectiveCwd}; recovery=restore write access to the checkout's .git/cabane, then re-dispatch`;
7316
+ turnLog.error({ err: detail, receiptPath }, reason);
7261
7317
  try {
7262
7318
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7263
7319
  body: `**Couldn't prepare your environment.** ${reason}`,
@@ -7268,13 +7324,38 @@ ${reason}`,
7268
7324
  } catch (postErr) {
7269
7325
  turnLog.warn(
7270
7326
  { err: postErr instanceof Error ? postErr.message : String(postErr) },
7271
- "dispatcher: checkout-missing notice post failed"
7327
+ "dispatcher: turn-receipt failure notice post failed"
7272
7328
  );
7273
7329
  }
7274
7330
  return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7275
7331
  }
7332
+ const proof = await proveWorkspaceTools(request, adapter.name, {
7333
+ ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7334
+ harnessFingerprint: turnContext.runtime
7335
+ });
7336
+ turnLog[proof.ok ? "info" : "error"](
7337
+ { workspaceProof: proof, checkout: effectiveCwd },
7338
+ `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout usable`
7339
+ );
7340
+ try {
7341
+ appendFileSync2(
7342
+ receiptPath,
7343
+ receiptLine({
7344
+ classification: proof.ok ? "ready" : "workspace_tools_missing",
7345
+ failedCapability: proof.failedCapability,
7346
+ workspaceTools: proof
7347
+ }),
7348
+ { mode: 384 }
7349
+ );
7350
+ } catch (error) {
7351
+ turnLog.warn(
7352
+ { err: error instanceof Error ? error.message : String(error) },
7353
+ "dispatcher: workspace-proof diagnostic write failed (the turn receipt is open)"
7354
+ );
7355
+ }
7276
7356
  if (!proof.ok) {
7277
- const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd ?? "unknown"}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7357
+ const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7358
+ closeTurnReceipt(false, reason);
7278
7359
  try {
7279
7360
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7280
7361
  body: `**Couldn't prepare your environment.** ${reason}`,
@@ -7386,7 +7467,6 @@ ${reason}`,
7386
7467
  }
7387
7468
  };
7388
7469
  const idleTimeoutMs = this.opts.idleTimeoutMs ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
7389
- const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
7390
7470
  const fireTimeout = (reason) => {
7391
7471
  if (abortController.signal.aborted) return;
7392
7472
  timeoutReason = reason;
@@ -7488,6 +7568,7 @@ ${reason}`,
7488
7568
  } finally {
7489
7569
  if (idleTimer) clearTimeout(idleTimer);
7490
7570
  clearTimeout(totalTimer);
7571
+ closeTurnReceipt(okResult, resultReason ?? null);
7491
7572
  const userCancelled = abortController.signal.aborted && timeoutReason === null;
7492
7573
  if (timeoutReason !== null) {
7493
7574
  resultReason = timeoutReason;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",