@cabane/companion 0.6.106 → 0.6.108
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 +188 -53
- package/dist/pairing-config.js +9 -1
- package/dist/runtime.js +182 -47
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -93,7 +93,15 @@ var prepareHookSchema = z.object({
|
|
|
93
93
|
//
|
|
94
94
|
// Default OFF, and deliberately: an operator hook that provisions
|
|
95
95
|
// unconditionally must not start running on every turn because it upgraded.
|
|
96
|
-
validateCached: z.boolean().optional()
|
|
96
|
+
validateCached: z.boolean().optional(),
|
|
97
|
+
// Independent operator-owned reporter. Receives sanitized failure metadata
|
|
98
|
+
// on stdin; never re-enters this hook and cannot change the turn outcome.
|
|
99
|
+
failureReporter: z.object({
|
|
100
|
+
command: z.string().min(1),
|
|
101
|
+
args: z.array(z.string()).optional(),
|
|
102
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
103
|
+
timeoutMs: z.number().int().positive().max(3e4).optional()
|
|
104
|
+
}).strict().optional()
|
|
97
105
|
}).strict();
|
|
98
106
|
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
99
107
|
var prepareResultSchema = z.object({
|
|
@@ -1769,8 +1777,8 @@ function requestEnrollmentCode(baseUrl, opts = {}) {
|
|
|
1769
1777
|
...opts.label ? { label: opts.label } : {}
|
|
1770
1778
|
});
|
|
1771
1779
|
}
|
|
1772
|
-
function deviceLabelFromHostname(
|
|
1773
|
-
const trimmed =
|
|
1780
|
+
function deviceLabelFromHostname(hostname5) {
|
|
1781
|
+
const trimmed = hostname5.trim().replace(/\.local$/i, "");
|
|
1774
1782
|
if (trimmed.length === 0) return void 0;
|
|
1775
1783
|
return trimmed.slice(0, 120);
|
|
1776
1784
|
}
|
|
@@ -2097,7 +2105,7 @@ async function logs(opts = {}) {
|
|
|
2097
2105
|
}
|
|
2098
2106
|
|
|
2099
2107
|
// src/commands/start.ts
|
|
2100
|
-
import { hostname as
|
|
2108
|
+
import { hostname as hostname4 } from "os";
|
|
2101
2109
|
|
|
2102
2110
|
// src/runtime.ts
|
|
2103
2111
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
@@ -2662,7 +2670,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
2662
2670
|
}
|
|
2663
2671
|
|
|
2664
2672
|
// src/supervisor.ts
|
|
2665
|
-
import { hostname as
|
|
2673
|
+
import { hostname as hostname3 } from "os";
|
|
2666
2674
|
|
|
2667
2675
|
// packages/agent-runtime/src/failure.ts
|
|
2668
2676
|
import { z as z4 } from "zod";
|
|
@@ -4383,6 +4391,15 @@ var MAX_ASK_QUESTION_BYTES = 400;
|
|
|
4383
4391
|
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
4384
4392
|
var MAX_ASK_ITEMS = 20;
|
|
4385
4393
|
var MAX_ASK_OPTION_BYTES = 200;
|
|
4394
|
+
var TASK_STAGES = [
|
|
4395
|
+
"proposed",
|
|
4396
|
+
"todo",
|
|
4397
|
+
"in_progress",
|
|
4398
|
+
"in_review",
|
|
4399
|
+
"done",
|
|
4400
|
+
"dropped",
|
|
4401
|
+
"deferred"
|
|
4402
|
+
];
|
|
4386
4403
|
var MIN_ASK_OPTIONS = 2;
|
|
4387
4404
|
var MAX_ASK_OPTIONS = 4;
|
|
4388
4405
|
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
@@ -4407,11 +4424,19 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
4407
4424
|
targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
|
|
4408
4425
|
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".`,
|
|
4409
4426
|
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".`,
|
|
4410
|
-
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".`,
|
|
4427
|
+
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". An option may instead be \`{ label, move: { task, status } }\` \u2014 a MOVE of a task: clicking it is the person making that move, and the task's next stage owner is told for you. A wrapper carrying a move is ONE question about ONE task, and you are NOT woken when the move applies.`,
|
|
4411
4428
|
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.`,
|
|
4412
4429
|
"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".`,
|
|
4413
4430
|
"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".`,
|
|
4414
|
-
"
|
|
4431
|
+
"options.label": `What the option SAYS. The destination stage is drawn beside it as a pill, so the label is the decision (\`Accept\`), not a restatement of the move.`,
|
|
4432
|
+
"options.move": `Makes this option a MOVE of a task: clicking it is the person making that move, recorded as theirs. The next stage's owner is told for you, and you are not woken.`,
|
|
4433
|
+
"options.move.task": `The task the move applies to \u2014 its reference (\`CT12\`) or its uuid. Every move in one ask names the SAME task.`,
|
|
4434
|
+
"options.move.status": `The stage the task goes to when the person clicks this option. A move the task cannot make from where it is now is refused when you raise the ask, by name.`,
|
|
4435
|
+
"questions.options.label": `What the option SAYS. The destination stage is drawn beside it as a pill, so the label is the decision (\`Accept\`), not a restatement of the move.`,
|
|
4436
|
+
"questions.options.move": `See \`options.move\`. An ask carrying a move has exactly one question.`,
|
|
4437
|
+
"questions.options.move.task": `The task the move applies to \u2014 its reference (\`CT12\`) or its uuid. Every move in one ask names the SAME task.`,
|
|
4438
|
+
"questions.options.move.status": `The stage the task goes to when the person clicks this option. A move the task cannot make from where it is now is refused when you raise the ask, by name.`,
|
|
4439
|
+
"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". An option may instead be \`{ label, move: { task, status } }\` \u2014 see the single-question form; an ask carrying a move has exactly one question.`,
|
|
4415
4440
|
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.",
|
|
4416
4441
|
"replaces.askId": "The id of the open ask to withdraw.",
|
|
4417
4442
|
"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.`
|
|
@@ -6649,10 +6674,15 @@ var CODEX_ADDENDUM = [
|
|
|
6649
6674
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
6650
6675
|
"error.",
|
|
6651
6676
|
turnControlPromptGuidance("mcp__cabane_companion__"),
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6677
|
+
// CT1552: every ancillary tool is written the way Codex's own tool list shows
|
|
6678
|
+
// it — qualified. The bare `read_binary` / `upload` / … this list used to mix
|
|
6679
|
+
// in sat two lines under the instruction to invoke the exact qualified names,
|
|
6680
|
+
// so the sentence contradicted itself. The bare `query` / `mutate` below stay:
|
|
6681
|
+
// that sentence is about operation names, after the qualified forms are given.
|
|
6682
|
+
"The ancillary tools are `mcp__cabane__list_workspaces`, `mcp__cabane__read_binary`,",
|
|
6683
|
+
"`mcp__cabane__upload`, `mcp__cabane__begin_upload`, `mcp__cabane__finalize_upload` and",
|
|
6684
|
+
"`mcp__cabane__mint_render_token` \u2014 all possibly deferred too.",
|
|
6685
|
+
"There is no Cabane `read`/`write`/`search` tool \u2014 those are",
|
|
6656
6686
|
"operation NAMES you pass to `query` / `mutate`. One more harness fact: you can interleave",
|
|
6657
6687
|
"narration",
|
|
6658
6688
|
"with tool calls, but your final message is the turn's answer \u2014 write your",
|
|
@@ -8388,12 +8418,34 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8388
8418
|
targetUserId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
8389
8419
|
question: z13.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
8390
8420
|
headline: z13.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
8391
|
-
options: z13.array(
|
|
8421
|
+
options: z13.array(
|
|
8422
|
+
z13.union([
|
|
8423
|
+
z13.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
8424
|
+
z13.object({
|
|
8425
|
+
label: z13.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
|
|
8426
|
+
move: z13.object({
|
|
8427
|
+
task: z13.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
|
|
8428
|
+
status: z13.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
|
|
8429
|
+
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
|
|
8430
|
+
})
|
|
8431
|
+
])
|
|
8432
|
+
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
8392
8433
|
questions: z13.array(
|
|
8393
8434
|
z13.object({
|
|
8394
8435
|
headline: z13.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
8395
8436
|
body: z13.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
8396
|
-
options: z13.array(
|
|
8437
|
+
options: z13.array(
|
|
8438
|
+
z13.union([
|
|
8439
|
+
z13.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
8440
|
+
z13.object({
|
|
8441
|
+
label: z13.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
|
|
8442
|
+
move: z13.object({
|
|
8443
|
+
task: z13.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
|
|
8444
|
+
status: z13.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
|
|
8445
|
+
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
|
|
8446
|
+
})
|
|
8447
|
+
])
|
|
8448
|
+
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
8397
8449
|
})
|
|
8398
8450
|
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
8399
8451
|
replaces: z13.object({
|
|
@@ -8604,6 +8656,68 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
8604
8656
|
}
|
|
8605
8657
|
}
|
|
8606
8658
|
|
|
8659
|
+
// src/prepare-failure.ts
|
|
8660
|
+
import { spawn as spawn4 } from "child_process";
|
|
8661
|
+
import { hostname as hostname2 } from "os";
|
|
8662
|
+
function prepareFailureCause(error) {
|
|
8663
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8664
|
+
if (/timed out/i.test(message)) return "prepare hook timed out";
|
|
8665
|
+
if (/MODULE_NOT_FOUND|Cannot find (?:module|package)/.test(message))
|
|
8666
|
+
return "prepare hook module missing; check the configured command and its installation for this device";
|
|
8667
|
+
if (/not valid JSON|JSON must carry|produced no output/.test(message))
|
|
8668
|
+
return "prepare hook returned malformed output; expected a cwd path or JSON with a non-empty cwd";
|
|
8669
|
+
if (/ENOENT/.test(message)) return "prepare hook executable missing (ENOENT)";
|
|
8670
|
+
if (/EACCES/.test(message)) return "prepare hook executable is not accessible (EACCES)";
|
|
8671
|
+
const exit = message.match(/exited with code (\d+)/)?.[1];
|
|
8672
|
+
return exit ? `prepare hook exited with code ${exit}` : "prepare hook failed; inspect the device-local hook log";
|
|
8673
|
+
}
|
|
8674
|
+
async function runPrepareWithReporting(runner, hook, input, context, reportError) {
|
|
8675
|
+
try {
|
|
8676
|
+
return await runner(hook, input);
|
|
8677
|
+
} catch (error) {
|
|
8678
|
+
if (hook.failureReporter) {
|
|
8679
|
+
try {
|
|
8680
|
+
await reportFailure(hook.failureReporter, {
|
|
8681
|
+
workspaceId: input.workspaceId,
|
|
8682
|
+
conversationId: input.conversationId,
|
|
8683
|
+
agentId: input.agentId,
|
|
8684
|
+
agentUsername: input.agentUsername,
|
|
8685
|
+
turnId: context.turnId,
|
|
8686
|
+
deviceId: context.deviceId ?? hostname2(),
|
|
8687
|
+
cause: prepareFailureCause(error)
|
|
8688
|
+
});
|
|
8689
|
+
} catch {
|
|
8690
|
+
reportError();
|
|
8691
|
+
}
|
|
8692
|
+
}
|
|
8693
|
+
throw error;
|
|
8694
|
+
}
|
|
8695
|
+
}
|
|
8696
|
+
function reportFailure(config, body) {
|
|
8697
|
+
return new Promise((resolve2, reject) => {
|
|
8698
|
+
const child = spawn4(config.command, config.args ?? [], {
|
|
8699
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
8700
|
+
env: { ...process.env, ...config.env }
|
|
8701
|
+
});
|
|
8702
|
+
const timer = setTimeout(() => {
|
|
8703
|
+
child.kill("SIGKILL");
|
|
8704
|
+
reject(new Error("reporter timeout"));
|
|
8705
|
+
}, config.timeoutMs ?? 15e3);
|
|
8706
|
+
child.stdin.on("error", () => {
|
|
8707
|
+
});
|
|
8708
|
+
child.once("error", () => {
|
|
8709
|
+
clearTimeout(timer);
|
|
8710
|
+
reject(new Error("reporter failed to start"));
|
|
8711
|
+
});
|
|
8712
|
+
child.once("close", (code) => {
|
|
8713
|
+
clearTimeout(timer);
|
|
8714
|
+
if (code === 0) resolve2();
|
|
8715
|
+
else reject(new Error("reporter failed"));
|
|
8716
|
+
});
|
|
8717
|
+
child.stdin.end(JSON.stringify(body));
|
|
8718
|
+
});
|
|
8719
|
+
}
|
|
8720
|
+
|
|
8607
8721
|
// src/prepared.ts
|
|
8608
8722
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
8609
8723
|
import { join as join12 } from "path";
|
|
@@ -8834,7 +8948,7 @@ function pruneOld(dir2, retain) {
|
|
|
8834
8948
|
}
|
|
8835
8949
|
|
|
8836
8950
|
// src/turn-containment.ts
|
|
8837
|
-
import { execFile, execFileSync, spawn as
|
|
8951
|
+
import { execFile, execFileSync, spawn as spawn5 } from "child_process";
|
|
8838
8952
|
import { readFileSync as readFileSync8 } from "fs";
|
|
8839
8953
|
import { promisify } from "util";
|
|
8840
8954
|
var execFileAsync = promisify(execFile);
|
|
@@ -8916,7 +9030,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8916
9030
|
command: options.command
|
|
8917
9031
|
});
|
|
8918
9032
|
return asContained(
|
|
8919
|
-
|
|
9033
|
+
spawn5(
|
|
8920
9034
|
"systemd-run",
|
|
8921
9035
|
[
|
|
8922
9036
|
"--user",
|
|
@@ -8943,7 +9057,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8943
9057
|
);
|
|
8944
9058
|
}
|
|
8945
9059
|
function spawnInGroup(options) {
|
|
8946
|
-
const child =
|
|
9060
|
+
const child = spawn5(options.command, options.args, {
|
|
8947
9061
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
8948
9062
|
env: options.env,
|
|
8949
9063
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -9645,18 +9759,28 @@ var TurnExecution = class {
|
|
|
9645
9759
|
if (prepareHook.validateCached) {
|
|
9646
9760
|
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
9647
9761
|
try {
|
|
9648
|
-
await
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9762
|
+
await runPrepareWithReporting(
|
|
9763
|
+
runHook,
|
|
9764
|
+
prepareHook,
|
|
9765
|
+
{
|
|
9766
|
+
workspaceId,
|
|
9767
|
+
conversationId: payload.conversationId,
|
|
9768
|
+
agentId: payload.agentId,
|
|
9769
|
+
agentUsername: this.opts.agentUsername,
|
|
9770
|
+
runtime: this.turnContext.runtime,
|
|
9771
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
9772
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9773
|
+
title: this.turnContext.conversation.title,
|
|
9774
|
+
messageBody: this.turnContext.message.body,
|
|
9775
|
+
prepared: cached2
|
|
9776
|
+
},
|
|
9777
|
+
{ turnId, deviceId: this.opts.deviceId },
|
|
9778
|
+
() => {
|
|
9779
|
+
turnLog.error(
|
|
9780
|
+
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
9781
|
+
);
|
|
9782
|
+
}
|
|
9783
|
+
);
|
|
9660
9784
|
} catch (err) {
|
|
9661
9785
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9662
9786
|
turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
@@ -9692,24 +9816,34 @@ var TurnExecution = class {
|
|
|
9692
9816
|
preparingTimer.unref?.();
|
|
9693
9817
|
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
9694
9818
|
try {
|
|
9695
|
-
const result = await
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9819
|
+
const result = await runPrepareWithReporting(
|
|
9820
|
+
runHook,
|
|
9821
|
+
prepareHook,
|
|
9822
|
+
{
|
|
9823
|
+
workspaceId,
|
|
9824
|
+
conversationId: payload.conversationId,
|
|
9825
|
+
agentId: payload.agentId,
|
|
9826
|
+
agentUsername: this.opts.agentUsername,
|
|
9827
|
+
runtime: this.turnContext.runtime,
|
|
9828
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
9829
|
+
// CT317/CT319: the trigger message's referenced-entry paths — what the
|
|
9830
|
+
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
9831
|
+
// an older API. The conversation anchor is gone (CT319).
|
|
9832
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9833
|
+
title: this.turnContext.conversation.title,
|
|
9834
|
+
// CT943: the dispatching message's text — where an `env:` directive
|
|
9835
|
+
// rides. The server has always sent the trigger body on the turn
|
|
9836
|
+
// context (for the live feed); this is the first thing to read it as
|
|
9837
|
+
// an INPUT, so a dispatch can ask for its environment in words.
|
|
9838
|
+
messageBody: this.turnContext.message.body
|
|
9839
|
+
},
|
|
9840
|
+
{ turnId, deviceId: this.opts.deviceId },
|
|
9841
|
+
() => {
|
|
9842
|
+
turnLog.error(
|
|
9843
|
+
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
9844
|
+
);
|
|
9845
|
+
}
|
|
9846
|
+
);
|
|
9713
9847
|
clearTimeout(preparingTimer);
|
|
9714
9848
|
if (preparingStarted) reportPreparing("done");
|
|
9715
9849
|
writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
|
|
@@ -10870,7 +11004,7 @@ var CompanionSupervisor = class {
|
|
|
10870
11004
|
async start() {
|
|
10871
11005
|
this.log.info(
|
|
10872
11006
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
10873
|
-
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ??
|
|
11007
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname3()}`
|
|
10874
11008
|
);
|
|
10875
11009
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
10876
11010
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
@@ -10889,7 +11023,7 @@ var CompanionSupervisor = class {
|
|
|
10889
11023
|
token: this.config.deviceToken,
|
|
10890
11024
|
log: this.log,
|
|
10891
11025
|
lastEventId: null,
|
|
10892
|
-
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ??
|
|
11026
|
+
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname3()}`),
|
|
10893
11027
|
onMessage: async (ev) => {
|
|
10894
11028
|
if (ev.event === "assignments_changed") {
|
|
10895
11029
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -11305,6 +11439,7 @@ var CompanionSupervisor = class {
|
|
|
11305
11439
|
});
|
|
11306
11440
|
if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
|
|
11307
11441
|
return new Dispatcher({
|
|
11442
|
+
deviceId: this.deviceId ?? void 0,
|
|
11308
11443
|
api: ctx.api,
|
|
11309
11444
|
baseUrl: ctx.baseUrl,
|
|
11310
11445
|
workspaceId: ctx.workspaceId,
|
|
@@ -11952,9 +12087,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
11952
12087
|
}
|
|
11953
12088
|
function defaultReexec() {
|
|
11954
12089
|
clearRuntimeState();
|
|
11955
|
-
void import("child_process").then(({ spawn:
|
|
12090
|
+
void import("child_process").then(({ spawn: spawn7 }) => {
|
|
11956
12091
|
try {
|
|
11957
|
-
const child =
|
|
12092
|
+
const child = spawn7(process.execPath, process.argv.slice(1), {
|
|
11958
12093
|
stdio: "inherit",
|
|
11959
12094
|
detached: false
|
|
11960
12095
|
});
|
|
@@ -12209,7 +12344,7 @@ async function closeSurfaces(control, dashboard) {
|
|
|
12209
12344
|
}
|
|
12210
12345
|
|
|
12211
12346
|
// src/commands/daemon.ts
|
|
12212
|
-
import { spawn as
|
|
12347
|
+
import { spawn as spawn6 } from "child_process";
|
|
12213
12348
|
import { closeSync as closeSync3, mkdirSync as mkdirSync12, openSync as openSync3 } from "fs";
|
|
12214
12349
|
|
|
12215
12350
|
// src/cli-entry.ts
|
|
@@ -12321,7 +12456,7 @@ function defaultSpawnDetached(args) {
|
|
|
12321
12456
|
mkdirSync12(cabaneDir(), { recursive: true });
|
|
12322
12457
|
const logFd = openSync3(companionLogPath(), "a");
|
|
12323
12458
|
try {
|
|
12324
|
-
return
|
|
12459
|
+
return spawn6(process.execPath, [cliPath, ...args], {
|
|
12325
12460
|
detached: true,
|
|
12326
12461
|
stdio: ["ignore", logFd, logFd],
|
|
12327
12462
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -12489,7 +12624,7 @@ function prePairFoundPhrase(harness) {
|
|
|
12489
12624
|
}
|
|
12490
12625
|
async function pairHere(opts, interactive) {
|
|
12491
12626
|
const baseUrl = resolvePairBaseUrl(opts.server);
|
|
12492
|
-
const label = deviceLabelFromHostname(
|
|
12627
|
+
const label = deviceLabelFromHostname(hostname4());
|
|
12493
12628
|
const aborter = new AbortController();
|
|
12494
12629
|
const onSigint = () => aborter.abort();
|
|
12495
12630
|
process.on("SIGINT", onSigint);
|
package/dist/pairing-config.js
CHANGED
|
@@ -76,7 +76,15 @@ var prepareHookSchema = z.object({
|
|
|
76
76
|
//
|
|
77
77
|
// Default OFF, and deliberately: an operator hook that provisions
|
|
78
78
|
// unconditionally must not start running on every turn because it upgraded.
|
|
79
|
-
validateCached: z.boolean().optional()
|
|
79
|
+
validateCached: z.boolean().optional(),
|
|
80
|
+
// Independent operator-owned reporter. Receives sanitized failure metadata
|
|
81
|
+
// on stdin; never re-enters this hook and cannot change the turn outcome.
|
|
82
|
+
failureReporter: z.object({
|
|
83
|
+
command: z.string().min(1),
|
|
84
|
+
args: z.array(z.string()).optional(),
|
|
85
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
86
|
+
timeoutMs: z.number().int().positive().max(3e4).optional()
|
|
87
|
+
}).strict().optional()
|
|
80
88
|
}).strict();
|
|
81
89
|
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
82
90
|
var prepareResultSchema = z.object({
|
package/dist/runtime.js
CHANGED
|
@@ -85,7 +85,15 @@ var prepareHookSchema = z.object({
|
|
|
85
85
|
//
|
|
86
86
|
// Default OFF, and deliberately: an operator hook that provisions
|
|
87
87
|
// unconditionally must not start running on every turn because it upgraded.
|
|
88
|
-
validateCached: z.boolean().optional()
|
|
88
|
+
validateCached: z.boolean().optional(),
|
|
89
|
+
// Independent operator-owned reporter. Receives sanitized failure metadata
|
|
90
|
+
// on stdin; never re-enters this hook and cannot change the turn outcome.
|
|
91
|
+
failureReporter: z.object({
|
|
92
|
+
command: z.string().min(1),
|
|
93
|
+
args: z.array(z.string()).optional(),
|
|
94
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
95
|
+
timeoutMs: z.number().int().positive().max(3e4).optional()
|
|
96
|
+
}).strict().optional()
|
|
89
97
|
}).strict();
|
|
90
98
|
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
91
99
|
var prepareResultSchema = z.object({
|
|
@@ -1996,7 +2004,7 @@ async function verifyRuntime(state, requestImpl = controlRequest) {
|
|
|
1996
2004
|
}
|
|
1997
2005
|
|
|
1998
2006
|
// src/supervisor.ts
|
|
1999
|
-
import { hostname } from "os";
|
|
2007
|
+
import { hostname as hostname2 } from "os";
|
|
2000
2008
|
|
|
2001
2009
|
// packages/agent-runtime/src/failure.ts
|
|
2002
2010
|
import { z as z3 } from "zod";
|
|
@@ -3796,6 +3804,15 @@ var MAX_ASK_QUESTION_BYTES = 400;
|
|
|
3796
3804
|
var MAX_ASK_HEADLINE_BYTES = 120;
|
|
3797
3805
|
var MAX_ASK_ITEMS = 20;
|
|
3798
3806
|
var MAX_ASK_OPTION_BYTES = 200;
|
|
3807
|
+
var TASK_STAGES = [
|
|
3808
|
+
"proposed",
|
|
3809
|
+
"todo",
|
|
3810
|
+
"in_progress",
|
|
3811
|
+
"in_review",
|
|
3812
|
+
"done",
|
|
3813
|
+
"dropped",
|
|
3814
|
+
"deferred"
|
|
3815
|
+
];
|
|
3799
3816
|
var MIN_ASK_OPTIONS = 2;
|
|
3800
3817
|
var MAX_ASK_OPTIONS = 4;
|
|
3801
3818
|
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
@@ -3820,11 +3837,19 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
3820
3837
|
targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
|
|
3821
3838
|
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".`,
|
|
3822
3839
|
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".`,
|
|
3823
|
-
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".`,
|
|
3840
|
+
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". An option may instead be \`{ label, move: { task, status } }\` \u2014 a MOVE of a task: clicking it is the person making that move, and the task's next stage owner is told for you. A wrapper carrying a move is ONE question about ONE task, and you are NOT woken when the move applies.`,
|
|
3824
3841
|
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.`,
|
|
3825
3842
|
"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".`,
|
|
3826
3843
|
"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".`,
|
|
3827
|
-
"
|
|
3844
|
+
"options.label": `What the option SAYS. The destination stage is drawn beside it as a pill, so the label is the decision (\`Accept\`), not a restatement of the move.`,
|
|
3845
|
+
"options.move": `Makes this option a MOVE of a task: clicking it is the person making that move, recorded as theirs. The next stage's owner is told for you, and you are not woken.`,
|
|
3846
|
+
"options.move.task": `The task the move applies to \u2014 its reference (\`CT12\`) or its uuid. Every move in one ask names the SAME task.`,
|
|
3847
|
+
"options.move.status": `The stage the task goes to when the person clicks this option. A move the task cannot make from where it is now is refused when you raise the ask, by name.`,
|
|
3848
|
+
"questions.options.label": `What the option SAYS. The destination stage is drawn beside it as a pill, so the label is the decision (\`Accept\`), not a restatement of the move.`,
|
|
3849
|
+
"questions.options.move": `See \`options.move\`. An ask carrying a move has exactly one question.`,
|
|
3850
|
+
"questions.options.move.task": `The task the move applies to \u2014 its reference (\`CT12\`) or its uuid. Every move in one ask names the SAME task.`,
|
|
3851
|
+
"questions.options.move.status": `The stage the task goes to when the person clicks this option. A move the task cannot make from where it is now is refused when you raise the ask, by name.`,
|
|
3852
|
+
"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". An option may instead be \`{ label, move: { task, status } }\` \u2014 see the single-question form; an ask carrying a move has exactly one question.`,
|
|
3828
3853
|
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.",
|
|
3829
3854
|
"replaces.askId": "The id of the open ask to withdraw.",
|
|
3830
3855
|
"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.`
|
|
@@ -6062,10 +6087,15 @@ var CODEX_ADDENDUM = [
|
|
|
6062
6087
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
6063
6088
|
"error.",
|
|
6064
6089
|
turnControlPromptGuidance("mcp__cabane_companion__"),
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6090
|
+
// CT1552: every ancillary tool is written the way Codex's own tool list shows
|
|
6091
|
+
// it — qualified. The bare `read_binary` / `upload` / … this list used to mix
|
|
6092
|
+
// in sat two lines under the instruction to invoke the exact qualified names,
|
|
6093
|
+
// so the sentence contradicted itself. The bare `query` / `mutate` below stay:
|
|
6094
|
+
// that sentence is about operation names, after the qualified forms are given.
|
|
6095
|
+
"The ancillary tools are `mcp__cabane__list_workspaces`, `mcp__cabane__read_binary`,",
|
|
6096
|
+
"`mcp__cabane__upload`, `mcp__cabane__begin_upload`, `mcp__cabane__finalize_upload` and",
|
|
6097
|
+
"`mcp__cabane__mint_render_token` \u2014 all possibly deferred too.",
|
|
6098
|
+
"There is no Cabane `read`/`write`/`search` tool \u2014 those are",
|
|
6069
6099
|
"operation NAMES you pass to `query` / `mutate`. One more harness fact: you can interleave",
|
|
6070
6100
|
"narration",
|
|
6071
6101
|
"with tool calls, but your final message is the turn's answer \u2014 write your",
|
|
@@ -7811,12 +7841,34 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7811
7841
|
targetUserId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
7812
7842
|
question: z13.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
7813
7843
|
headline: z13.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
7814
|
-
options: z13.array(
|
|
7844
|
+
options: z13.array(
|
|
7845
|
+
z13.union([
|
|
7846
|
+
z13.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
7847
|
+
z13.object({
|
|
7848
|
+
label: z13.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
|
|
7849
|
+
move: z13.object({
|
|
7850
|
+
task: z13.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
|
|
7851
|
+
status: z13.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
|
|
7852
|
+
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
|
|
7853
|
+
})
|
|
7854
|
+
])
|
|
7855
|
+
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
7815
7856
|
questions: z13.array(
|
|
7816
7857
|
z13.object({
|
|
7817
7858
|
headline: z13.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
7818
7859
|
body: z13.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
7819
|
-
options: z13.array(
|
|
7860
|
+
options: z13.array(
|
|
7861
|
+
z13.union([
|
|
7862
|
+
z13.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
7863
|
+
z13.object({
|
|
7864
|
+
label: z13.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
|
|
7865
|
+
move: z13.object({
|
|
7866
|
+
task: z13.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
|
|
7867
|
+
status: z13.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
|
|
7868
|
+
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
|
|
7869
|
+
})
|
|
7870
|
+
])
|
|
7871
|
+
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
7820
7872
|
})
|
|
7821
7873
|
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
7822
7874
|
replaces: z13.object({
|
|
@@ -8027,6 +8079,68 @@ function readOutboxFloor(read, turnId, log) {
|
|
|
8027
8079
|
}
|
|
8028
8080
|
}
|
|
8029
8081
|
|
|
8082
|
+
// src/prepare-failure.ts
|
|
8083
|
+
import { spawn as spawn4 } from "child_process";
|
|
8084
|
+
import { hostname } from "os";
|
|
8085
|
+
function prepareFailureCause(error) {
|
|
8086
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8087
|
+
if (/timed out/i.test(message)) return "prepare hook timed out";
|
|
8088
|
+
if (/MODULE_NOT_FOUND|Cannot find (?:module|package)/.test(message))
|
|
8089
|
+
return "prepare hook module missing; check the configured command and its installation for this device";
|
|
8090
|
+
if (/not valid JSON|JSON must carry|produced no output/.test(message))
|
|
8091
|
+
return "prepare hook returned malformed output; expected a cwd path or JSON with a non-empty cwd";
|
|
8092
|
+
if (/ENOENT/.test(message)) return "prepare hook executable missing (ENOENT)";
|
|
8093
|
+
if (/EACCES/.test(message)) return "prepare hook executable is not accessible (EACCES)";
|
|
8094
|
+
const exit = message.match(/exited with code (\d+)/)?.[1];
|
|
8095
|
+
return exit ? `prepare hook exited with code ${exit}` : "prepare hook failed; inspect the device-local hook log";
|
|
8096
|
+
}
|
|
8097
|
+
async function runPrepareWithReporting(runner, hook, input, context, reportError) {
|
|
8098
|
+
try {
|
|
8099
|
+
return await runner(hook, input);
|
|
8100
|
+
} catch (error) {
|
|
8101
|
+
if (hook.failureReporter) {
|
|
8102
|
+
try {
|
|
8103
|
+
await reportFailure(hook.failureReporter, {
|
|
8104
|
+
workspaceId: input.workspaceId,
|
|
8105
|
+
conversationId: input.conversationId,
|
|
8106
|
+
agentId: input.agentId,
|
|
8107
|
+
agentUsername: input.agentUsername,
|
|
8108
|
+
turnId: context.turnId,
|
|
8109
|
+
deviceId: context.deviceId ?? hostname(),
|
|
8110
|
+
cause: prepareFailureCause(error)
|
|
8111
|
+
});
|
|
8112
|
+
} catch {
|
|
8113
|
+
reportError();
|
|
8114
|
+
}
|
|
8115
|
+
}
|
|
8116
|
+
throw error;
|
|
8117
|
+
}
|
|
8118
|
+
}
|
|
8119
|
+
function reportFailure(config, body) {
|
|
8120
|
+
return new Promise((resolve2, reject) => {
|
|
8121
|
+
const child = spawn4(config.command, config.args ?? [], {
|
|
8122
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
8123
|
+
env: { ...process.env, ...config.env }
|
|
8124
|
+
});
|
|
8125
|
+
const timer = setTimeout(() => {
|
|
8126
|
+
child.kill("SIGKILL");
|
|
8127
|
+
reject(new Error("reporter timeout"));
|
|
8128
|
+
}, config.timeoutMs ?? 15e3);
|
|
8129
|
+
child.stdin.on("error", () => {
|
|
8130
|
+
});
|
|
8131
|
+
child.once("error", () => {
|
|
8132
|
+
clearTimeout(timer);
|
|
8133
|
+
reject(new Error("reporter failed to start"));
|
|
8134
|
+
});
|
|
8135
|
+
child.once("close", (code) => {
|
|
8136
|
+
clearTimeout(timer);
|
|
8137
|
+
if (code === 0) resolve2();
|
|
8138
|
+
else reject(new Error("reporter failed"));
|
|
8139
|
+
});
|
|
8140
|
+
child.stdin.end(JSON.stringify(body));
|
|
8141
|
+
});
|
|
8142
|
+
}
|
|
8143
|
+
|
|
8030
8144
|
// src/prepared.ts
|
|
8031
8145
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
8032
8146
|
import { join as join12 } from "path";
|
|
@@ -8257,7 +8371,7 @@ function pruneOld(dir2, retain) {
|
|
|
8257
8371
|
}
|
|
8258
8372
|
|
|
8259
8373
|
// src/turn-containment.ts
|
|
8260
|
-
import { execFile, execFileSync, spawn as
|
|
8374
|
+
import { execFile, execFileSync, spawn as spawn5 } from "child_process";
|
|
8261
8375
|
import { readFileSync as readFileSync8 } from "fs";
|
|
8262
8376
|
import { promisify } from "util";
|
|
8263
8377
|
var execFileAsync = promisify(execFile);
|
|
@@ -8339,7 +8453,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8339
8453
|
command: options.command
|
|
8340
8454
|
});
|
|
8341
8455
|
return asContained(
|
|
8342
|
-
|
|
8456
|
+
spawn5(
|
|
8343
8457
|
"systemd-run",
|
|
8344
8458
|
[
|
|
8345
8459
|
"--user",
|
|
@@ -8366,7 +8480,7 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8366
8480
|
);
|
|
8367
8481
|
}
|
|
8368
8482
|
function spawnInGroup(options) {
|
|
8369
|
-
const child =
|
|
8483
|
+
const child = spawn5(options.command, options.args, {
|
|
8370
8484
|
...options.cwd ? { cwd: options.cwd } : {},
|
|
8371
8485
|
env: options.env,
|
|
8372
8486
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -9068,18 +9182,28 @@ var TurnExecution = class {
|
|
|
9068
9182
|
if (prepareHook.validateCached) {
|
|
9069
9183
|
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
9070
9184
|
try {
|
|
9071
|
-
await
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9185
|
+
await runPrepareWithReporting(
|
|
9186
|
+
runHook,
|
|
9187
|
+
prepareHook,
|
|
9188
|
+
{
|
|
9189
|
+
workspaceId,
|
|
9190
|
+
conversationId: payload.conversationId,
|
|
9191
|
+
agentId: payload.agentId,
|
|
9192
|
+
agentUsername: this.opts.agentUsername,
|
|
9193
|
+
runtime: this.turnContext.runtime,
|
|
9194
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
9195
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9196
|
+
title: this.turnContext.conversation.title,
|
|
9197
|
+
messageBody: this.turnContext.message.body,
|
|
9198
|
+
prepared: cached2
|
|
9199
|
+
},
|
|
9200
|
+
{ turnId, deviceId: this.opts.deviceId },
|
|
9201
|
+
() => {
|
|
9202
|
+
turnLog.error(
|
|
9203
|
+
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
9204
|
+
);
|
|
9205
|
+
}
|
|
9206
|
+
);
|
|
9083
9207
|
} catch (err) {
|
|
9084
9208
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9085
9209
|
turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
@@ -9115,24 +9239,34 @@ var TurnExecution = class {
|
|
|
9115
9239
|
preparingTimer.unref?.();
|
|
9116
9240
|
const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
|
|
9117
9241
|
try {
|
|
9118
|
-
const result = await
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9242
|
+
const result = await runPrepareWithReporting(
|
|
9243
|
+
runHook,
|
|
9244
|
+
prepareHook,
|
|
9245
|
+
{
|
|
9246
|
+
workspaceId,
|
|
9247
|
+
conversationId: payload.conversationId,
|
|
9248
|
+
agentId: payload.agentId,
|
|
9249
|
+
agentUsername: this.opts.agentUsername,
|
|
9250
|
+
runtime: this.turnContext.runtime,
|
|
9251
|
+
hostAccess: this.turnContext.policy.hostFs,
|
|
9252
|
+
// CT317/CT319: the trigger message's referenced-entry paths — what the
|
|
9253
|
+
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
9254
|
+
// an older API. The conversation anchor is gone (CT319).
|
|
9255
|
+
triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
|
|
9256
|
+
title: this.turnContext.conversation.title,
|
|
9257
|
+
// CT943: the dispatching message's text — where an `env:` directive
|
|
9258
|
+
// rides. The server has always sent the trigger body on the turn
|
|
9259
|
+
// context (for the live feed); this is the first thing to read it as
|
|
9260
|
+
// an INPUT, so a dispatch can ask for its environment in words.
|
|
9261
|
+
messageBody: this.turnContext.message.body
|
|
9262
|
+
},
|
|
9263
|
+
{ turnId, deviceId: this.opts.deviceId },
|
|
9264
|
+
() => {
|
|
9265
|
+
turnLog.error(
|
|
9266
|
+
"dispatcher: prepare failure reporter failed; original failure preserved"
|
|
9267
|
+
);
|
|
9268
|
+
}
|
|
9269
|
+
);
|
|
9136
9270
|
clearTimeout(preparingTimer);
|
|
9137
9271
|
if (preparingStarted) reportPreparing("done");
|
|
9138
9272
|
writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
|
|
@@ -10293,7 +10427,7 @@ var CompanionSupervisor = class {
|
|
|
10293
10427
|
async start() {
|
|
10294
10428
|
this.log.info(
|
|
10295
10429
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
10296
|
-
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ??
|
|
10430
|
+
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
|
|
10297
10431
|
);
|
|
10298
10432
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
10299
10433
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
@@ -10312,7 +10446,7 @@ var CompanionSupervisor = class {
|
|
|
10312
10446
|
token: this.config.deviceToken,
|
|
10313
10447
|
log: this.log,
|
|
10314
10448
|
lastEventId: null,
|
|
10315
|
-
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ??
|
|
10449
|
+
onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`),
|
|
10316
10450
|
onMessage: async (ev) => {
|
|
10317
10451
|
if (ev.event === "assignments_changed") {
|
|
10318
10452
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -10728,6 +10862,7 @@ var CompanionSupervisor = class {
|
|
|
10728
10862
|
});
|
|
10729
10863
|
if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
|
|
10730
10864
|
return new Dispatcher({
|
|
10865
|
+
deviceId: this.deviceId ?? void 0,
|
|
10731
10866
|
api: ctx.api,
|
|
10732
10867
|
baseUrl: ctx.baseUrl,
|
|
10733
10868
|
workspaceId: ctx.workspaceId,
|
|
@@ -11375,9 +11510,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
11375
11510
|
}
|
|
11376
11511
|
function defaultReexec() {
|
|
11377
11512
|
clearRuntimeState();
|
|
11378
|
-
void import("child_process").then(({ spawn:
|
|
11513
|
+
void import("child_process").then(({ spawn: spawn6 }) => {
|
|
11379
11514
|
try {
|
|
11380
|
-
const child =
|
|
11515
|
+
const child = spawn6(process.execPath, process.argv.slice(1), {
|
|
11381
11516
|
stdio: "inherit",
|
|
11382
11517
|
detached: false
|
|
11383
11518
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.108",
|
|
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",
|