@cabane/companion 0.6.24 → 0.6.26
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 +73 -19
- package/dist/runtime.js +73 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6835,6 +6835,22 @@ function checkoutState(cwd) {
|
|
|
6835
6835
|
function runKey(conversationId, agentId) {
|
|
6836
6836
|
return `${conversationId}|${agentId}`;
|
|
6837
6837
|
}
|
|
6838
|
+
var ERROR_BODY_LOG_CAP = 2e3;
|
|
6839
|
+
function describeErrorBody(body) {
|
|
6840
|
+
if (body === void 0 || body === null) return void 0;
|
|
6841
|
+
let text;
|
|
6842
|
+
if (typeof body === "string") {
|
|
6843
|
+
text = body;
|
|
6844
|
+
} else {
|
|
6845
|
+
try {
|
|
6846
|
+
text = JSON.stringify(body);
|
|
6847
|
+
} catch {
|
|
6848
|
+
text = String(body);
|
|
6849
|
+
}
|
|
6850
|
+
}
|
|
6851
|
+
if (text.length === 0) return void 0;
|
|
6852
|
+
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
6853
|
+
}
|
|
6838
6854
|
function describeSubAgentError(status2, body) {
|
|
6839
6855
|
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
6840
6856
|
switch (code) {
|
|
@@ -6860,6 +6876,13 @@ var Dispatcher = class {
|
|
|
6860
6876
|
opts;
|
|
6861
6877
|
// SJ383: per-(conversation, agent) abort registry.
|
|
6862
6878
|
aborts;
|
|
6879
|
+
// CT1109: pairs already told, in the conversation, that their session state is
|
|
6880
|
+
// being refused. The failure repeats every single turn until someone fixes the
|
|
6881
|
+
// payload, so without this the notice would be a per-turn drumbeat; one notice
|
|
6882
|
+
// is the signal and the rest is noise. Deliberately in-memory and unbounded-free:
|
|
6883
|
+
// a companion restart re-arms it, which is the behaviour we want — a restart is
|
|
6884
|
+
// exactly when it's worth re-stating that memory is still being lost.
|
|
6885
|
+
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
6863
6886
|
notifyStart(info) {
|
|
6864
6887
|
try {
|
|
6865
6888
|
this.opts.observer?.onStart(info);
|
|
@@ -7430,6 +7453,7 @@ ${reason}`,
|
|
|
7430
7453
|
) : null;
|
|
7431
7454
|
let sessionWritten = false;
|
|
7432
7455
|
let sessionDegraded = false;
|
|
7456
|
+
let sessionWriteRejected = false;
|
|
7433
7457
|
let okResult = false;
|
|
7434
7458
|
let resultReason;
|
|
7435
7459
|
const turnRuntime = turnContext.runtime;
|
|
@@ -7550,10 +7574,25 @@ ${reason}`,
|
|
|
7550
7574
|
{ agentSessionId: event.state }
|
|
7551
7575
|
);
|
|
7552
7576
|
} catch (err) {
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7577
|
+
const status2 = err instanceof ApiError ? err.status : void 0;
|
|
7578
|
+
if (status2 !== void 0 && status2 >= 400 && status2 < 500) {
|
|
7579
|
+
sessionWriteRejected = true;
|
|
7580
|
+
turnLog.error(
|
|
7581
|
+
{
|
|
7582
|
+
err: err instanceof Error ? err.message : String(err),
|
|
7583
|
+
status: status2,
|
|
7584
|
+
responseBody: describeErrorBody(err instanceof ApiError ? err.body : void 0),
|
|
7585
|
+
stateLength: event.state.length,
|
|
7586
|
+
runtime: turnRuntime
|
|
7587
|
+
},
|
|
7588
|
+
"dispatcher: session-state write REJECTED (4xx) \u2014 this turn is not resumable and the next turn starts with no memory of it"
|
|
7589
|
+
);
|
|
7590
|
+
} else {
|
|
7591
|
+
turnLog.warn(
|
|
7592
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7593
|
+
"dispatcher: session-id write failed (will retry next turn)"
|
|
7594
|
+
);
|
|
7595
|
+
}
|
|
7557
7596
|
}
|
|
7558
7597
|
}
|
|
7559
7598
|
} else if (event.type === "result") {
|
|
@@ -7680,6 +7719,10 @@ ${reason}`,
|
|
|
7680
7719
|
if (sessionDegraded) {
|
|
7681
7720
|
body.degraded = true;
|
|
7682
7721
|
}
|
|
7722
|
+
if (sessionWriteRejected && !this.sessionWriteNotified.has(key)) {
|
|
7723
|
+
body.sessionWriteRejected = true;
|
|
7724
|
+
this.sessionWriteNotified.add(key);
|
|
7725
|
+
}
|
|
7683
7726
|
this.opts.connectorHealth?.recordSettle(turnRuntime, {
|
|
7684
7727
|
ok: okResult,
|
|
7685
7728
|
errorReason: body.errorReason ?? null
|
|
@@ -8334,6 +8377,7 @@ var CompanionSupervisor = class {
|
|
|
8334
8377
|
refreshing = false;
|
|
8335
8378
|
stopped = false;
|
|
8336
8379
|
draining = false;
|
|
8380
|
+
restartPending = false;
|
|
8337
8381
|
// CT484: latch so the companion/server version-skew warning is logged once, not
|
|
8338
8382
|
// on every 30s heartbeat.
|
|
8339
8383
|
versionSkewWarned = false;
|
|
@@ -8444,7 +8488,8 @@ var CompanionSupervisor = class {
|
|
|
8444
8488
|
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
8445
8489
|
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
8446
8490
|
// the same fail-soft contract `models` keeps.
|
|
8447
|
-
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
8491
|
+
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
|
|
8492
|
+
...this.restartPending ? { restarting: true } : {}
|
|
8448
8493
|
});
|
|
8449
8494
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
8450
8495
|
this.deviceId = res.deviceId;
|
|
@@ -9082,32 +9127,41 @@ var CompanionSupervisor = class {
|
|
|
9082
9127
|
);
|
|
9083
9128
|
}
|
|
9084
9129
|
async drainForRestart(graceMs) {
|
|
9085
|
-
this.
|
|
9086
|
-
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
9087
|
-
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
9130
|
+
this.restartPending = true;
|
|
9088
9131
|
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
9089
9132
|
if (!this.deviceApi) throw new Error("cannot establish deploy drain before pairing");
|
|
9090
|
-
await this.
|
|
9091
|
-
|
|
9092
|
-
const
|
|
9133
|
+
await this.sendHeartbeat();
|
|
9134
|
+
const drainStartedAt = Date.now();
|
|
9135
|
+
const deadline = Date.now() + Math.max(0, graceMs);
|
|
9093
9136
|
let timedOut = false;
|
|
9094
|
-
|
|
9137
|
+
while (true) {
|
|
9138
|
+
const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
|
|
9139
|
+
if (turns.length === 0) break;
|
|
9140
|
+
const remainingMs = deadline - Date.now();
|
|
9141
|
+
if (remainingMs <= 0) {
|
|
9142
|
+
timedOut = true;
|
|
9143
|
+
break;
|
|
9144
|
+
}
|
|
9095
9145
|
let timer;
|
|
9096
9146
|
await Promise.race([
|
|
9097
9147
|
Promise.allSettled(turns),
|
|
9098
9148
|
new Promise((resolve) => {
|
|
9099
|
-
timer = setTimeout(
|
|
9100
|
-
() => {
|
|
9101
|
-
timedOut = true;
|
|
9102
|
-
resolve();
|
|
9103
|
-
},
|
|
9104
|
-
Math.max(0, graceMs)
|
|
9105
|
-
);
|
|
9149
|
+
timer = setTimeout(resolve, remainingMs);
|
|
9106
9150
|
timer.unref?.();
|
|
9107
9151
|
})
|
|
9108
9152
|
]);
|
|
9109
9153
|
if (timer) clearTimeout(timer);
|
|
9110
9154
|
}
|
|
9155
|
+
this.log.info(
|
|
9156
|
+
{ waitedMs: Date.now() - drainStartedAt, timedOut },
|
|
9157
|
+
timedOut ? "companion: deploy drain grace elapsed; fencing admission for restart" : "companion: deploy drain reached a quiet point; fencing admission for restart"
|
|
9158
|
+
);
|
|
9159
|
+
this.draining = true;
|
|
9160
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
9161
|
+
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
9162
|
+
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
9163
|
+
await this.deviceApi.beginDrain();
|
|
9164
|
+
for (const wr of this.workspaces.values()) wr.sub?.stop();
|
|
9111
9165
|
await Promise.allSettled(
|
|
9112
9166
|
[...this.workspaces.values()].flatMap(
|
|
9113
9167
|
(wr) => [...wr.agents.values()].map((agent) => agent.api.drainOutbox())
|
package/dist/runtime.js
CHANGED
|
@@ -6454,6 +6454,22 @@ function checkoutState(cwd) {
|
|
|
6454
6454
|
function runKey(conversationId, agentId) {
|
|
6455
6455
|
return `${conversationId}|${agentId}`;
|
|
6456
6456
|
}
|
|
6457
|
+
var ERROR_BODY_LOG_CAP = 2e3;
|
|
6458
|
+
function describeErrorBody(body) {
|
|
6459
|
+
if (body === void 0 || body === null) return void 0;
|
|
6460
|
+
let text;
|
|
6461
|
+
if (typeof body === "string") {
|
|
6462
|
+
text = body;
|
|
6463
|
+
} else {
|
|
6464
|
+
try {
|
|
6465
|
+
text = JSON.stringify(body);
|
|
6466
|
+
} catch {
|
|
6467
|
+
text = String(body);
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
if (text.length === 0) return void 0;
|
|
6471
|
+
return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
|
|
6472
|
+
}
|
|
6457
6473
|
function describeSubAgentError(status, body) {
|
|
6458
6474
|
const code = body && typeof body === "object" && "error" in body ? String(body.error) : void 0;
|
|
6459
6475
|
switch (code) {
|
|
@@ -6479,6 +6495,13 @@ var Dispatcher = class {
|
|
|
6479
6495
|
opts;
|
|
6480
6496
|
// SJ383: per-(conversation, agent) abort registry.
|
|
6481
6497
|
aborts;
|
|
6498
|
+
// CT1109: pairs already told, in the conversation, that their session state is
|
|
6499
|
+
// being refused. The failure repeats every single turn until someone fixes the
|
|
6500
|
+
// payload, so without this the notice would be a per-turn drumbeat; one notice
|
|
6501
|
+
// is the signal and the rest is noise. Deliberately in-memory and unbounded-free:
|
|
6502
|
+
// a companion restart re-arms it, which is the behaviour we want — a restart is
|
|
6503
|
+
// exactly when it's worth re-stating that memory is still being lost.
|
|
6504
|
+
sessionWriteNotified = /* @__PURE__ */ new Set();
|
|
6482
6505
|
notifyStart(info) {
|
|
6483
6506
|
try {
|
|
6484
6507
|
this.opts.observer?.onStart(info);
|
|
@@ -7049,6 +7072,7 @@ ${reason}`,
|
|
|
7049
7072
|
) : null;
|
|
7050
7073
|
let sessionWritten = false;
|
|
7051
7074
|
let sessionDegraded = false;
|
|
7075
|
+
let sessionWriteRejected = false;
|
|
7052
7076
|
let okResult = false;
|
|
7053
7077
|
let resultReason;
|
|
7054
7078
|
const turnRuntime = turnContext.runtime;
|
|
@@ -7169,10 +7193,25 @@ ${reason}`,
|
|
|
7169
7193
|
{ agentSessionId: event.state }
|
|
7170
7194
|
);
|
|
7171
7195
|
} catch (err) {
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7196
|
+
const status = err instanceof ApiError ? err.status : void 0;
|
|
7197
|
+
if (status !== void 0 && status >= 400 && status < 500) {
|
|
7198
|
+
sessionWriteRejected = true;
|
|
7199
|
+
turnLog.error(
|
|
7200
|
+
{
|
|
7201
|
+
err: err instanceof Error ? err.message : String(err),
|
|
7202
|
+
status,
|
|
7203
|
+
responseBody: describeErrorBody(err instanceof ApiError ? err.body : void 0),
|
|
7204
|
+
stateLength: event.state.length,
|
|
7205
|
+
runtime: turnRuntime
|
|
7206
|
+
},
|
|
7207
|
+
"dispatcher: session-state write REJECTED (4xx) \u2014 this turn is not resumable and the next turn starts with no memory of it"
|
|
7208
|
+
);
|
|
7209
|
+
} else {
|
|
7210
|
+
turnLog.warn(
|
|
7211
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
7212
|
+
"dispatcher: session-id write failed (will retry next turn)"
|
|
7213
|
+
);
|
|
7214
|
+
}
|
|
7176
7215
|
}
|
|
7177
7216
|
}
|
|
7178
7217
|
} else if (event.type === "result") {
|
|
@@ -7299,6 +7338,10 @@ ${reason}`,
|
|
|
7299
7338
|
if (sessionDegraded) {
|
|
7300
7339
|
body.degraded = true;
|
|
7301
7340
|
}
|
|
7341
|
+
if (sessionWriteRejected && !this.sessionWriteNotified.has(key)) {
|
|
7342
|
+
body.sessionWriteRejected = true;
|
|
7343
|
+
this.sessionWriteNotified.add(key);
|
|
7344
|
+
}
|
|
7302
7345
|
this.opts.connectorHealth?.recordSettle(turnRuntime, {
|
|
7303
7346
|
ok: okResult,
|
|
7304
7347
|
errorReason: body.errorReason ?? null
|
|
@@ -7953,6 +7996,7 @@ var CompanionSupervisor = class {
|
|
|
7953
7996
|
refreshing = false;
|
|
7954
7997
|
stopped = false;
|
|
7955
7998
|
draining = false;
|
|
7999
|
+
restartPending = false;
|
|
7956
8000
|
// CT484: latch so the companion/server version-skew warning is logged once, not
|
|
7957
8001
|
// on every 30s heartbeat.
|
|
7958
8002
|
versionSkewWarned = false;
|
|
@@ -8063,7 +8107,8 @@ var CompanionSupervisor = class {
|
|
|
8063
8107
|
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
8064
8108
|
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
8065
8109
|
// the same fail-soft contract `models` keeps.
|
|
8066
|
-
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
8110
|
+
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
|
|
8111
|
+
...this.restartPending ? { restarting: true } : {}
|
|
8067
8112
|
});
|
|
8068
8113
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
8069
8114
|
this.deviceId = res.deviceId;
|
|
@@ -8701,32 +8746,41 @@ var CompanionSupervisor = class {
|
|
|
8701
8746
|
);
|
|
8702
8747
|
}
|
|
8703
8748
|
async drainForRestart(graceMs) {
|
|
8704
|
-
this.
|
|
8705
|
-
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
8706
|
-
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
8749
|
+
this.restartPending = true;
|
|
8707
8750
|
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
8708
8751
|
if (!this.deviceApi) throw new Error("cannot establish deploy drain before pairing");
|
|
8709
|
-
await this.
|
|
8710
|
-
|
|
8711
|
-
const
|
|
8752
|
+
await this.sendHeartbeat();
|
|
8753
|
+
const drainStartedAt = Date.now();
|
|
8754
|
+
const deadline = Date.now() + Math.max(0, graceMs);
|
|
8712
8755
|
let timedOut = false;
|
|
8713
|
-
|
|
8756
|
+
while (true) {
|
|
8757
|
+
const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
|
|
8758
|
+
if (turns.length === 0) break;
|
|
8759
|
+
const remainingMs = deadline - Date.now();
|
|
8760
|
+
if (remainingMs <= 0) {
|
|
8761
|
+
timedOut = true;
|
|
8762
|
+
break;
|
|
8763
|
+
}
|
|
8714
8764
|
let timer;
|
|
8715
8765
|
await Promise.race([
|
|
8716
8766
|
Promise.allSettled(turns),
|
|
8717
8767
|
new Promise((resolve) => {
|
|
8718
|
-
timer = setTimeout(
|
|
8719
|
-
() => {
|
|
8720
|
-
timedOut = true;
|
|
8721
|
-
resolve();
|
|
8722
|
-
},
|
|
8723
|
-
Math.max(0, graceMs)
|
|
8724
|
-
);
|
|
8768
|
+
timer = setTimeout(resolve, remainingMs);
|
|
8725
8769
|
timer.unref?.();
|
|
8726
8770
|
})
|
|
8727
8771
|
]);
|
|
8728
8772
|
if (timer) clearTimeout(timer);
|
|
8729
8773
|
}
|
|
8774
|
+
this.log.info(
|
|
8775
|
+
{ waitedMs: Date.now() - drainStartedAt, timedOut },
|
|
8776
|
+
timedOut ? "companion: deploy drain grace elapsed; fencing admission for restart" : "companion: deploy drain reached a quiet point; fencing admission for restart"
|
|
8777
|
+
);
|
|
8778
|
+
this.draining = true;
|
|
8779
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
8780
|
+
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
8781
|
+
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
8782
|
+
await this.deviceApi.beginDrain();
|
|
8783
|
+
for (const wr of this.workspaces.values()) wr.sub?.stop();
|
|
8730
8784
|
await Promise.allSettled(
|
|
8731
8785
|
[...this.workspaces.values()].flatMap(
|
|
8732
8786
|
(wr) => [...wr.agents.values()].map((agent) => agent.api.drainOutbox())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.26",
|
|
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",
|