@cabane/companion 0.6.13 → 0.6.15

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.
package/dist/cli.js CHANGED
@@ -6278,7 +6278,7 @@ var ConnectorHealthStore = class {
6278
6278
 
6279
6279
  // src/dispatcher.ts
6280
6280
  import { randomUUID } from "crypto";
6281
- import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10 } from "fs";
6281
+ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10, statSync as statSync2 } from "fs";
6282
6282
  import { join as join13 } from "path";
6283
6283
 
6284
6284
  // src/summon.ts
@@ -6620,7 +6620,7 @@ async function writeCodexInstructionsFile(contents) {
6620
6620
  }
6621
6621
 
6622
6622
  // src/prepared.ts
6623
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6623
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6624
6624
  import { join as join10 } from "path";
6625
6625
  function dirFor(workspaceId) {
6626
6626
  return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
@@ -6655,6 +6655,9 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
6655
6655
  "utf8"
6656
6656
  );
6657
6657
  }
6658
+ function clearPrepared(workspaceId, conversationId, agentId) {
6659
+ rmSync4(pathFor3(workspaceId, conversationId, agentId), { force: true });
6660
+ }
6658
6661
 
6659
6662
  // src/secrets.ts
6660
6663
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
@@ -6748,7 +6751,7 @@ function resolveMcpSecrets(mcpServers, store) {
6748
6751
  }
6749
6752
 
6750
6753
  // src/transcript-writer.ts
6751
- import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync4 } from "fs";
6754
+ import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync5 } from "fs";
6752
6755
  import { join as join12 } from "path";
