@cabane/companion 0.6.38 → 0.6.39
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 +78 -2
- package/dist/runtime.js +78 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2679,6 +2679,19 @@ var DeviceApi = class {
|
|
|
2679
2679
|
beginDrain() {
|
|
2680
2680
|
return this.request("POST", "/api/companion/drain", {});
|
|
2681
2681
|
}
|
|
2682
|
+
// CT1146: report that a dispatch addressed to THIS device can't be run, because
|
|
2683
|
+
// the agent isn't one this device runs (and still wasn't after a forced
|
|
2684
|
+
// assignments refresh). The server clears the Working flag, retires the dispatch
|
|
2685
|
+
// and posts an explanatory system message.
|
|
2686
|
+
//
|
|
2687
|
+
// This rides the DEVICE token deliberately: the agent's own credential is
|
|
2688
|
+
// delivered through the device's assignments, so an agent this device doesn't
|
|
2689
|
+
// roster is exactly an agent it holds no credential for. The device token is the
|
|
2690
|
+
// only thing it can present, which is why this lives on `DeviceApi` rather than
|
|
2691
|
+
// the per-agent `CabaneApi`.
|
|
2692
|
+
reportDispatchDrop(body) {
|
|
2693
|
+
return this.request("POST", "/api/companion/dispatch-drop", body);
|
|
2694
|
+
}
|
|
2682
2695
|
// Per-device liveness ping. Reports the companion build version and the env-var
|
|
2683
2696
|
// names the operator's secret store exposes (never values), so CT30's UI can
|
|
2684
2697
|
// warn pre-emptively about an agent that needs a secret this device lacks.
|
|
@@ -3458,18 +3471,24 @@ function summarizeToolResult(content) {
|
|
|
3458
3471
|
}
|
|
3459
3472
|
if (Array.isArray(content)) {
|
|
3460
3473
|
const parts = [];
|
|
3474
|
+
let sawContentEntry = false;
|
|
3461
3475
|
for (const raw of content) {
|
|
3462
3476
|
if (typeof raw === "string") {
|
|
3477
|
+
sawContentEntry = true;
|
|
3463
3478
|
parts.push(raw);
|
|
3464
3479
|
} else if (raw && typeof raw === "object") {
|
|
3465
3480
|
const block = raw;
|
|
3481
|
+
if (typeof block.type === "string") sawContentEntry = true;
|
|
3466
3482
|
if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
3467
3483
|
}
|
|
3468
3484
|
}
|
|
3469
3485
|
const joined = parts.join("\n").trim();
|
|
3470
|
-
|
|
3486
|
+
if (joined.length > 0) return truncate(joined, TOOL_IO_MAX_CHARS);
|
|
3487
|
+
if (sawContentEntry || content.length === 0) return "";
|
|
3471
3488
|
}
|
|
3472
|
-
return "";
|
|
3489
|
+
if (typeof content !== "object") return "";
|
|
3490
|
+
if (!Array.isArray(content) && Object.keys(content).length === 0) return "";
|
|
3491
|
+
return summarizeToolIo(content);
|
|
3473
3492
|
}
|
|
3474
3493
|
function truncate(s, max) {
|
|
3475
3494
|
if (s.length <= max) return s;
|
|
@@ -9498,6 +9517,10 @@ var CompanionSupervisor = class {
|
|
|
9498
9517
|
if (!agent) {
|
|
9499
9518
|
agent = await this.recoverRacedAgent(wr, payload);
|
|
9500
9519
|
if (!agent) {
|
|
9520
|
+
if (this.deviceId && payload.deviceId === this.deviceId) {
|
|
9521
|
+
const reported = await this.reportUnrunnableDispatch(wr, payload);
|
|
9522
|
+
if (!reported) return;
|
|
9523
|
+
}
|
|
9501
9524
|
if (ev.id) wr.cursor.settle(ev.id);
|
|
9502
9525
|
return;
|
|
9503
9526
|
}
|
|
@@ -9519,6 +9542,59 @@ var CompanionSupervisor = class {
|
|
|
9519
9542
|
await tail;
|
|
9520
9543
|
if (wr.chains.get(chainKey) === tail) wr.chains.delete(chainKey);
|
|
9521
9544
|
}
|
|
9545
|
+
// CT1146: tell the server this device cannot run a dispatch it was addressed to.
|
|
9546
|
+
//
|
|
9547
|
+
// Returns whether the report LANDED, because that answer decides whether the
|
|
9548
|
+
// caller may settle the cursor. Nothing else on either side ends this turn: the
|
|
9549
|
+
// server's sweep resolves the agent's effective connector, which in a roster
|
|
9550
|
+
// desync still points at this device, so it is never classified `no_device` and
|
|
9551
|
+
// only the silent 12h age backstop touches it. An unreported drop is therefore
|
|
9552
|
+
// indistinguishable from the original defect.
|
|
9553
|
+
//
|
|
9554
|
+
// Bounded retry, because the common failure is transient (a 503 mid-deploy, a
|
|
9555
|
+
// dropped socket) and one more attempt a second later usually lands. Bounded
|
|
9556
|
+
// hard — this runs inline on the event loop, and an unbounded wait here would
|
|
9557
|
+
// wedge the workspace's whole stream.
|
|
9558
|
+
async reportUnrunnableDispatch(wr, payload) {
|
|
9559
|
+
if (!this.deviceApi) return false;
|
|
9560
|
+
const backoffMs = [0, 1e3, 3e3];
|
|
9561
|
+
let lastErr;
|
|
9562
|
+
for (const wait of backoffMs) {
|
|
9563
|
+
if (this.stopped) return false;
|
|
9564
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
9565
|
+
try {
|
|
9566
|
+
const { cleared } = await this.deviceApi.reportDispatchDrop({
|
|
9567
|
+
workspaceId: wr.workspaceId,
|
|
9568
|
+
conversationId: payload.conversationId,
|
|
9569
|
+
agentId: payload.agentId,
|
|
9570
|
+
messageId: payload.messageId,
|
|
9571
|
+
reason: "agent_not_on_device"
|
|
9572
|
+
});
|
|
9573
|
+
this.log.warn(
|
|
9574
|
+
{
|
|
9575
|
+
workspaceId: wr.workspaceId,
|
|
9576
|
+
conversationId: payload.conversationId,
|
|
9577
|
+
agentId: payload.agentId,
|
|
9578
|
+
cleared
|
|
9579
|
+
},
|
|
9580
|
+
"companion: dispatch addressed to this device is for an agent it does not run \u2014 reported as unrunnable"
|
|
9581
|
+
);
|
|
9582
|
+
return true;
|
|
9583
|
+
} catch (err) {
|
|
9584
|
+
lastErr = err;
|
|
9585
|
+
}
|
|
9586
|
+
}
|
|
9587
|
+
this.log.error(
|
|
9588
|
+
{
|
|
9589
|
+
workspaceId: wr.workspaceId,
|
|
9590
|
+
conversationId: payload.conversationId,
|
|
9591
|
+
agentId: payload.agentId,
|
|
9592
|
+
err: lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
9593
|
+
},
|
|
9594
|
+
"companion: could not report an unrunnable dispatch \u2014 leaving it unsettled so a reconnect replays it"
|
|
9595
|
+
);
|
|
9596
|
+
return false;
|
|
9597
|
+
}
|
|
9522
9598
|
// Pull-on-miss for the assignment-change race: a dispatch arrived for an agent
|
|
9523
9599
|
// we don't yet run. If it's addressed to THIS device, the assignment just
|
|
9524
9600
|
// changed under us (the poll hasn't caught it) — force a fresh assignments pull
|
package/dist/runtime.js
CHANGED
|
@@ -2107,6 +2107,19 @@ var DeviceApi = class {
|
|
|
2107
2107
|
beginDrain() {
|
|
2108
2108
|
return this.request("POST", "/api/companion/drain", {});
|
|
2109
2109
|
}
|
|
2110
|
+
// CT1146: report that a dispatch addressed to THIS device can't be run, because
|
|
2111
|
+
// the agent isn't one this device runs (and still wasn't after a forced
|
|
2112
|
+
// assignments refresh). The server clears the Working flag, retires the dispatch
|
|
2113
|
+
// and posts an explanatory system message.
|
|
2114
|
+
//
|
|
2115
|
+
// This rides the DEVICE token deliberately: the agent's own credential is
|
|
2116
|
+
// delivered through the device's assignments, so an agent this device doesn't
|
|
2117
|
+
// roster is exactly an agent it holds no credential for. The device token is the
|
|
2118
|
+
// only thing it can present, which is why this lives on `DeviceApi` rather than
|
|
2119
|
+
// the per-agent `CabaneApi`.
|
|
2120
|
+
reportDispatchDrop(body) {
|
|
2121
|
+
return this.request("POST", "/api/companion/dispatch-drop", body);
|
|
2122
|
+
}
|
|
2110
2123
|
// Per-device liveness ping. Reports the companion build version and the env-var
|
|
2111
2124
|
// names the operator's secret store exposes (never values), so CT30's UI can
|
|
2112
2125
|
// warn pre-emptively about an agent that needs a secret this device lacks.
|
|
@@ -2965,18 +2978,24 @@ function summarizeToolResult(content) {
|
|
|
2965
2978
|
}
|
|
2966
2979
|
if (Array.isArray(content)) {
|
|
2967
2980
|
const parts = [];
|
|
2981
|
+
let sawContentEntry = false;
|
|
2968
2982
|
for (const raw of content) {
|
|
2969
2983
|
if (typeof raw === "string") {
|
|
2984
|
+
sawContentEntry = true;
|
|
2970
2985
|
parts.push(raw);
|
|
2971
2986
|
} else if (raw && typeof raw === "object") {
|
|
2972
2987
|
const block = raw;
|
|
2988
|
+
if (typeof block.type === "string") sawContentEntry = true;
|
|
2973
2989
|
if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
2974
2990
|
}
|
|
2975
2991
|
}
|
|
2976
2992
|
const joined = parts.join("\n").trim();
|
|
2977
|
-
|
|
2993
|
+
if (joined.length > 0) return truncate(joined, TOOL_IO_MAX_CHARS);
|
|
2994
|
+
if (sawContentEntry || content.length === 0) return "";
|
|
2978
2995
|
}
|
|
2979
|
-
return "";
|
|
2996
|
+
if (typeof content !== "object") return "";
|
|
2997
|
+
if (!Array.isArray(content) && Object.keys(content).length === 0) return "";
|
|
2998
|
+
return summarizeToolIo(content);
|
|
2980
2999
|
}
|
|
2981
3000
|
function truncate(s, max) {
|
|
2982
3001
|
if (s.length <= max) return s;
|
|
@@ -9005,6 +9024,10 @@ var CompanionSupervisor = class {
|
|
|
9005
9024
|
if (!agent) {
|
|
9006
9025
|
agent = await this.recoverRacedAgent(wr, payload);
|
|
9007
9026
|
if (!agent) {
|
|
9027
|
+
if (this.deviceId && payload.deviceId === this.deviceId) {
|
|
9028
|
+
const reported = await this.reportUnrunnableDispatch(wr, payload);
|
|
9029
|
+
if (!reported) return;
|
|
9030
|
+
}
|
|
9008
9031
|
if (ev.id) wr.cursor.settle(ev.id);
|
|
9009
9032
|
return;
|
|
9010
9033
|
}
|
|
@@ -9026,6 +9049,59 @@ var CompanionSupervisor = class {
|
|
|
9026
9049
|
await tail;
|
|
9027
9050
|
if (wr.chains.get(chainKey) === tail) wr.chains.delete(chainKey);
|
|
9028
9051
|
}
|
|
9052
|
+
// CT1146: tell the server this device cannot run a dispatch it was addressed to.
|
|
9053
|
+
//
|
|
9054
|
+
// Returns whether the report LANDED, because that answer decides whether the
|
|
9055
|
+
// caller may settle the cursor. Nothing else on either side ends this turn: the
|
|
9056
|
+
// server's sweep resolves the agent's effective connector, which in a roster
|
|
9057
|
+
// desync still points at this device, so it is never classified `no_device` and
|
|
9058
|
+
// only the silent 12h age backstop touches it. An unreported drop is therefore
|
|
9059
|
+
// indistinguishable from the original defect.
|
|
9060
|
+
//
|
|
9061
|
+
// Bounded retry, because the common failure is transient (a 503 mid-deploy, a
|
|
9062
|
+
// dropped socket) and one more attempt a second later usually lands. Bounded
|
|
9063
|
+
// hard — this runs inline on the event loop, and an unbounded wait here would
|
|
9064
|
+
// wedge the workspace's whole stream.
|
|
9065
|
+
async reportUnrunnableDispatch(wr, payload) {
|
|
9066
|
+
if (!this.deviceApi) return false;
|
|
9067
|
+
const backoffMs = [0, 1e3, 3e3];
|
|
9068
|
+
let lastErr;
|
|
9069
|
+
for (const wait of backoffMs) {
|
|
9070
|
+
if (this.stopped) return false;
|
|
9071
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
9072
|
+
try {
|
|
9073
|
+
const { cleared } = await this.deviceApi.reportDispatchDrop({
|
|
9074
|
+
workspaceId: wr.workspaceId,
|
|
9075
|
+
conversationId: payload.conversationId,
|
|
9076
|
+
agentId: payload.agentId,
|
|
9077
|
+
messageId: payload.messageId,
|
|
9078
|
+
reason: "agent_not_on_device"
|
|
9079
|
+
});
|
|
9080
|
+
this.log.warn(
|
|
9081
|
+
{
|
|
9082
|
+
workspaceId: wr.workspaceId,
|
|
9083
|
+
conversationId: payload.conversationId,
|
|
9084
|
+
agentId: payload.agentId,
|
|
9085
|
+
cleared
|
|
9086
|
+
},
|
|
9087
|
+
"companion: dispatch addressed to this device is for an agent it does not run \u2014 reported as unrunnable"
|
|
9088
|
+
);
|
|
9089
|
+
return true;
|
|
9090
|
+
} catch (err) {
|
|
9091
|
+
lastErr = err;
|
|
9092
|
+
}
|
|
9093
|
+
}
|
|
9094
|
+
this.log.error(
|
|
9095
|
+
{
|
|
9096
|
+
workspaceId: wr.workspaceId,
|
|
9097
|
+
conversationId: payload.conversationId,
|
|
9098
|
+
agentId: payload.agentId,
|
|
9099
|
+
err: lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
9100
|
+
},
|
|
9101
|
+
"companion: could not report an unrunnable dispatch \u2014 leaving it unsettled so a reconnect replays it"
|
|
9102
|
+
);
|
|
9103
|
+
return false;
|
|
9104
|
+
}
|
|
9029
9105
|
// Pull-on-miss for the assignment-change race: a dispatch arrived for an agent
|
|
9030
9106
|
// we don't yet run. If it's addressed to THIS device, the assignment just
|
|
9031
9107
|
// changed under us (the poll hasn't caught it) — force a fresh assignments pull
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.39",
|
|
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",
|