@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.
- package/dist/cli.js +115 -34
- package/dist/runtime.js +115 -34
- 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
|
-
...
|
|
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
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
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
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
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:
|
|
7602
|
-
"dispatcher:
|
|
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
|
-
|
|
7607
|
-
|
|
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:
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
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
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
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:
|
|
7255
|
-
"dispatcher:
|
|
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
|
-
|
|
7260
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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",
|