6753
6756
  function transcriptsDir() {
6754
6757
  return join12(cabaneDir(), "transcripts");
@@ -6819,7 +6822,7 @@ function pruneOld(dir2, retain) {
6819
6822
  const drop = files.sort().slice(0, files.length - retain);
6820
6823
  for (const f of drop) {
6821
6824
  try {
6822
- rmSync4(join12(dir2, f), { force: true });
6825
+ rmSync5(join12(dir2, f), { force: true });
6823
6826
  } catch {
6824
6827
  }
6825
6828
  }
@@ -7117,6 +7120,21 @@ var SKIPPED_MARKER_BODY = "(skipped)";
7117
7120
  var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
7118
7121
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
7119
7122
  var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
7123
+ function checkoutState(cwd) {
7124
+ if (!cwd) return { ok: false, reason: "no checkout was resolved for this turn" };
7125
+ if (!existsSync9(cwd)) return { ok: false, reason: `the checkout directory is gone (${cwd})` };
7126
+ const gitPath = join13(cwd, ".git");
7127
+ if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
7128
+ let stat;
7129
+ try {
7130
+ stat = statSync2(gitPath);
7131
+ } catch (error) {
7132
+ return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
7133
+ }
7134
+ if (stat.isDirectory() && !existsSync9(join13(gitPath, "HEAD")))
7135
+ return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
7136
+ return { ok: true, reason: "usable" };
7137
+ }
7120
7138
  function runKey(conversationId, agentId) {
7121
7139
  return `${conversationId}|${agentId}`;
7122
7140
  }
@@ -7286,7 +7304,15 @@ var Dispatcher = class {
7286
7304
  const triggerIsPrepareFailure = message.body.startsWith(PREPARE_FAILED_PREFIX);
7287
7305
  const prepareFailureDispatch = turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? { dispatch: turnContext.dispatchedByAgentId } : {};
7288
7306
  if (prepareHook) {
7289
- const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
7307
+ let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
7308
+ if (cached2 && !checkoutState(cached2.cwd).ok) {
7309
+ turnLog.warn(
7310
+ { cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
7311
+ "dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
7312
+ );
7313
+ clearPrepared(workspaceId, payload.conversationId, payload.agentId);
7314
+ cached2 = null;
7315
+ }
7290
7316
  if (cached2) {
7291
7317
  effectiveCwd = cached2.cwd;
7292
7318
  hookEnv = cached2.env;
@@ -7542,21 +7568,49 @@ ${reason}`,
7542
7568
  `runtime_unavailable:${err.runtime}`
7543
7569
  );
7544
7570
  }
7571
+ let turnReceiptPath = null;
7572
+ const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
7573
+ const closeTurnReceipt = (ok, reason) => {
7574
+ if (!turnReceiptPath) return;
7575
+ const target = turnReceiptPath;
7576
+ turnReceiptPath = null;
7577
+ try {
7578
+ appendFileSync2(
7579
+ target,
7580
+ `${JSON.stringify({
7581
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7582
+ event: "settled",
7583
+ turnId,
7584
+ ok,
7585
+ reason
7586
+ })}
7587
+ `,
7588
+ { mode: 384 }
7589
+ );
7590
+ } catch (error) {
7591
+ turnLog.warn(
7592
+ { err: error instanceof Error ? error.message : String(error) },
7593
+ "dispatcher: turn-settled diagnostic write failed"
7594
+ );
7595
+ }
7596
+ };
7545
7597
  if (prepareHook && hookEnv?.CABANE_TASK_ID) {
7546
7598
  const proof = await proveWorkspaceTools(request, adapter.name, {
7547
7599
  ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7548
7600
  harnessFingerprint: turnContext.runtime
7549
7601
  });
7550
- turnLog[proof.ok ? "info" : "error"](
7551
- { workspaceProof: proof, checkout: effectiveCwd ?? null },
7552
- `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}`
7602
+ const checkout = checkoutState(effectiveCwd);
7603
+ turnLog[proof.ok && checkout.ok ? "info" : "error"](
7604
+ { workspaceProof: proof, checkout: effectiveCwd ?? null, checkoutState: checkout },
7605
+ `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout ${checkout.ok ? "usable" : checkout.reason}`
7553
7606
  );
7554
- if (effectiveCwd) {
7607
+ if (effectiveCwd && checkout.ok) {
7555
7608
  try {
7556
7609
  const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7557
7610
  mkdirSync10(diagnosticDir, { recursive: true });
7611
+ const receiptPath = join13(diagnosticDir, "readiness.jsonl");
7558
7612
  appendFileSync2(
7559
- join13(diagnosticDir, "readiness.jsonl"),
7613
+ receiptPath,
7560
7614
  `${JSON.stringify({
7561
7615
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7562
7616
  taskId: hookEnv.CABANE_TASK_ID,
@@ -7564,11 +7618,18 @@ ${reason}`,
7564
7618
  checkout: effectiveCwd,
7565
7619
  classification: proof.ok ? "ready" : "workspace_tools_missing",
7566
7620
  failedCapability: proof.failedCapability,
7567
- workspaceTools: proof
7621
+ workspaceTools: proof,
7622
+ // CT1022: the two fields the environment reaper reads — which turn
7623
+ // this is (so its settle can be matched among interleaved agents)
7624
+ // and how long it may legitimately run (so an unclosed receipt
7625
+ // expires on this turn's real deadline, not the reaper's guess).
7626
+ turnId,
7627
+ totalTimeoutMs
7568
7628
  })}
7569
7629
  `,
7570
7630
  { mode: 384 }
7571
7631
  );
7632
+ turnReceiptPath = receiptPath;
7572
7633
  } catch (error) {
7573
7634
  turnLog.warn(
7574
7635
  { err: error instanceof Error ? error.message : String(error) },
@@ -7576,8 +7637,26 @@ ${reason}`,
7576
7637
  );
7577
7638
  }
7578
7639
  }
7640
+ if (!checkout.ok) {
7641
+ const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7642
+ try {
7643
+ await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7644
+ body: `**Couldn't prepare your environment.** ${reason}`,
7645
+ kind: "final",
7646
+ turnId,
7647
+ parentMessageId: payload.messageId
7648
+ });
7649
+ } catch (postErr) {
7650
+ turnLog.warn(
7651
+ { err: postErr instanceof Error ? postErr.message : String(postErr) },
7652
+ "dispatcher: checkout-missing notice post failed"
7653
+ );
7654
+ }
7655
+ return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7656
+ }
7579
7657
  if (!proof.ok) {
7580
7658
  const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd ?? "unknown"}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7659
+ closeTurnReceipt(false, reason);
7581
7660
  try {
7582
7661
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7583
7662
  body: `**Couldn't prepare your environment.** ${reason}`,
@@ -7689,7 +7768,6 @@ ${reason}`,
7689
7768
  }
7690
7769
  };
7691
7770
  const idleTimeoutMs = this.opts.idleTimeoutMs ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
7692
- const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
7693
7771
  const fireTimeout = (reason) => {
7694
7772
  if (abortController.signal.aborted) return;
7695
7773
  timeoutReason = reason;
@@ -7791,6 +7869,7 @@ ${reason}`,
7791
7869
  } finally {
7792
7870
  if (idleTimer) clearTimeout(idleTimer);
7793
7871
  clearTimeout(totalTimer);
7872
+ closeTurnReceipt(okResult, resultReason ?? null);
7794
7873
  const userCancelled = abortController.signal.aborted && timeoutReason === null;
7795
7874
  if (timeoutReason !== null) {
7796
7875
  resultReason = timeoutReason;
@@ -8140,7 +8219,7 @@ import {
8140
8219
  readdirSync as readdirSync2,
8141
8220
  readFileSync as readFileSync8,
8142
8221
  renameSync as renameSync3,
8143
- rmSync as rmSync5,
8222
+ rmSync as rmSync6,
8144
8223
  writeFileSync as writeFileSync7
8145
8224
  } from "fs";
8146
8225
  import { join as join14 } from "path";
@@ -8174,7 +8253,7 @@ var Outbox = class {
8174
8253
  renameSync3(tmp, target);
8175
8254
  } catch (err) {
8176
8255
  try {
8177
- rmSync5(tmp, { force: true });
8256
+ rmSync6(tmp, { force: true });
8178
8257
  } catch {
8179
8258
  }
8180
8259
  this.log?.warn(
@@ -8221,7 +8300,7 @@ var Outbox = class {
8221
8300
  // Remove a delivered (or terminally-discarded) entry. No-op if already gone.
8222
8301
  remove(turnId, seq) {
8223
8302
  try {
8224
- rmSync5(this.fileFor(turnId, seq), { force: true });
8303
+ rmSync6(this.fileFor(turnId, seq), { force: true });
8225
8304
  } catch {
8226
8305
  }
8227
8306
  }
@@ -8240,7 +8319,7 @@ var Outbox = class {
8240
8319
  "companion outbox: dropping unreadable entry"
8241
8320
  );
8242
8321
  try {
8243
- rmSync5(full, { force: true });
8322
+ rmSync6(full, { force: true });
8244
8323
  } catch {
8245
8324
  }
8246
8325
  }
@@ -9307,7 +9386,7 @@ function handleUncaught(log, err, origin) {
9307
9386
  }
9308
9387
 
9309
9388
  // src/crash-marker.ts
9310
- import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
9389
+ import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
9311
9390
  import { join as join15 } from "path";
9312
9391
  function crashMarkerPath() {
9313
9392
  return join15(cabaneDir(), "last-error.json");
@@ -9322,7 +9401,7 @@ function recordCrash(rec2) {
9322
9401
  function clearCrash() {
9323
9402
  try {
9324
9403
  const path3 = crashMarkerPath();
9325
- if (existsSync11(path3)) rmSync6(path3, { force: true });
9404
+ if (existsSync11(path3)) rmSync7(path3, { force: true });
9326
9405
  } catch {
9327
9406
  }
9328
9407
  }
package/dist/runtime.js CHANGED
@@ -5931,7 +5931,7 @@ var ConnectorHealthStore = class {
5931
5931
 
5932
5932
  // src/dispatcher.ts
5933
5933
  import { randomUUID } from "crypto";
5934
- import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
5934
+ import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync as statSync2 } from "fs";
5935
5935
  import { join as join13 } from "path";
5936
5936
 
5937
5937
  // src/summon.ts
@@ -6273,7 +6273,7 @@ async function writeCodexInstructionsFile(contents) {
6273
6273
  }
6274
6274
 
6275
6275
  // src/prepared.ts
6276
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6276
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
6277
6277
  import { join as join10 } from "path";
6278
6278
  function dirFor(workspaceId) {
6279
6279
  return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
@@ -6308,6 +6308,9 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
6308
6308
  "utf8"
6309
6309
  );
6310
6310
  }
6311
+ function clearPrepared(workspaceId, conversationId, agentId) {
6312
+ rmSync4(pathFor3(workspaceId, conversationId, agentId), { force: true });
6313
+ }
6311
6314
 
6312
6315
  // src/secrets.ts
6313
6316
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
@@ -6401,7 +6404,7 @@ function resolveMcpSecrets(mcpServers, store) {
6401
6404
  }
6402
6405
 
6403
6406
  // src/transcript-writer.ts
6404
- import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync4 } from "fs";
6407
+ import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync5 } from "fs";
6405
6408
  import { join as join12 } from "path";
6406
6409
  function transcriptsDir() {
6407
6410
  return join12(cabaneDir(), "transcripts");
@@ -6472,7 +6475,7 @@ function pruneOld(dir2, retain) {
6472
6475
  const drop = files.sort().slice(0, files.length - retain);
6473
6476
  for (const f of drop) {
6474
6477
  try {
6475
- rmSync4(join12(dir2, f), { force: true });
6478
+ rmSync5(join12(dir2, f), { force: true });
6476
6479
  } catch {
6477
6480
  }
6478
6481
  }
@@ -6770,6 +6773,21 @@ var SKIPPED_MARKER_BODY = "(skipped)";
6770
6773
  var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
6771
6774
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
6772
6775
  var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
6776
+ function checkoutState(cwd) {
6777
+ if (!cwd) return { ok: false, reason: "no checkout was resolved for this turn" };
6778
+ if (!existsSync9(cwd)) return { ok: false, reason: `the checkout directory is gone (${cwd})` };
6779
+ const gitPath = join13(cwd, ".git");
6780
+ if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
6781
+ let stat;
6782
+ try {
6783
+ stat = statSync2(gitPath);
6784
+ } catch (error) {
6785
+ return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
6786
+ }
6787
+ if (stat.isDirectory() && !existsSync9(join13(gitPath, "HEAD")))
6788
+ return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
6789
+ return { ok: true, reason: "usable" };
6790
+ }
6773
6791
  function runKey(conversationId, agentId) {
6774
6792
  return `${conversationId}|${agentId}`;
6775
6793
  }
@@ -6939,7 +6957,15 @@ var Dispatcher = class {
6939
6957
  const triggerIsPrepareFailure = message.body.startsWith(PREPARE_FAILED_PREFIX);
6940
6958
  const prepareFailureDispatch = turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? { dispatch: turnContext.dispatchedByAgentId } : {};
6941
6959
  if (prepareHook) {
6942
- const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
6960
+ let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
6961
+ if (cached2 && !checkoutState(cached2.cwd).ok) {
6962
+ turnLog.warn(
6963
+ { cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
6964
+ "dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
6965
+ );
6966
+ clearPrepared(workspaceId, payload.conversationId, payload.agentId);
6967
+ cached2 = null;
6968
+ }
6943
6969
  if (cached2) {
6944
6970
  effectiveCwd = cached2.cwd;
6945
6971
  hookEnv = cached2.env;
@@ -7195,21 +7221,49 @@ ${reason}`,
7195
7221
  `runtime_unavailable:${err.runtime}`
7196
7222
  );
7197
7223
  }
7224
+ let turnReceiptPath = null;
7225
+ const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
7226
+ const closeTurnReceipt = (ok, reason) => {
7227
+ if (!turnReceiptPath) return;
7228
+ const target = turnReceiptPath;
7229
+ turnReceiptPath = null;
7230
+ try {
7231
+ appendFileSync2(
7232
+ target,
7233
+ `${JSON.stringify({
7234
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7235
+ event: "settled",
7236
+ turnId,
7237
+ ok,
7238
+ reason
7239
+ })}
7240
+ `,
7241
+ { mode: 384 }
7242
+ );
7243
+ } catch (error) {
7244
+ turnLog.warn(
7245
+ { err: error instanceof Error ? error.message : String(error) },
7246
+ "dispatcher: turn-settled diagnostic write failed"
7247
+ );
7248
+ }
7249
+ };
7198
7250
  if (prepareHook && hookEnv?.CABANE_TASK_ID) {
7199
7251
  const proof = await proveWorkspaceTools(request, adapter.name, {
7200
7252
  ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7201
7253
  harnessFingerprint: turnContext.runtime
7202
7254
  });
7203
- turnLog[proof.ok ? "info" : "error"](
7204
- { workspaceProof: proof, checkout: effectiveCwd ?? null },
7205
- `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}`
7255
+ const checkout = checkoutState(effectiveCwd);
7256
+ turnLog[proof.ok && checkout.ok ? "info" : "error"](
7257
+ { workspaceProof: proof, checkout: effectiveCwd ?? null, checkoutState: checkout },
7258
+ `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout ${checkout.ok ? "usable" : checkout.reason}`
7206
7259
  );
7207
- if (effectiveCwd) {
7260
+ if (effectiveCwd && checkout.ok) {
7208
7261
  try {
7209
7262
  const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7210
7263
  mkdirSync9(diagnosticDir, { recursive: true });
7264
+ const receiptPath = join13(diagnosticDir, "readiness.jsonl");
7211
7265
  appendFileSync2(
7212
- join13(diagnosticDir, "readiness.jsonl"),
7266
+ receiptPath,
7213
7267
  `${JSON.stringify({
7214
7268
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7215
7269
  taskId: hookEnv.CABANE_TASK_ID,
@@ -7217,11 +7271,18 @@ ${reason}`,
7217
7271
  checkout: effectiveCwd,
7218
7272
  classification: proof.ok ? "ready" : "workspace_tools_missing",
7219
7273
  failedCapability: proof.failedCapability,
7220
- workspaceTools: proof
7274
+ workspaceTools: proof,
7275
+ // CT1022: the two fields the environment reaper reads — which turn
7276
+ // this is (so its settle can be matched among interleaved agents)
7277
+ // and how long it may legitimately run (so an unclosed receipt
7278
+ // expires on this turn's real deadline, not the reaper's guess).
7279
+ turnId,
7280
+ totalTimeoutMs
7221
7281
  })}
7222
7282
  `,
7223
7283
  { mode: 384 }
7224
7284
  );
7285
+ turnReceiptPath = receiptPath;
7225
7286
  } catch (error) {
7226
7287
  turnLog.warn(
7227
7288
  { err: error instanceof Error ? error.message : String(error) },
@@ -7229,8 +7290,26 @@ ${reason}`,
7229
7290
  );
7230
7291
  }
7231
7292
  }
7293
+ if (!checkout.ok) {
7294
+ const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7295
+ try {
7296
+ await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7297
+ body: `**Couldn't prepare your environment.** ${reason}`,
7298
+ kind: "final",
7299
+ turnId,
7300
+ parentMessageId: payload.messageId
7301
+ });
7302
+ } catch (postErr) {
7303
+ turnLog.warn(
7304
+ { err: postErr instanceof Error ? postErr.message : String(postErr) },
7305
+ "dispatcher: checkout-missing notice post failed"
7306
+ );
7307
+ }
7308
+ return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7309
+ }
7232
7310
  if (!proof.ok) {
7233
7311
  const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd ?? "unknown"}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7312
+ closeTurnReceipt(false, reason);
7234
7313
  try {
7235
7314
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7236
7315
  body: `**Couldn't prepare your environment.** ${reason}`,
@@ -7342,7 +7421,6 @@ ${reason}`,
7342
7421
  }
7343
7422
  };
7344
7423
  const idleTimeoutMs = this.opts.idleTimeoutMs ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
7345
- const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
7346
7424
  const fireTimeout = (reason) => {
7347
7425
  if (abortController.signal.aborted) return;
7348
7426
  timeoutReason = reason;
@@ -7444,6 +7522,7 @@ ${reason}`,
7444
7522
  } finally {
7445
7523
  if (idleTimer) clearTimeout(idleTimer);
7446
7524
  clearTimeout(totalTimer);
7525
+ closeTurnReceipt(okResult, resultReason ?? null);
7447
7526
  const userCancelled = abortController.signal.aborted && timeoutReason === null;
7448
7527
  if (timeoutReason !== null) {
7449
7528
  resultReason = timeoutReason;
@@ -7793,7 +7872,7 @@ import {
7793
7872
  readdirSync as readdirSync2,
7794
7873
  readFileSync as readFileSync8,
7795
7874
  renameSync as renameSync3,
7796
- rmSync as rmSync5,
7875
+ rmSync as rmSync6,
7797
7876
  writeFileSync as writeFileSync7
7798
7877
  } from "fs";
7799
7878
  import { join as join14 } from "path";
@@ -7827,7 +7906,7 @@ var Outbox = class {
7827
7906
  renameSync3(tmp, target);
7828
7907
  } catch (err) {
7829
7908
  try {
7830
- rmSync5(tmp, { force: true });
7909
+ rmSync6(tmp, { force: true });
7831
7910
  } catch {
7832
7911
  }
7833
7912
  this.log?.warn(
@@ -7874,7 +7953,7 @@ var Outbox = class {
7874
7953
  // Remove a delivered (or terminally-discarded) entry. No-op if already gone.
7875
7954
  remove(turnId, seq) {
7876
7955
  try {
7877
- rmSync5(this.fileFor(turnId, seq), { force: true });
7956
+ rmSync6(this.fileFor(turnId, seq), { force: true });
7878
7957
  } catch {
7879
7958
  }
7880
7959
  }
@@ -7893,7 +7972,7 @@ var Outbox = class {
7893
7972
  "companion outbox: dropping unreadable entry"
7894
7973
  );
7895
7974
  try {
7896
- rmSync5(full, { force: true });
7975
+ rmSync6(full, { force: true });
7897
7976
  } catch {
7898
7977
  }
7899
7978
  }
@@ -8960,7 +9039,7 @@ function handleUncaught(log, err, origin) {
8960
9039
  }
8961
9040
 
8962
9041
  // src/crash-marker.ts
8963
- import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
9042
+ import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
8964
9043
  import { join as join15 } from "path";
8965
9044
  function crashMarkerPath() {
8966
9045
  return join15(cabaneDir(), "last-error.json");
@@ -8975,7 +9054,7 @@ function recordCrash(rec) {
8975
9054
  function clearCrash() {
8976
9055
  try {
8977
9056
  const path3 = crashMarkerPath();
8978
- if (existsSync11(path3)) rmSync6(path3, { force: true });
9057
+ if (existsSync11(path3)) rmSync7(path3, { force: true });
8979
9058
  } catch {
8980
9059
  }
8981
9060
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
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",