@cabane/companion 0.6.40 → 0.6.42
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 +106 -89
- package/dist/runtime.js +26 -26
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -819,7 +819,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
819
819
|
var HARNESS_LABELS = {
|
|
820
820
|
"claude-code": "Claude Code",
|
|
821
821
|
codex: "Codex",
|
|
822
|
-
opencode: "
|
|
822
|
+
opencode: "OpenCode"
|
|
823
823
|
};
|
|
824
824
|
var LABELS = HARNESS_LABELS;
|
|
825
825
|
var OFFER_ORDER = ["claude-code", "codex", "opencode"];
|
|
@@ -949,9 +949,6 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
949
949
|
enable: "opencode"
|
|
950
950
|
};
|
|
951
951
|
}
|
|
952
|
-
function detectedRuntimesFor(snapshot) {
|
|
953
|
-
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
954
|
-
}
|
|
955
952
|
var PROBE_TIMEOUT_MS = 4e3;
|
|
956
953
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
957
954
|
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
@@ -1198,8 +1195,6 @@ function blank() {
|
|
|
1198
1195
|
process.stdout.write("\n");
|
|
1199
1196
|
}
|
|
1200
1197
|
var tick = (rest) => `${pc.green("\u2713")} ${rest}`;
|
|
1201
|
-
var arrow = (rest) => `${pc.yellow("\u2192")} ${rest}`;
|
|
1202
|
-
var bang = (rest) => `${pc.yellow("!")} ${rest}`;
|
|
1203
1198
|
function tildePath(path) {
|
|
1204
1199
|
const home = homedir2();
|
|
1205
1200
|
if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
|
|
@@ -7449,16 +7444,22 @@ async function proveWorkspaceTools(req, runtime, opts = {}) {
|
|
|
7449
7444
|
"x-cabane-active-conversation": req.cabane.activeConversationId
|
|
7450
7445
|
};
|
|
7451
7446
|
try {
|
|
7452
|
-
const initialized = await initializeWithRetry(
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7447
|
+
const initialized = await initializeWithRetry(
|
|
7448
|
+
fetchImpl,
|
|
7449
|
+
req.cabane.mcpUrl,
|
|
7450
|
+
headers,
|
|
7451
|
+
{
|
|
7452
|
+
jsonrpc: "2.0",
|
|
7453
|
+
id: 1,
|
|
7454
|
+
method: "initialize",
|
|
7455
|
+
params: {
|
|
7456
|
+
protocolVersion: "2025-03-26",
|
|
7457
|
+
capabilities: {},
|
|
7458
|
+
clientInfo: { name: "cabane-companion-readiness", version: "1" }
|
|
7459
|
+
}
|
|
7460
|
+
},
|
|
7461
|
+
opts.retryDelaysMs ?? INITIALIZE_RETRY_DELAYS_MS
|
|
7462
|
+
);
|
|
7462
7463
|
if (initialized.status === 401 || initialized.status === 403)
|
|
7463
7464
|
return fail(base, "authentication_failed", `initialize returned HTTP ${initialized.status}`);
|
|
7464
7465
|
if (initialized.transient)
|
|
@@ -7502,10 +7503,10 @@ async function proveWorkspaceTools(req, runtime, opts = {}) {
|
|
|
7502
7503
|
);
|
|
7503
7504
|
}
|
|
7504
7505
|
}
|
|
7505
|
-
async function initializeWithRetry(fetchImpl, url, headers, body) {
|
|
7506
|
+
async function initializeWithRetry(fetchImpl, url, headers, body, retryDelaysMs) {
|
|
7506
7507
|
let lastFailure = null;
|
|
7507
7508
|
const deadline = Date.now() + INITIALIZE_RETRY_BUDGET_MS;
|
|
7508
|
-
for (let attempt = 0; attempt <=
|
|
7509
|
+
for (let attempt = 0; attempt <= retryDelaysMs.length; attempt += 1) {
|
|
7509
7510
|
try {
|
|
7510
7511
|
const remainingMs = deadline - Date.now();
|
|
7511
7512
|
if (remainingMs <= 0) break;
|
|
@@ -7529,7 +7530,7 @@ async function initializeWithRetry(fetchImpl, url, headers, body) {
|
|
|
7529
7530
|
transient: true
|
|
7530
7531
|
};
|
|
7531
7532
|
}
|
|
7532
|
-
const retryDelayMs =
|
|
7533
|
+
const retryDelayMs = retryDelaysMs[attempt];
|
|
7533
7534
|
if (retryDelayMs === void 0 || Date.now() + retryDelayMs >= deadline) break;
|
|
7534
7535
|
await delay(retryDelayMs);
|
|
7535
7536
|
}
|
|
@@ -8165,6 +8166,7 @@ ${reason}`,
|
|
|
8165
8166
|
}
|
|
8166
8167
|
const proof = await proveWorkspaceTools(request, adapter.name, {
|
|
8167
8168
|
...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
|
|
8169
|
+
...this.opts.workspaceProofRetryDelaysMs ? { retryDelaysMs: this.opts.workspaceProofRetryDelaysMs } : {},
|
|
8168
8170
|
harnessFingerprint: turnContext.runtime
|
|
8169
8171
|
});
|
|
8170
8172
|
turnLog[proof.ok ? "info" : "error"](
|
|
@@ -8982,6 +8984,7 @@ var ASSIGNMENTS_POLL_MS = 6e4;
|
|
|
8982
8984
|
var DRAIN_BASE_MS = 1e3;
|
|
8983
8985
|
var DRAIN_MAX_MS = 3e4;
|
|
8984
8986
|
var DRAIN_IDLE_MS = 15e3;
|
|
8987
|
+
var DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS = [0, 1e3, 3e3];
|
|
8985
8988
|
var CompanionSupervisor = class {
|
|
8986
8989
|
workspaces = /* @__PURE__ */ new Map();
|
|
8987
8990
|
config;
|
|
@@ -9008,6 +9011,7 @@ var CompanionSupervisor = class {
|
|
|
9008
9011
|
exitFn;
|
|
9009
9012
|
reexecFn;
|
|
9010
9013
|
dispatcherFactory;
|
|
9014
|
+
unrunnableRetryDelaysMs;
|
|
9011
9015
|
deviceApi = null;
|
|
9012
9016
|
deviceSub = null;
|
|
9013
9017
|
heartbeatTimer = null;
|
|
@@ -9040,6 +9044,7 @@ var CompanionSupervisor = class {
|
|
|
9040
9044
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
9041
9045
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
9042
9046
|
this.dispatcherFactory = opts.dispatcherFactory;
|
|
9047
|
+
this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
|
|
9043
9048
|
}
|
|
9044
9049
|
// Stand up the data plane: pair check, initial assignments pull, then the
|
|
9045
9050
|
// heartbeat + poll loops. A companion with no device token (logged out) does
|
|
@@ -9140,13 +9145,7 @@ var CompanionSupervisor = class {
|
|
|
9140
9145
|
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
9141
9146
|
// A null (probe failed / no opencode) omits the field, and the server then
|
|
9142
9147
|
// leaves this device's stored availability untouched.
|
|
9143
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
9144
|
-
// CT1082: what this machine has that the user hasn't connected, so the web
|
|
9145
|
-
// UI can offer it without a companion round-trip. Sent only once a probe has
|
|
9146
|
-
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
9147
|
-
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
9148
|
-
// the same fail-soft contract `models` keeps.
|
|
9149
|
-
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
9148
|
+
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
9150
9149
|
});
|
|
9151
9150
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
9152
9151
|
this.deviceId = res.deviceId;
|
|
@@ -9550,9 +9549,8 @@ var CompanionSupervisor = class {
|
|
|
9550
9549
|
// wedge the workspace's whole stream.
|
|
9551
9550
|
async reportUnrunnableDispatch(wr, payload) {
|
|
9552
9551
|
if (!this.deviceApi) return false;
|
|
9553
|
-
const backoffMs = [0, 1e3, 3e3];
|
|
9554
9552
|
let lastErr;
|
|
9555
|
-
for (const wait of
|
|
9553
|
+
for (const wait of this.unrunnableRetryDelaysMs) {
|
|
9556
9554
|
if (this.stopped) return false;
|
|
9557
9555
|
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
9558
9556
|
try {
|
|
@@ -10241,6 +10239,7 @@ async function startScript(opts, interactive) {
|
|
|
10241
10239
|
return;
|
|
10242
10240
|
}
|
|
10243
10241
|
blank();
|
|
10242
|
+
const held = !isDevicePaired() ? await collectHarnessChoices(interactive) : [];
|
|
10244
10243
|
let paired = null;
|
|
10245
10244
|
if (!isDevicePaired()) {
|
|
10246
10245
|
paired = await pairHere(opts, interactive);
|
|
@@ -10262,12 +10261,18 @@ async function startScript(opts, interactive) {
|
|
|
10262
10261
|
return;
|
|
10263
10262
|
}
|
|
10264
10263
|
const runtime = result.runtime;
|
|
10264
|
+
const connected = [];
|
|
10265
|
+
for (const choice of held) {
|
|
10266
|
+
if (!choice.accepted) continue;
|
|
10267
|
+
const outcome = await runtime.connectHarness(choice.runtime, choice.serverUrl);
|
|
10268
|
+
if (outcome.ok) connected.push(HARNESS_LABELS[choice.runtime]);
|
|
10269
|
+
else write(`${INDENT}${outcome.error}`);
|
|
10270
|
+
}
|
|
10265
10271
|
await runtime.heartbeatNow();
|
|
10266
|
-
const offered = await runConnectorOffer(runtime, { interactive, justPaired });
|
|
10267
10272
|
if (interactive && !opts.foreground) {
|
|
10268
10273
|
await handOffToBackground(runtime, {
|
|
10269
10274
|
justPaired,
|
|
10270
|
-
|
|
10275
|
+
connected
|
|
10271
10276
|
});
|
|
10272
10277
|
return;
|
|
10273
10278
|
}
|
|
@@ -10279,7 +10284,7 @@ async function startScript(opts, interactive) {
|
|
|
10279
10284
|
write(`${INDENT}Listening for messages\u2026`);
|
|
10280
10285
|
setConsoleLogging(true);
|
|
10281
10286
|
} else {
|
|
10282
|
-
|
|
10287
|
+
blank();
|
|
10283
10288
|
write(
|
|
10284
10289
|
`${INDENT}Cabane companion is running. No terminal attached, so it stays in the foreground.`
|
|
10285
10290
|
);
|
|
@@ -10301,11 +10306,60 @@ function reportAlreadyRunning(pid) {
|
|
|
10301
10306
|
write("Stop it first with `cabane-companion stop` if you want to relaunch.");
|
|
10302
10307
|
write("Connect a harness to the running companion: cabane-companion connect claude-code");
|
|
10303
10308
|
}
|
|
10309
|
+
async function collectHarnessChoices(interactive) {
|
|
10310
|
+
const opencodeUrl = "http://127.0.0.1:4096";
|
|
10311
|
+
const [claudePresent, claudeVersion, codexVersion, opencodeVersion] = await Promise.all([
|
|
10312
|
+
claudeOnPath().catch(() => false),
|
|
10313
|
+
probeCliVersion("claude").catch(() => null),
|
|
10314
|
+
probeCliVersion("codex").catch(() => null),
|
|
10315
|
+
probeOpencodeVersion(opencodeUrl).catch(() => null)
|
|
10316
|
+
]);
|
|
10317
|
+
const found = [
|
|
10318
|
+
...claudePresent ? [{ runtime: "claude-code", version: claudeVersion }] : [],
|
|
10319
|
+
...codexVersion ? [{ runtime: "codex", version: codexVersion }] : [],
|
|
10320
|
+
...opencodeVersion ? [{ runtime: "opencode", version: opencodeVersion, serverUrl: opencodeUrl }] : []
|
|
10321
|
+
].sort((a, b) => OFFER_ORDER.indexOf(a.runtime) - OFFER_ORDER.indexOf(b.runtime));
|
|
10322
|
+
if (found.length === 0) {
|
|
10323
|
+
write(
|
|
10324
|
+
`${INDENT}No coding agent found on this machine yet \u2014 you can pair now and add one later.`
|
|
10325
|
+
);
|
|
10326
|
+
blank();
|
|
10327
|
+
return [];
|
|
10328
|
+
}
|
|
10329
|
+
write(`${INDENT}Welcome to Cabane. Let's set up this machine \u2014 two steps, both right here:`);
|
|
10330
|
+
blank();
|
|
10331
|
+
write(`${DATA_INDENT}1. Choose which coding agents Cabane can use on this machine`);
|
|
10332
|
+
write(`${DATA_INDENT}2. Pair this machine with your Cabane account`);
|
|
10333
|
+
blank();
|
|
10334
|
+
const choices = [];
|
|
10335
|
+
for (const harness of found) {
|
|
10336
|
+
const phrase = prePairFoundPhrase(harness);
|
|
10337
|
+
if (!interactive) {
|
|
10338
|
+
write(`${INDENT}${phrase}. Add it with: ${connectCommand(harness.runtime)}`);
|
|
10339
|
+
choices.push({ ...harness, accepted: false });
|
|
10340
|
+
continue;
|
|
10341
|
+
}
|
|
10342
|
+
const accepted = await confirm(`${phrase}. Make it available in Cabane?`);
|
|
10343
|
+
choices.push({ ...harness, accepted });
|
|
10344
|
+
if (accepted) {
|
|
10345
|
+
write(
|
|
10346
|
+
INDENT + tick(`${HARNESS_LABELS[harness.runtime]} \u2014 available once this device is paired.`)
|
|
10347
|
+
);
|
|
10348
|
+
} else {
|
|
10349
|
+
write(`${INDENT}Skipped. Add it any time with: ${connectCommand(harness.runtime)}`);
|
|
10350
|
+
}
|
|
10351
|
+
blank();
|
|
10352
|
+
}
|
|
10353
|
+
return choices;
|
|
10354
|
+
}
|
|
10355
|
+
function prePairFoundPhrase(harness) {
|
|
10356
|
+
if (harness.runtime === "opencode") return `We found OpenCode at ${harness.serverUrl}`;
|
|
10357
|
+
const label = HARNESS_LABELS[harness.runtime];
|
|
10358
|
+
return harness.version ? `We found ${label} on this machine (${harness.version})` : `We found ${label} on this machine`;
|
|
10359
|
+
}
|
|
10304
10360
|
async function pairHere(opts, interactive) {
|
|
10305
10361
|
const baseUrl = resolvePairBaseUrl(opts.server);
|
|
10306
10362
|
const label = deviceLabelFromHostname(hostname2());
|
|
10307
|
-
write(`${INDENT}Not paired yet.`);
|
|
10308
|
-
blank();
|
|
10309
10363
|
const aborter = new AbortController();
|
|
10310
10364
|
const onSigint = () => aborter.abort();
|
|
10311
10365
|
process.on("SIGINT", onSigint);
|
|
@@ -10318,7 +10372,7 @@ async function pairHere(opts, interactive) {
|
|
|
10318
10372
|
if (attempt === 1) printFirstCode(code);
|
|
10319
10373
|
else printFreshCode(code);
|
|
10320
10374
|
spinner.start(
|
|
10321
|
-
`Waiting for you to confirm
|
|
10375
|
+
`Waiting for you to confirm in Cabane\u2026 (the code is good for ${minutes} minutes)`
|
|
10322
10376
|
);
|
|
10323
10377
|
try {
|
|
10324
10378
|
const device = await awaitEnrollment(baseUrl, code, { signal: aborter.signal });
|
|
@@ -10328,7 +10382,7 @@ async function pairHere(opts, interactive) {
|
|
|
10328
10382
|
if (err instanceof EnrollmentCancelledError) {
|
|
10329
10383
|
spinner.clear();
|
|
10330
10384
|
blank();
|
|
10331
|
-
write(`${INDENT}Pairing cancelled. Run
|
|
10385
|
+
write(`${INDENT}Pairing cancelled. Run cabane-companion start when you're ready.`);
|
|
10332
10386
|
return null;
|
|
10333
10387
|
}
|
|
10334
10388
|
if (!(err instanceof EnrollmentExpiredError)) throw err;
|
|
@@ -10339,8 +10393,9 @@ async function pairHere(opts, interactive) {
|
|
|
10339
10393
|
}
|
|
10340
10394
|
spinner.freeze();
|
|
10341
10395
|
blank();
|
|
10342
|
-
write(
|
|
10343
|
-
|
|
10396
|
+
write(
|
|
10397
|
+
`${INDENT}Still not confirmed after ${minutesWaited} minutes \u2014 stopping here. Run cabane-companion start again when you're ready.`
|
|
10398
|
+
);
|
|
10344
10399
|
return null;
|
|
10345
10400
|
}
|
|
10346
10401
|
}
|
|
@@ -10350,11 +10405,11 @@ async function pairHere(opts, interactive) {
|
|
|
10350
10405
|
}
|
|
10351
10406
|
}
|
|
10352
10407
|
function printFirstCode(code) {
|
|
10353
|
-
write(`${INDENT}Enter this code in Cabane:`);
|
|
10408
|
+
write(`${INDENT}Now let's pair this machine. Enter this code in Cabane:`);
|
|
10354
10409
|
blank();
|
|
10355
10410
|
write(`${DATA_INDENT}${code.userCode}`);
|
|
10356
10411
|
blank();
|
|
10357
|
-
write(`${INDENT}or
|
|
10412
|
+
write(`${INDENT}or, if Cabane isn\u2019t open:`);
|
|
10358
10413
|
blank();
|
|
10359
10414
|
write(`${DATA_INDENT}${code.verificationUriComplete}`);
|
|
10360
10415
|
blank();
|
|
@@ -10369,54 +10424,6 @@ function printFreshCode(code) {
|
|
|
10369
10424
|
function possessive(ownerName) {
|
|
10370
10425
|
return ownerName ? `${ownerName}'s` : "your Cabane";
|
|
10371
10426
|
}
|
|
10372
|
-
async function runConnectorOffer(runtime, ctx) {
|
|
10373
|
-
const snapshot = runtime.harnesses();
|
|
10374
|
-
const detected = OFFER_ORDER.map((r) => snapshot.find((h) => h.runtime === r)).filter(
|
|
10375
|
-
(h) => !!h && h.state === "detected_not_exposed"
|
|
10376
|
-
);
|
|
10377
|
-
if (detected.length === 0) {
|
|
10378
|
-
if (!ctx.justPaired) return { printedSomething: false };
|
|
10379
|
-
blank();
|
|
10380
|
-
write(
|
|
10381
|
-
INDENT + bang(
|
|
10382
|
-
"No coding agent found on this machine yet \u2014 install Claude Code, Codex or opencode and sign in,"
|
|
10383
|
-
)
|
|
10384
|
-
);
|
|
10385
|
-
write(`${INDENT} then: cabane-companion connect claude-code (or codex, or opencode)`);
|
|
10386
|
-
return { printedSomething: true };
|
|
10387
|
-
}
|
|
10388
|
-
blank();
|
|
10389
|
-
if (!ctx.interactive) {
|
|
10390
|
-
for (const h of detected) {
|
|
10391
|
-
write(
|
|
10392
|
-
`${INDENT}${foundPhrase(h, runtime.config)} but it isn't connected. Connect it: ${connectCommand(h.runtime)}`
|
|
10393
|
-
);
|
|
10394
|
-
}
|
|
10395
|
-
return { printedSomething: true };
|
|
10396
|
-
}
|
|
10397
|
-
for (const h of detected) {
|
|
10398
|
-
const yes = await confirm(`${foundPhrase(h, runtime.config)}. Connect it to Cabane?`);
|
|
10399
|
-
if (!yes) {
|
|
10400
|
-
write(`${INDENT}Skipped. Connect it later with: ${connectCommand(h.runtime)}`);
|
|
10401
|
-
continue;
|
|
10402
|
-
}
|
|
10403
|
-
const outcome = await runtime.connectHarness(h.runtime);
|
|
10404
|
-
if (!outcome.ok) {
|
|
10405
|
-
write(`${INDENT}${outcome.error}`);
|
|
10406
|
-
continue;
|
|
10407
|
-
}
|
|
10408
|
-
write(INDENT + tick(outcome.message));
|
|
10409
|
-
}
|
|
10410
|
-
return { printedSomething: true };
|
|
10411
|
-
}
|
|
10412
|
-
function foundPhrase(h, cfg) {
|
|
10413
|
-
const label = HARNESS_LABELS[h.runtime];
|
|
10414
|
-
if (h.runtime === "opencode") {
|
|
10415
|
-
const url = cfg.opencode?.serverUrl;
|
|
10416
|
-
return url ? `We found ${label} at ${url}` : `We found ${label} on this machine`;
|
|
10417
|
-
}
|
|
10418
|
-
return h.version ? `We found ${label} on this machine (${h.version})` : `We found ${label} on this machine`;
|
|
10419
|
-
}
|
|
10420
10427
|
function connectCommand(runtime) {
|
|
10421
10428
|
return `cabane-companion connect ${runtime}`;
|
|
10422
10429
|
}
|
|
@@ -10426,12 +10433,22 @@ async function handOffToBackground(runtime, ctx) {
|
|
|
10426
10433
|
if (!outcome.started) {
|
|
10427
10434
|
return;
|
|
10428
10435
|
}
|
|
10429
|
-
if (ctx.
|
|
10436
|
+
if (ctx.justPaired) {
|
|
10437
|
+
for (const label of ctx.connected) write(INDENT + tick(`${label} connected.`));
|
|
10438
|
+
if (ctx.connected.length === 0) {
|
|
10439
|
+
write(
|
|
10440
|
+
`${INDENT}Add a coding agent any time: cabane-companion connect claude-code (or codex, or opencode)`
|
|
10441
|
+
);
|
|
10442
|
+
}
|
|
10443
|
+
}
|
|
10430
10444
|
write(INDENT + tick("Cabane companion is running in the background."));
|
|
10431
10445
|
write(` Stop: cabane-companion stop Logs: ${tildePath(companionLogPath())}`);
|
|
10432
10446
|
if (ctx.justPaired) {
|
|
10433
10447
|
blank();
|
|
10434
|
-
write(INDENT
|
|
10448
|
+
write(`${INDENT}! It won't restart on its own \u2014 after a reboot, or if it ever stops,`);
|
|
10449
|
+
write(`${INDENT} just run cabane-companion start again.`);
|
|
10450
|
+
blank();
|
|
10451
|
+
write(`${INDENT}All set on this side. Finish up in Cabane.`);
|
|
10435
10452
|
}
|
|
10436
10453
|
}
|
|
10437
10454
|
async function runAttached(runtime) {
|
package/dist/runtime.js
CHANGED
|
@@ -1297,7 +1297,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
1297
1297
|
var HARNESS_LABELS = {
|
|
1298
1298
|
"claude-code": "Claude Code",
|
|
1299
1299
|
codex: "Codex",
|
|
1300
|
-
opencode: "
|
|
1300
|
+
opencode: "OpenCode"
|
|
1301
1301
|
};
|
|
1302
1302
|
var LABELS = HARNESS_LABELS;
|
|
1303
1303
|
function deriveHarnessSnapshot(signals) {
|
|
@@ -1419,9 +1419,6 @@ function deriveOpencode(signals, manifestHas) {
|
|
|
1419
1419
|
enable: "opencode"
|
|
1420
1420
|
};
|
|
1421
1421
|
}
|
|
1422
|
-
function detectedRuntimesFor(snapshot) {
|
|
1423
|
-
return snapshot.harnesses.filter((h) => h.state === "detected_not_exposed").map((h) => ({ runtime: h.runtime, version: h.version }));
|
|
1424
|
-
}
|
|
1425
1422
|
var PROBE_TIMEOUT_MS = 4e3;
|
|
1426
1423
|
async function probeHarnessSignals(cfg, deps = {}) {
|
|
1427
1424
|
const probeClaudePresence = deps.probeClaudePresence ?? claudeOnPath;
|
|
@@ -6956,16 +6953,22 @@ async function proveWorkspaceTools(req, runtime, opts = {}) {
|
|
|
6956
6953
|
"x-cabane-active-conversation": req.cabane.activeConversationId
|
|
6957
6954
|
};
|
|
6958
6955
|
try {
|
|
6959
|
-
const initialized = await initializeWithRetry(
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6956
|
+
const initialized = await initializeWithRetry(
|
|
6957
|
+
fetchImpl,
|
|
6958
|
+
req.cabane.mcpUrl,
|
|
6959
|
+
headers,
|
|
6960
|
+
{
|
|
6961
|
+
jsonrpc: "2.0",
|
|
6962
|
+
id: 1,
|
|
6963
|
+
method: "initialize",
|
|
6964
|
+
params: {
|
|
6965
|
+
protocolVersion: "2025-03-26",
|
|
6966
|
+
capabilities: {},
|
|
6967
|
+
clientInfo: { name: "cabane-companion-readiness", version: "1" }
|
|
6968
|
+
}
|
|
6969
|
+
},
|
|
6970
|
+
opts.retryDelaysMs ?? INITIALIZE_RETRY_DELAYS_MS
|
|
6971
|
+
);
|
|
6969
6972
|
if (initialized.status === 401 || initialized.status === 403)
|
|
6970
6973
|
return fail(base, "authentication_failed", `initialize returned HTTP ${initialized.status}`);
|
|
6971
6974
|
if (initialized.transient)
|
|
@@ -7009,10 +7012,10 @@ async function proveWorkspaceTools(req, runtime, opts = {}) {
|
|
|
7009
7012
|
);
|
|
7010
7013
|
}
|
|
7011
7014
|
}
|
|
7012
|
-
async function initializeWithRetry(fetchImpl, url, headers, body) {
|
|
7015
|
+
async function initializeWithRetry(fetchImpl, url, headers, body, retryDelaysMs) {
|
|
7013
7016
|
let lastFailure = null;
|
|
7014
7017
|
const deadline = Date.now() + INITIALIZE_RETRY_BUDGET_MS;
|
|
7015
|
-
for (let attempt = 0; attempt <=
|
|
7018
|
+
for (let attempt = 0; attempt <= retryDelaysMs.length; attempt += 1) {
|
|
7016
7019
|
try {
|
|
7017
7020
|
const remainingMs = deadline - Date.now();
|
|
7018
7021
|
if (remainingMs <= 0) break;
|
|
@@ -7036,7 +7039,7 @@ async function initializeWithRetry(fetchImpl, url, headers, body) {
|
|
|
7036
7039
|
transient: true
|
|
7037
7040
|
};
|
|
7038
7041
|
}
|
|
7039
|
-
const retryDelayMs =
|
|
7042
|
+
const retryDelayMs = retryDelaysMs[attempt];
|
|
7040
7043
|
if (retryDelayMs === void 0 || Date.now() + retryDelayMs >= deadline) break;
|
|
7041
7044
|
await delay(retryDelayMs);
|
|
7042
7045
|
}
|
|
@@ -7672,6 +7675,7 @@ ${reason}`,
|
|
|
7672
7675
|
}
|
|
7673
7676
|
const proof = await proveWorkspaceTools(request, adapter.name, {
|
|
7674
7677
|
...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
|
|
7678
|
+
...this.opts.workspaceProofRetryDelaysMs ? { retryDelaysMs: this.opts.workspaceProofRetryDelaysMs } : {},
|
|
7675
7679
|
harnessFingerprint: turnContext.runtime
|
|
7676
7680
|
});
|
|
7677
7681
|
turnLog[proof.ok ? "info" : "error"](
|
|
@@ -8489,6 +8493,7 @@ var ASSIGNMENTS_POLL_MS = 6e4;
|
|
|
8489
8493
|
var DRAIN_BASE_MS = 1e3;
|
|
8490
8494
|
var DRAIN_MAX_MS = 3e4;
|
|
8491
8495
|
var DRAIN_IDLE_MS = 15e3;
|
|
8496
|
+
var DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS = [0, 1e3, 3e3];
|
|
8492
8497
|
var CompanionSupervisor = class {
|
|
8493
8498
|
workspaces = /* @__PURE__ */ new Map();
|
|
8494
8499
|
config;
|
|
@@ -8515,6 +8520,7 @@ var CompanionSupervisor = class {
|
|
|
8515
8520
|
exitFn;
|
|
8516
8521
|
reexecFn;
|
|
8517
8522
|
dispatcherFactory;
|
|
8523
|
+
unrunnableRetryDelaysMs;
|
|
8518
8524
|
deviceApi = null;
|
|
8519
8525
|
deviceSub = null;
|
|
8520
8526
|
heartbeatTimer = null;
|
|
@@ -8547,6 +8553,7 @@ var CompanionSupervisor = class {
|
|
|
8547
8553
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
8548
8554
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
8549
8555
|
this.dispatcherFactory = opts.dispatcherFactory;
|
|
8556
|
+
this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
|
|
8550
8557
|
}
|
|
8551
8558
|
// Stand up the data plane: pair check, initial assignments pull, then the
|
|
8552
8559
|
// heartbeat + poll loops. A companion with no device token (logged out) does
|
|
@@ -8647,13 +8654,7 @@ var CompanionSupervisor = class {
|
|
|
8647
8654
|
// CT584: include enumerated models only when the probe SUCCEEDED (non-null).
|
|
8648
8655
|
// A null (probe failed / no opencode) omits the field, and the server then
|
|
8649
8656
|
// leaves this device's stored availability untouched.
|
|
8650
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
8651
|
-
// CT1082: what this machine has that the user hasn't connected, so the web
|
|
8652
|
-
// UI can offer it without a companion round-trip. Sent only once a probe has
|
|
8653
|
-
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
8654
|
-
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
8655
|
-
// the same fail-soft contract `models` keeps.
|
|
8656
|
-
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
8657
|
+
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
8657
8658
|
});
|
|
8658
8659
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
8659
8660
|
this.deviceId = res.deviceId;
|
|
@@ -9057,9 +9058,8 @@ var CompanionSupervisor = class {
|
|
|
9057
9058
|
// wedge the workspace's whole stream.
|
|
9058
9059
|
async reportUnrunnableDispatch(wr, payload) {
|
|
9059
9060
|
if (!this.deviceApi) return false;
|
|
9060
|
-
const backoffMs = [0, 1e3, 3e3];
|
|
9061
9061
|
let lastErr;
|
|
9062
|
-
for (const wait of
|
|
9062
|
+
for (const wait of this.unrunnableRetryDelaysMs) {
|
|
9063
9063
|
if (this.stopped) return false;
|
|
9064
9064
|
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
9065
9065
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.42",
|
|
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",
|