@cabane/companion 0.6.120 → 0.6.121
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 +163 -342
- package/dist/runtime.js +146 -325
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1904,7 +1904,7 @@ async function pair(opts = {}) {
|
|
|
1904
1904
|
}
|
|
1905
1905
|
|
|
1906
1906
|
// src/cli.ts
|
|
1907
|
-
import { readFileSync as
|
|
1907
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
1908
1908
|
|
|
1909
1909
|
// src/commands/logs.ts
|
|
1910
1910
|
import { once } from "events";
|
|
@@ -3112,27 +3112,6 @@ var CabaneApi = class {
|
|
|
3112
3112
|
outboxSize() {
|
|
3113
3113
|
return this.opts.outbox?.size() ?? 0;
|
|
3114
3114
|
}
|
|
3115
|
-
getConversation(workspaceId, conversationId) {
|
|
3116
|
-
return this.request("GET", `/api/workspaces/${workspaceId}/conversations/${conversationId}`);
|
|
3117
|
-
}
|
|
3118
|
-
// SJ383: companion-only participant ops. All three authenticate with the
|
|
3119
|
-
// workspace's agent-bound PAT (passed as `token` on this client) — never
|
|
3120
|
-
// the user PAT.
|
|
3121
|
-
// Recovery-path read: returns the companion's `agentSessionId` for this
|
|
3122
|
-
// conversation so the dispatcher can pass it as `Options.resume`, plus
|
|
3123
|
-
// the current `activeRunStartedAt` so a freshly-reconnected companion can
|
|
3124
|
-
// see whether a prior run is still flagged in-flight.
|
|
3125
|
-
// SJ383: recovery-path read for a participant row — the `agentSessionId` a
|
|
3126
|
-
// freshly-reconnected companion resumes on. CT262: the per-turn piggybacks this
|
|
3127
|
-
// fetch grew (the ABOUT charters / visionBlocks / channel / members / conversationContext)
|
|
3128
|
-
// are gone — `getTurnContext` composes the whole turn now — so this is back to
|
|
3129
|
-
// the plain recovery read, with no `messageId` param.
|
|
3130
|
-
getParticipantAgent(workspaceId, conversationId, agentId) {
|
|
3131
|
-
return this.request(
|
|
3132
|
-
"GET",
|
|
3133
|
-
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}`
|
|
3134
|
-
);
|
|
3135
|
-
}
|
|
3136
3115
|
// CT262: the ONE turn-context fetch. Collapses the companion's old four-fetch
|
|
3137
3116
|
// choreography (getConversation + getMessage + getAgentSelf + getParticipantAgent)
|
|
3138
3117
|
// into a single call: the server composes the whole server portion of the
|
|
@@ -3412,34 +3391,6 @@ var CabaneApi = class {
|
|
|
3412
3391
|
signal
|
|
3413
3392
|
);
|
|
3414
3393
|
}
|
|
3415
|
-
// SJ493: fetch the agent's self-view — identity + operating context. The
|
|
3416
|
-
// companion calls this on each dispatch to get its `systemPrompt` (composed
|
|
3417
|
-
// server-side from the bundled default + the agent's charter) rather than
|
|
3418
|
-
// baking a copy of the prompt into the download. Cabane is the control plane
|
|
3419
|
-
// for the prompt, so changing it (or the per-agent charter) takes effect
|
|
3420
|
-
// without shipping a new companion. Agent-PAT authed; the workspace is implied
|
|
3421
|
-
// by the PAT, so no workspace arg.
|
|
3422
|
-
// CT245: pass the triggering turn's `conversationId` so the server returns the
|
|
3423
|
-
// run-config RESOLVED for this conversation (agent default + that
|
|
3424
|
-
// conversation-agent's per-conversation override), computed with the same
|
|
3425
|
-
// shared resolver the in-app path uses. Omitting it (older call sites) returns
|
|
3426
|
-
// the agent default — graceful degradation, no version coupling.
|
|
3427
|
-
getAgentSelf(conversationId) {
|
|
3428
|
-
const path = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
|
|
3429
|
-
return this.request("GET", path);
|
|
3430
|
-
}
|
|
3431
|
-
// The companion fetches the triggering message body by listing the
|
|
3432
|
-
// conversation's messages and finding the one with `id === messageId`.
|
|
3433
|
-
// Cabane has no single-message GET endpoint; for v0 this is fine because
|
|
3434
|
-
// the companion only reaches for the specific row immediately after the
|
|
3435
|
-
// event fires (the thread is small at that point).
|
|
3436
|
-
async getMessage(workspaceId, conversationId, messageId2) {
|
|
3437
|
-
const res = await this.request(
|
|
3438
|
-
"GET",
|
|
3439
|
-
`/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`
|
|
3440
|
-
);
|
|
3441
|
-
return res.messages.find((m) => m.id === messageId2) ?? null;
|
|
3442
|
-
}
|
|
3443
3394
|
};
|
|
3444
3395
|
function isRetryable(err) {
|
|
3445
3396
|
if (err instanceof ApiError) return err.status >= 500;
|
|
@@ -3554,78 +3505,21 @@ function errorMessage2(status2, body) {
|
|
|
3554
3505
|
return `${status2} error`;
|
|
3555
3506
|
}
|
|
3556
3507
|
|
|
3557
|
-
// src/cursor.ts
|
|
3558
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
3559
|
-
import { join as join9 } from "path";
|
|
3560
|
-
function pathFor(workspaceId) {
|
|
3561
|
-
return join9(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
3562
|
-
}
|
|
3563
|
-
function readCursor(workspaceId) {
|
|
3564
|
-
const path = pathFor(workspaceId);
|
|
3565
|
-
if (!existsSync6(path)) return null;
|
|
3566
|
-
const raw = readFileSync4(path, "utf8").trim();
|
|
3567
|
-
return raw.length > 0 ? raw : null;
|
|
3568
|
-
}
|
|
3569
|
-
function writeCursor(workspaceId, eventId) {
|
|
3570
|
-
const path = pathFor(workspaceId);
|
|
3571
|
-
mkdirSync6(join9(cabaneDir(), "cursors"), { recursive: true });
|
|
3572
|
-
writeFileSync4(path, eventId + "\n", "utf8");
|
|
3573
|
-
}
|
|
3574
|
-
|
|
3575
|
-
// src/cursor-tracker.ts
|
|
3576
|
-
var CursorTracker = class {
|
|
3577
|
-
constructor(workspaceId) {
|
|
3578
|
-
this.workspaceId = workspaceId;
|
|
3579
|
-
}
|
|
3580
|
-
workspaceId;
|
|
3581
|
-
// Event ids in arrival order that haven't yet flushed to the persisted
|
|
3582
|
-
// cursor. Holds the in-flight (un-settled) dispatch events plus any later
|
|
3583
|
-
// events that have settled but are blocked behind an earlier in-flight one.
|
|
3584
|
-
pending = [];
|
|
3585
|
-
// Ids observed as settled. An entry is dropped the moment it flushes, so the
|
|
3586
|
-
// set only ever holds settled-but-blocked ids — bounded by concurrent
|
|
3587
|
-
// in-flight turns and the traffic that arrived during them.
|
|
3588
|
-
settled = /* @__PURE__ */ new Set();
|
|
3589
|
-
// Record an event id in arrival order. Must be called synchronously, in
|
|
3590
|
-
// stream order, for every event the supervisor will later `settle` — so the
|
|
3591
|
-
// cumulative prefix reflects the true stream sequence. Idempotent: an id
|
|
3592
|
-
// already tracked (an in-flight event, or a settled-but-blocked one) is
|
|
3593
|
-
// ignored, so a process-restart replay can re-observe the same ids without
|
|
3594
|
-
// double-counting.
|
|
3595
|
-
observe(eventId) {
|
|
3596
|
-
if (this.pending.includes(eventId) || this.settled.has(eventId)) return;
|
|
3597
|
-
this.pending.push(eventId);
|
|
3598
|
-
}
|
|
3599
|
-
// Mark an event settled — for a dispatch turn, only once it has completed;
|
|
3600
|
-
// for everything else, immediately after it's handled. Flushes the persisted
|
|
3601
|
-
// cursor forward through the now-longer contiguous settled prefix.
|
|
3602
|
-
settle(eventId) {
|
|
3603
|
-
this.settled.add(eventId);
|
|
3604
|
-
let advanced = null;
|
|
3605
|
-
while (this.pending.length > 0 && this.settled.has(this.pending[0])) {
|
|
3606
|
-
const id = this.pending.shift();
|
|
3607
|
-
this.settled.delete(id);
|
|
3608
|
-
advanced = id;
|
|
3609
|
-
}
|
|
3610
|
-
if (advanced) writeCursor(this.workspaceId, advanced);
|
|
3611
|
-
}
|
|
3612
|
-
};
|
|
3613
|
-
|
|
3614
3508
|
// src/dispatch-dedupe.ts
|
|
3615
|
-
import { mkdirSync as
|
|
3616
|
-
import { join as
|
|
3509
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6 } from "fs";
|
|
3510
|
+
import { join as join9 } from "path";
|
|
3617
3511
|
var MAX_IDS = 256;
|
|
3618
3512
|
function dir(log) {
|
|
3619
|
-
return
|
|
3513
|
+
return join9(cabaneDir(), log);
|
|
3620
3514
|
}
|
|
3621
|
-
function
|
|
3622
|
-
return
|
|
3515
|
+
function pathFor(log, workspaceId) {
|
|
3516
|
+
return join9(dir(log), encodeURIComponent(workspaceId));
|
|
3623
3517
|
}
|
|
3624
3518
|
function readIds(log, workspaceId) {
|
|
3625
|
-
const path =
|
|
3626
|
-
if (!
|
|
3519
|
+
const path = pathFor(log, workspaceId);
|
|
3520
|
+
if (!existsSync6(path)) return [];
|
|
3627
3521
|
try {
|
|
3628
|
-
return
|
|
3522
|
+
return readFileSync4(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3629
3523
|
} catch {
|
|
3630
3524
|
return [];
|
|
3631
3525
|
}
|
|
@@ -3638,8 +3532,8 @@ function mark(log, workspaceId, eventId) {
|
|
|
3638
3532
|
if (ids.includes(eventId)) return;
|
|
3639
3533
|
ids.push(eventId);
|
|
3640
3534
|
const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
|
|
3641
|
-
|
|
3642
|
-
|
|
3535
|
+
mkdirSync6(dir(log), { recursive: true });
|
|
3536
|
+
writeFileSync4(pathFor(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
|
|
3643
3537
|
}
|
|
3644
3538
|
function hasDispatched(workspaceId, eventId) {
|
|
3645
3539
|
return has("dispatched", workspaceId, eventId);
|
|
@@ -3656,17 +3550,17 @@ function markCompleted(workspaceId, eventId) {
|
|
|
3656
3550
|
}
|
|
3657
3551
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
3658
3552
|
function resumeDir() {
|
|
3659
|
-
return
|
|
3553
|
+
return join9(cabaneDir(), "resume-attempts");
|
|
3660
3554
|
}
|
|
3661
3555
|
function resumePathFor(workspaceId) {
|
|
3662
|
-
return
|
|
3556
|
+
return join9(resumeDir(), encodeURIComponent(workspaceId));
|
|
3663
3557
|
}
|
|
3664
3558
|
function readResumeCounts(workspaceId) {
|
|
3665
3559
|
const out = /* @__PURE__ */ new Map();
|
|
3666
3560
|
const path = resumePathFor(workspaceId);
|
|
3667
|
-
if (!
|
|
3561
|
+
if (!existsSync6(path)) return out;
|
|
3668
3562
|
try {
|
|
3669
|
-
for (const line of
|
|
3563
|
+
for (const line of readFileSync4(path, "utf8").split("\n")) {
|
|
3670
3564
|
const trimmed = line.trim();
|
|
3671
3565
|
if (!trimmed) continue;
|
|
3672
3566
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -3686,8 +3580,8 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
3686
3580
|
counts.set(eventId, next);
|
|
3687
3581
|
const entries = [...counts.entries()];
|
|
3688
3582
|
const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
|
|
3689
|
-
|
|
3690
|
-
|
|
3583
|
+
mkdirSync6(resumeDir(), { recursive: true });
|
|
3584
|
+
writeFileSync4(
|
|
3691
3585
|
resumePathFor(workspaceId),
|
|
3692
3586
|
trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
|
|
3693
3587
|
"utf8"
|
|
@@ -3695,18 +3589,18 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
3695
3589
|
return next;
|
|
3696
3590
|
}
|
|
3697
3591
|
function turnDir() {
|
|
3698
|
-
return
|
|
3592
|
+
return join9(cabaneDir(), "turns");
|
|
3699
3593
|
}
|
|
3700
3594
|
function turnPathFor(workspaceId) {
|
|
3701
|
-
return
|
|
3595
|
+
return join9(turnDir(), encodeURIComponent(workspaceId));
|
|
3702
3596
|
}
|
|
3703
3597
|
var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3704
3598
|
function readTurnIds(workspaceId) {
|
|
3705
3599
|
const out = /* @__PURE__ */ new Map();
|
|
3706
3600
|
const path = turnPathFor(workspaceId);
|
|
3707
|
-
if (!
|
|
3601
|
+
if (!existsSync6(path)) return out;
|
|
3708
3602
|
try {
|
|
3709
|
-
for (const line of
|
|
3603
|
+
for (const line of readFileSync4(path, "utf8").split("\n")) {
|
|
3710
3604
|
const trimmed = line.trim();
|
|
3711
3605
|
if (!trimmed) continue;
|
|
3712
3606
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -3726,10 +3620,10 @@ function turnIdForEvent(workspaceId, eventId) {
|
|
|
3726
3620
|
var TURN_ID_OVERFLOW_WARN = 8192;
|
|
3727
3621
|
function writeTurnIds(workspaceId, turns) {
|
|
3728
3622
|
const entries = [...turns.entries()];
|
|
3729
|
-
|
|
3623
|
+
mkdirSync6(turnDir(), { recursive: true });
|
|
3730
3624
|
const path = turnPathFor(workspaceId);
|
|
3731
3625
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
3732
|
-
|
|
3626
|
+
writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
|
|
3733
3627
|
renameSync3(tmp, path);
|
|
3734
3628
|
return entries.length;
|
|
3735
3629
|
}
|
|
@@ -8504,12 +8398,12 @@ var ConnectorHealthStore = class {
|
|
|
8504
8398
|
};
|
|
8505
8399
|
|
|
8506
8400
|
// src/dispatcher.ts
|
|
8507
|
-
import { existsSync as
|
|
8508
|
-
import { join as
|
|
8401
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
8402
|
+
import { join as join14 } from "path";
|
|
8509
8403
|
|
|
8510
8404
|
// src/turn-execution.ts
|
|
8511
8405
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
8512
|
-
import { existsSync as
|
|
8406
|
+
import { existsSync as existsSync9 } from "fs";
|
|
8513
8407
|
|
|
8514
8408
|
// src/turn-control-tools.ts
|
|
8515
8409
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -8745,24 +8639,10 @@ function buildCompanionTurnRequest(params) {
|
|
|
8745
8639
|
session: t.session,
|
|
8746
8640
|
cabane: {
|
|
8747
8641
|
mcpUrl: cabaneMcpUrl(params.baseUrl),
|
|
8748
|
-
|
|
8749
|
-
// when the API didn't mint one (older API / unresolvable delegation).
|
|
8750
|
-
bearer: params.turnToken ?? params.agentPat,
|
|
8642
|
+
bearer: params.turnToken,
|
|
8751
8643
|
activeConversationId: params.activeConversationId,
|
|
8752
8644
|
workspaceId: params.workspaceId,
|
|
8753
|
-
|
|
8754
|
-
// this turn — the surface admits `turn_token` auth exclusively, so a
|
|
8755
|
-
// PAT-fallback bearer would be rejected there. Absent it, external adapters
|
|
8756
|
-
// simply don't mount it that turn.
|
|
8757
|
-
//
|
|
8758
|
-
// CT1074: that fallback is now an OLD-API case only. The server mints a token
|
|
8759
|
-
// for every turn it dispatched, so "no token" no longer means "no human behind
|
|
8760
|
-
// the turn" — it means the API predates this change. The condition is right as
|
|
8761
|
-
// it stands and stays where the failure is honest (a rejected bearer is worse
|
|
8762
|
-
// than an unmounted server); what was wrong was upstream, where an undelegated
|
|
8763
|
-
// turn minted nothing and this silently dropped all five verbs on Codex and
|
|
8764
|
-
// opencode with no refusal anyone could see.
|
|
8765
|
-
...params.turnToken ? { turnControlUrl: turnControlMcpUrl(params.baseUrl) } : {}
|
|
8645
|
+
turnControlUrl: turnControlMcpUrl(params.baseUrl)
|
|
8766
8646
|
},
|
|
8767
8647
|
local: {
|
|
8768
8648
|
...params.cwd ? { cwd: params.cwd } : {},
|
|
@@ -8786,11 +8666,11 @@ function trimSlash2(s) {
|
|
|
8786
8666
|
// src/codex-instructions.ts
|
|
8787
8667
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
8788
8668
|
import { tmpdir } from "os";
|
|
8789
|
-
import { join as
|
|
8669
|
+
import { join as join10 } from "path";
|
|
8790
8670
|
var PREFIX = "cabane-codex-instructions-";
|
|
8791
8671
|
async function writeCodexInstructionsFile(contents) {
|
|
8792
|
-
const dir2 = await mkdtemp(
|
|
8793
|
-
const path =
|
|
8672
|
+
const dir2 = await mkdtemp(join10(tmpdir(), PREFIX));
|
|
8673
|
+
const path = join10(dir2, "instructions.md");
|
|
8794
8674
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
8795
8675
|
return {
|
|
8796
8676
|
path,
|
|
@@ -8896,22 +8776,22 @@ function reportFailure(config, body) {
|
|
|
8896
8776
|
}
|
|
8897
8777
|
|
|
8898
8778
|
// src/prepared.ts
|
|
8899
|
-
import { mkdirSync as
|
|
8900
|
-
import { join as
|
|
8779
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
8780
|
+
import { join as join11 } from "path";
|
|
8901
8781
|
function dirFor(workspaceId) {
|
|
8902
|
-
return
|
|
8782
|
+
return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
8903
8783
|
}
|
|
8904
8784
|
function conversationDir(workspaceId, conversationId) {
|
|
8905
|
-
return
|
|
8785
|
+
return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
8906
8786
|
}
|
|
8907
|
-
function
|
|
8908
|
-
return
|
|
8787
|
+
function pathFor2(workspaceId, conversationId, agentId) {
|
|
8788
|
+
return join11(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
8909
8789
|
}
|
|
8910
8790
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
8911
|
-
const path =
|
|
8912
|
-
if (!
|
|
8791
|
+
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
8792
|
+
if (!existsSync7(path)) return null;
|
|
8913
8793
|
try {
|
|
8914
|
-
const parsed = JSON.parse(
|
|
8794
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
8915
8795
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
8916
8796
|
return {
|
|
8917
8797
|
cwd: parsed.cwd,
|
|
@@ -8924,32 +8804,32 @@ function readPrepared(workspaceId, conversationId, agentId) {
|
|
|
8924
8804
|
}
|
|
8925
8805
|
}
|
|
8926
8806
|
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8807
|
+
mkdirSync7(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
8808
|
+
writeFileSync5(
|
|
8809
|
+
pathFor2(workspaceId, conversationId, agentId),
|
|
8930
8810
|
JSON.stringify(result) + "\n",
|
|
8931
8811
|
"utf8"
|
|
8932
8812
|
);
|
|
8933
8813
|
}
|
|
8934
8814
|
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
8935
|
-
rmSync5(
|
|
8815
|
+
rmSync5(pathFor2(workspaceId, conversationId, agentId), { force: true });
|
|
8936
8816
|
}
|
|
8937
8817
|
|
|
8938
8818
|
// src/secrets.ts
|
|
8939
|
-
import { existsSync as
|
|
8940
|
-
import { join as
|
|
8819
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
8820
|
+
import { join as join12 } from "path";
|
|
8941
8821
|
import { z as z14 } from "zod";
|
|
8942
8822
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8943
8823
|
function secretsPath() {
|
|
8944
|
-
return
|
|
8824
|
+
return join12(cabaneDir(), "secrets.json");
|
|
8945
8825
|
}
|
|
8946
8826
|
var secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
8947
8827
|
function loadSecretStore() {
|
|
8948
8828
|
const path = secretsPath();
|
|
8949
|
-
if (!
|
|
8829
|
+
if (!existsSync8(path)) return makeStore({});
|
|
8950
8830
|
let raw;
|
|
8951
8831
|
try {
|
|
8952
|
-
raw =
|
|
8832
|
+
raw = readFileSync6(path, "utf8");
|
|
8953
8833
|
} catch (err) {
|
|
8954
8834
|
throw new ConfigError(
|
|
8955
8835
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -9027,10 +8907,10 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
9027
8907
|
}
|
|
9028
8908
|
|
|
9029
8909
|
// src/transcript-writer.ts
|
|
9030
|
-
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as
|
|
9031
|
-
import { basename, dirname as dirname6, join as
|
|
8910
|
+
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
8911
|
+
import { basename, dirname as dirname6, join as join13 } from "path";
|
|
9032
8912
|
function transcriptsDir() {
|
|
9033
|
-
return
|
|
8913
|
+
return join13(cabaneDir(), "transcripts");
|
|
9034
8914
|
}
|
|
9035
8915
|
var RETAIN = 200;
|
|
9036
8916
|
var ANOMALY_RETAIN = 50;
|
|
@@ -9040,9 +8920,9 @@ var TranscriptWriter = class {
|
|
|
9040
8920
|
onWarn;
|
|
9041
8921
|
constructor(dir2, meta, onWarn) {
|
|
9042
8922
|
this.onWarn = onWarn;
|
|
9043
|
-
this.path =
|
|
8923
|
+
this.path = join13(dir2, fileName(meta));
|
|
9044
8924
|
try {
|
|
9045
|
-
|
|
8925
|
+
mkdirSync8(dir2, { recursive: true });
|
|
9046
8926
|
try {
|
|
9047
8927
|
chmodSync4(dir2, 448);
|
|
9048
8928
|
} catch {
|
|
@@ -9069,10 +8949,10 @@ var TranscriptWriter = class {
|
|
|
9069
8949
|
preserveAnomaly() {
|
|
9070
8950
|
if (this.broken) return;
|
|
9071
8951
|
try {
|
|
9072
|
-
const dir2 =
|
|
9073
|
-
|
|
8952
|
+
const dir2 = join13(dirname6(this.path), "anomalies");
|
|
8953
|
+
mkdirSync8(dir2, { recursive: true, mode: 448 });
|
|
9074
8954
|
chmodSync4(dir2, 448);
|
|
9075
|
-
const target =
|
|
8955
|
+
const target = join13(dir2, basename(this.path));
|
|
9076
8956
|
copyFileSync(this.path, target);
|
|
9077
8957
|
chmodSync4(target, 384);
|
|
9078
8958
|
pruneOld(dir2, ANOMALY_RETAIN);
|
|
@@ -9118,7 +8998,7 @@ function pruneOld(dir2, retain) {
|
|
|
9118
8998
|
const drop = files.sort().slice(0, files.length - retain);
|
|
9119
8999
|
for (const f of drop) {
|
|
9120
9000
|
try {
|
|
9121
|
-
rmSync6(
|
|
9001
|
+
rmSync6(join13(dir2, f), { force: true });
|
|
9122
9002
|
} catch {
|
|
9123
9003
|
}
|
|
9124
9004
|
}
|
|
@@ -9126,7 +9006,7 @@ function pruneOld(dir2, retain) {
|
|
|
9126
9006
|
|
|
9127
9007
|
// src/turn-containment.ts
|
|
9128
9008
|
import { execFile, execFileSync, spawn as spawn5 } from "child_process";
|
|
9129
|
-
import { readFileSync as
|
|
9009
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
9130
9010
|
import { promisify } from "util";
|
|
9131
9011
|
var execFileAsync = promisify(execFile);
|
|
9132
9012
|
var asContained = (child) => child;
|
|
@@ -9171,7 +9051,7 @@ function currentSystemdUnit(cgroupText) {
|
|
|
9171
9051
|
}
|
|
9172
9052
|
function readCurrentSystemdUnit() {
|
|
9173
9053
|
try {
|
|
9174
|
-
return currentSystemdUnit(
|
|
9054
|
+
return currentSystemdUnit(readFileSync7("/proc/self/cgroup", "utf8"));
|
|
9175
9055
|
} catch {
|
|
9176
9056
|
return null;
|
|
9177
9057
|
}
|
|
@@ -9597,6 +9477,7 @@ function missingSecretReason(missing) {
|
|
|
9597
9477
|
return `${prefix}+${missing.length}`;
|
|
9598
9478
|
}
|
|
9599
9479
|
var RUNTIME_UNAVAILABLE_PREFIX = "**This agent's runtime isn't available on this companion.** The model this agent uses needs a runtime this device isn't running, so I can't run this turn here. Details:";
|
|
9480
|
+
var NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
|
|
9600
9481
|
var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't dispatchable on this companion) \u2014 the companion is likely running outdated code; refresh it, then re-address the agent`;
|
|
9601
9482
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
9602
9483
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 15 * 6e4;
|
|
@@ -9876,6 +9757,10 @@ var TurnExecution = class {
|
|
|
9876
9757
|
const fetchReason = `fetch_failed: ${describeError(err)}`;
|
|
9877
9758
|
throw this.concluded(fetchReason, fetchReason);
|
|
9878
9759
|
}
|
|
9760
|
+
if (!turnContext.turnToken) {
|
|
9761
|
+
turnLog.warn({ turnId }, "dispatcher: turn context carried no turn token");
|
|
9762
|
+
throw this.concluded(NO_TURN_TOKEN_REASON, NO_TURN_TOKEN_REASON);
|
|
9763
|
+
}
|
|
9879
9764
|
this.turnContext = turnContext;
|
|
9880
9765
|
if (this.resumed) {
|
|
9881
9766
|
const sources = { serverFloor: turnContext.turnSeqFloor, outboxFloor };
|
|
@@ -9923,7 +9808,7 @@ var TurnExecution = class {
|
|
|
9923
9808
|
const prepareHook = this.opts.local.prepareHook;
|
|
9924
9809
|
const cabaneCwd = this.turnContext.cwd;
|
|
9925
9810
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
9926
|
-
if (effectiveCwd && !
|
|
9811
|
+
if (effectiveCwd && !existsSync9(effectiveCwd)) {
|
|
9927
9812
|
turnLog.warn(
|
|
9928
9813
|
{ cwd: effectiveCwd },
|
|
9929
9814
|
"The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
|
|
@@ -10127,10 +10012,8 @@ var TurnExecution = class {
|
|
|
10127
10012
|
this.request = buildCompanionTurnRequest({
|
|
10128
10013
|
turnContext: this.turnContext,
|
|
10129
10014
|
baseUrl: this.opts.baseUrl,
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
// the companion PAT (`agentPat`) inside `buildCompanionTurnRequest` otherwise.
|
|
10133
|
-
...this.turnContext.turnToken ? { turnToken: this.turnContext.turnToken } : {},
|
|
10015
|
+
// ACC8: always present; `fetchTurnContext` refused a context without one.
|
|
10016
|
+
turnToken: this.turnContext.turnToken,
|
|
10134
10017
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
10135
10018
|
...this.effectiveCwd ? { cwd: this.effectiveCwd } : {},
|
|
10136
10019
|
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
@@ -10642,7 +10525,7 @@ function fingerprintSessionState(state) {
|
|
|
10642
10525
|
// src/dispatcher.ts
|
|
10643
10526
|
function checkoutState(cwd) {
|
|
10644
10527
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
10645
|
-
if (!
|
|
10528
|
+
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
10646
10529
|
let entries;
|
|
10647
10530
|
try {
|
|
10648
10531
|
entries = readdirSync2(cwd);
|
|
@@ -10652,15 +10535,15 @@ function checkoutState(cwd) {
|
|
|
10652
10535
|
if (entries.length === 0) {
|
|
10653
10536
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
10654
10537
|
}
|
|
10655
|
-
const gitPath =
|
|
10656
|
-
if (!
|
|
10538
|
+
const gitPath = join14(cwd, ".git");
|
|
10539
|
+
if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
|
|
10657
10540
|
let stat;
|
|
10658
10541
|
try {
|
|
10659
10542
|
stat = statSync2(gitPath);
|
|
10660
10543
|
} catch (error) {
|
|
10661
10544
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
10662
10545
|
}
|
|
10663
|
-
if (stat.isDirectory() && !
|
|
10546
|
+
if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
|
|
10664
10547
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
10665
10548
|
return { ok: true, reason: "usable" };
|
|
10666
10549
|
}
|
|
@@ -10820,15 +10703,15 @@ var DispatchClaims = class {
|
|
|
10820
10703
|
|
|
10821
10704
|
// src/outbox.ts
|
|
10822
10705
|
import {
|
|
10823
|
-
existsSync as
|
|
10824
|
-
mkdirSync as
|
|
10706
|
+
existsSync as existsSync11,
|
|
10707
|
+
mkdirSync as mkdirSync9,
|
|
10825
10708
|
readdirSync as readdirSync3,
|
|
10826
|
-
readFileSync as
|
|
10709
|
+
readFileSync as readFileSync8,
|
|
10827
10710
|
renameSync as renameSync4,
|
|
10828
10711
|
rmSync as rmSync7,
|
|
10829
|
-
writeFileSync as
|
|
10712
|
+
writeFileSync as writeFileSync6
|
|
10830
10713
|
} from "fs";
|
|
10831
|
-
import { join as
|
|
10714
|
+
import { join as join15 } from "path";
|
|
10832
10715
|
var MAX_ENTRIES = 2e3;
|
|
10833
10716
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10834
10717
|
var Outbox = class {
|
|
@@ -10841,21 +10724,21 @@ var Outbox = class {
|
|
|
10841
10724
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
10842
10725
|
// cases route writes at the right tmpdir.
|
|
10843
10726
|
dir() {
|
|
10844
|
-
return
|
|
10727
|
+
return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
10845
10728
|
}
|
|
10846
10729
|
fileFor(turnId, seq) {
|
|
10847
|
-
return
|
|
10730
|
+
return join15(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
10848
10731
|
}
|
|
10849
10732
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
10850
10733
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
10851
10734
|
// per-workspace bounds.
|
|
10852
10735
|
persist(entry) {
|
|
10853
10736
|
const dir2 = this.dir();
|
|
10854
|
-
|
|
10737
|
+
mkdirSync9(dir2, { recursive: true });
|
|
10855
10738
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
10856
10739
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
10857
10740
|
try {
|
|
10858
|
-
|
|
10741
|
+
writeFileSync6(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
10859
10742
|
renameSync4(tmp, target);
|
|
10860
10743
|
} catch (err) {
|
|
10861
10744
|
try {
|
|
@@ -10876,7 +10759,7 @@ var Outbox = class {
|
|
|
10876
10759
|
// wedging the drain.
|
|
10877
10760
|
list() {
|
|
10878
10761
|
const dir2 = this.dir();
|
|
10879
|
-
if (!
|
|
10762
|
+
if (!existsSync11(dir2)) return [];
|
|
10880
10763
|
let names;
|
|
10881
10764
|
try {
|
|
10882
10765
|
names = readdirSync3(dir2);
|
|
@@ -10886,9 +10769,9 @@ var Outbox = class {
|
|
|
10886
10769
|
const entries = [];
|
|
10887
10770
|
for (const name of names) {
|
|
10888
10771
|
if (!name.endsWith(".json")) continue;
|
|
10889
|
-
const full =
|
|
10772
|
+
const full = join15(dir2, name);
|
|
10890
10773
|
try {
|
|
10891
|
-
const parsed = JSON.parse(
|
|
10774
|
+
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
10892
10775
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
10893
10776
|
entries.push(parsed);
|
|
10894
10777
|
} else {
|
|
@@ -10922,7 +10805,7 @@ var Outbox = class {
|
|
|
10922
10805
|
// different: it is positive evidence that nothing was ever queued here.
|
|
10923
10806
|
maxSeqForTurn(turnId) {
|
|
10924
10807
|
const dir2 = this.dir();
|
|
10925
|
-
if (!
|
|
10808
|
+
if (!existsSync11(dir2)) return 0;
|
|
10926
10809
|
const names = readdirSync3(dir2);
|
|
10927
10810
|
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
10928
10811
|
let max = 0;
|
|
@@ -10942,7 +10825,7 @@ var Outbox = class {
|
|
|
10942
10825
|
}
|
|
10943
10826
|
size() {
|
|
10944
10827
|
const dir2 = this.dir();
|
|
10945
|
-
if (!
|
|
10828
|
+
if (!existsSync11(dir2)) return 0;
|
|
10946
10829
|
try {
|
|
10947
10830
|
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
10948
10831
|
} catch {
|
|
@@ -11096,7 +10979,7 @@ var SseSubscriber = class {
|
|
|
11096
10979
|
}
|
|
11097
10980
|
async connect() {
|
|
11098
10981
|
const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
|
|
11099
|
-
const url =
|
|
10982
|
+
const url = `${base}${this.opts.path}`;
|
|
11100
10983
|
this.controller = new AbortController();
|
|
11101
10984
|
const headers = {
|
|
11102
10985
|
Authorization: `Bearer ${this.opts.token}`,
|
|
@@ -11203,6 +11086,9 @@ var CompanionSupervisor = class {
|
|
|
11203
11086
|
unrunnableRetryDelaysMs;
|
|
11204
11087
|
deviceApi = null;
|
|
11205
11088
|
deviceSub = null;
|
|
11089
|
+
// ACC8: whether the device stream is open, which is what every workspace's
|
|
11090
|
+
// link on the dashboard reports.
|
|
11091
|
+
deviceConnected = false;
|
|
11206
11092
|
recoveryChecked = false;
|
|
11207
11093
|
recovering = false;
|
|
11208
11094
|
heartbeated = false;
|
|
@@ -11269,7 +11155,18 @@ var CompanionSupervisor = class {
|
|
|
11269
11155
|
token: this.config.deviceToken,
|
|
11270
11156
|
log: this.log,
|
|
11271
11157
|
lastEventId: null,
|
|
11272
|
-
onOpen: () =>
|
|
11158
|
+
onOpen: () => {
|
|
11159
|
+
this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname3()}`);
|
|
11160
|
+
this.deviceConnected = true;
|
|
11161
|
+
for (const wr of this.workspaces.values()) {
|
|
11162
|
+
this.hub.setConnected(wr.workspaceId, true);
|
|
11163
|
+
for (const a of wr.agents.values()) a.kickDrain();
|
|
11164
|
+
}
|
|
11165
|
+
},
|
|
11166
|
+
onClose: () => {
|
|
11167
|
+
this.deviceConnected = false;
|
|
11168
|
+
for (const wr of this.workspaces.values()) this.hub.setConnected(wr.workspaceId, false);
|
|
11169
|
+
},
|
|
11273
11170
|
onMessage: async (ev) => {
|
|
11274
11171
|
if (ev.event === "assignments_changed") {
|
|
11275
11172
|
if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
|
|
@@ -11301,10 +11198,7 @@ var CompanionSupervisor = class {
|
|
|
11301
11198
|
// at least the loops keeping the event loop alive; the SSE promises let a
|
|
11302
11199
|
// clean teardown resolve.
|
|
11303
11200
|
finishedPromises() {
|
|
11304
|
-
return [
|
|
11305
|
-
...this.deviceSub ? [this.deviceSub.finished] : [],
|
|
11306
|
-
...[...this.workspaces.values()].flatMap((w) => w.sub ? [w.sub.finished] : [])
|
|
11307
|
-
];
|
|
11201
|
+
return [...this.deviceSub ? [this.deviceSub.finished] : []];
|
|
11308
11202
|
}
|
|
11309
11203
|
// ---- device-level loops ----
|
|
11310
11204
|
kickHeartbeat() {
|
|
@@ -11452,11 +11346,8 @@ var CompanionSupervisor = class {
|
|
|
11452
11346
|
workspaceId,
|
|
11453
11347
|
slug: first.workspaceSlug,
|
|
11454
11348
|
name: first.workspaceName,
|
|
11455
|
-
sub: null,
|
|
11456
|
-
sseAgentId: null,
|
|
11457
11349
|
agents: /* @__PURE__ */ new Map(),
|
|
11458
|
-
chains: /* @__PURE__ */ new Map()
|
|
11459
|
-
cursor: new CursorTracker(workspaceId)
|
|
11350
|
+
chains: /* @__PURE__ */ new Map()
|
|
11460
11351
|
};
|
|
11461
11352
|
this.workspaces.set(workspaceId, wr);
|
|
11462
11353
|
this.hub.registerWorkspace({
|
|
@@ -11464,6 +11355,7 @@ var CompanionSupervisor = class {
|
|
|
11464
11355
|
slug: first.workspaceSlug,
|
|
11465
11356
|
name: first.workspaceName
|
|
11466
11357
|
});
|
|
11358
|
+
this.hub.setConnected(workspaceId, this.deviceConnected);
|
|
11467
11359
|
}
|
|
11468
11360
|
const store = loadSecretStoreTolerant();
|
|
11469
11361
|
const exposed = new Set(store.names());
|
|
@@ -11523,7 +11415,7 @@ var CompanionSupervisor = class {
|
|
|
11523
11415
|
);
|
|
11524
11416
|
}
|
|
11525
11417
|
}
|
|
11526
|
-
this.
|
|
11418
|
+
if (wr.agents.size === 0) void this.removeWorkspace(wr.workspaceId);
|
|
11527
11419
|
}
|
|
11528
11420
|
addAgent(wr, it, credential, runConfig) {
|
|
11529
11421
|
const agentLog = this.log.child({ agentName: it.agentDisplayName });
|
|
@@ -11733,71 +11625,18 @@ var CompanionSupervisor = class {
|
|
|
11733
11625
|
connectorHealth: this.connectorHealth
|
|
11734
11626
|
});
|
|
11735
11627
|
}
|
|
11736
|
-
// Stand up (or re-point) the workspace's single shared SSE subscription. It is
|
|
11737
|
-
// authed with one assigned agent's PAT; if that agent is unassigned, re-pick
|
|
11738
|
-
// another. With no agents left the workspace runner is torn down.
|
|
11739
|
-
ensureWorkspaceSse(wr) {
|
|
11740
|
-
if (wr.agents.size === 0) {
|
|
11741
|
-
void this.removeWorkspace(wr.workspaceId);
|
|
11742
|
-
return;
|
|
11743
|
-
}
|
|
11744
|
-
if (wr.sub && wr.sseAgentId && wr.agents.has(wr.sseAgentId)) return;
|
|
11745
|
-
const sseAgent = wr.agents.get(wr.sseAgentId ?? "") ?? [...wr.agents.values()][0];
|
|
11746
|
-
if (wr.sub) wr.sub.stop();
|
|
11747
|
-
wr.sseAgentId = sseAgent.agentId;
|
|
11748
|
-
wr.sub = new SseSubscriber({
|
|
11749
|
-
baseUrl: this.config.baseUrl,
|
|
11750
|
-
workspaceId: wr.workspaceId,
|
|
11751
|
-
token: sseAgent.credential,
|
|
11752
|
-
log: this.log,
|
|
11753
|
-
lastEventId: readCursor(wr.workspaceId),
|
|
11754
|
-
onOpen: () => {
|
|
11755
|
-
this.hub.setConnected(wr.workspaceId, true);
|
|
11756
|
-
for (const a of wr.agents.values()) a.kickDrain();
|
|
11757
|
-
},
|
|
11758
|
-
onClose: () => this.hub.setConnected(wr.workspaceId, false),
|
|
11759
|
-
onMessage: (ev) => this.handleWorkspaceMessage(wr, ev),
|
|
11760
|
-
onAuthFailure: (status2) => {
|
|
11761
|
-
this.hub.setAuthFailed(wr.workspaceId);
|
|
11762
|
-
this.log.debug(
|
|
11763
|
-
{ workspaceId: wr.workspaceId, status: status2, sseAgentId: wr.sseAgentId },
|
|
11764
|
-
"companion: workspace stream auth failed \u2014 re-pulling assignments"
|
|
11765
|
-
);
|
|
11766
|
-
wr.sseAgentId = null;
|
|
11767
|
-
void this.refreshAssignments();
|
|
11768
|
-
}
|
|
11769
|
-
});
|
|
11770
|
-
wr.sub.start();
|
|
11771
|
-
this.log.debug({ workspaceId: wr.workspaceId }, "companion: subscribed");
|
|
11772
|
-
}
|
|
11773
11628
|
async removeWorkspace(workspaceId) {
|
|
11774
11629
|
const wr = this.workspaces.get(workspaceId);
|
|
11775
11630
|
if (!wr) return;
|
|
11776
11631
|
for (const agentId of [...wr.agents.keys()]) this.removeAgent(wr, agentId);
|
|
11777
|
-
if (wr.sub) {
|
|
11778
|
-
wr.sub.stop();
|
|
11779
|
-
await wr.sub.finished;
|
|
11780
|
-
}
|
|
11781
11632
|
this.workspaces.delete(workspaceId);
|
|
11782
11633
|
this.hub.removeWorkspace(workspaceId);
|
|
11783
11634
|
}
|
|
11784
11635
|
// ---- SSE event routing (per workspace, BY agentId) ----
|
|
11785
|
-
// Fire-and-forget per event (the subscriber doesn't await us). Cancel events
|
|
11786
|
-
// route to the matching agent immediately; dispatch events serialize per
|
|
11787
|
-
// (conversation, agent); everything else just advances the cursor.
|
|
11788
|
-
async handleWorkspaceMessage(wr, ev) {
|
|
11789
|
-
if (ev.event !== "message") return;
|
|
11790
|
-
this.hub.recordEvent(wr.workspaceId);
|
|
11791
|
-
const wire = this.parseFrame(ev);
|
|
11792
|
-
if (!wire) return;
|
|
11793
|
-
if (ev.id) wr.cursor.observe(ev.id);
|
|
11794
|
-
await this.deliverFrame(wr, ev.id, wire);
|
|
11795
|
-
}
|
|
11796
11636
|
// ACC10: the device stream's frames — dispatch and cancel, addressed to this
|
|
11797
|
-
// device and authenticated as it.
|
|
11798
|
-
//
|
|
11799
|
-
//
|
|
11800
|
-
// nothing here observes or settles one.
|
|
11637
|
+
// device and authenticated as it. ACC8: the only stream this companion opens.
|
|
11638
|
+
// It keeps no cursor: it replays what is still offered on every connect
|
|
11639
|
+
// instead of resuming from a `Last-Event-ID`.
|
|
11801
11640
|
async handleDeviceMessage(ev) {
|
|
11802
11641
|
const wire = this.parseFrame(ev);
|
|
11803
11642
|
if (!wire) return;
|
|
@@ -11820,7 +11659,7 @@ var CompanionSupervisor = class {
|
|
|
11820
11659
|
}
|
|
11821
11660
|
}
|
|
11822
11661
|
this.hub.recordEvent(wr.workspaceId);
|
|
11823
|
-
await this.deliverFrame(wr,
|
|
11662
|
+
await this.deliverFrame(wr, wire);
|
|
11824
11663
|
}
|
|
11825
11664
|
parseFrame(ev) {
|
|
11826
11665
|
try {
|
|
@@ -11833,30 +11672,27 @@ var CompanionSupervisor = class {
|
|
|
11833
11672
|
return null;
|
|
11834
11673
|
}
|
|
11835
11674
|
}
|
|
11836
|
-
// ACC10: the dedupe gate
|
|
11837
|
-
//
|
|
11838
|
-
//
|
|
11839
|
-
// stream's cursor. Settling a duplicate earlier than that would advance the
|
|
11840
|
-
// low-water mark past an in-flight turn, which is exactly the CT282 bug.
|
|
11675
|
+
// ACC10: the dedupe gate. A reconnect replays every dispatch still offered,
|
|
11676
|
+
// including one whose turn is already running here; the first copy runs the
|
|
11677
|
+
// turn and a later one waits for it and then does nothing.
|
|
11841
11678
|
//
|
|
11842
11679
|
// A cancel needs no gate: `dispatcher.cancel` on an already-aborted run is a
|
|
11843
11680
|
// no-op that reports `false`, so a second copy is inert.
|
|
11844
|
-
async deliverFrame(wr,
|
|
11681
|
+
async deliverFrame(wr, wire) {
|
|
11845
11682
|
const claim = wire.type === "device:dispatch_requested" && wire.id ? this.claims.claim(wire.id) : null;
|
|
11846
11683
|
if (claim && !claim.first) {
|
|
11847
11684
|
await claim.done;
|
|
11848
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11849
11685
|
return;
|
|
11850
11686
|
}
|
|
11851
11687
|
try {
|
|
11852
|
-
await this.routeFrame(wr,
|
|
11688
|
+
await this.routeFrame(wr, wire);
|
|
11853
11689
|
} finally {
|
|
11854
11690
|
if (claim) this.claims.release(wire.id);
|
|
11855
11691
|
}
|
|
11856
11692
|
}
|
|
11857
11693
|
// Everything below here is the routing as it was: it takes a frame and knows
|
|
11858
11694
|
// nothing about which stream carried it.
|
|
11859
|
-
async routeFrame(wr,
|
|
11695
|
+
async routeFrame(wr, wire) {
|
|
11860
11696
|
if (wire.type === "device:cancel_requested") {
|
|
11861
11697
|
const payload2 = {
|
|
11862
11698
|
type: "device:cancel_requested",
|
|
@@ -11876,12 +11712,10 @@ var CompanionSupervisor = class {
|
|
|
11876
11712
|
);
|
|
11877
11713
|
}
|
|
11878
11714
|
}
|
|
11879
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11880
11715
|
return;
|
|
11881
11716
|
}
|
|
11882
11717
|
const payload = dispatchPayload(wire);
|
|
11883
11718
|
if (!payload) {
|
|
11884
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11885
11719
|
return;
|
|
11886
11720
|
}
|
|
11887
11721
|
if (this.deviceId && payload.deviceId && payload.deviceId !== this.deviceId) {
|
|
@@ -11894,7 +11728,6 @@ var CompanionSupervisor = class {
|
|
|
11894
11728
|
},
|
|
11895
11729
|
"companion: dispatch addressed to another device; ignoring"
|
|
11896
11730
|
);
|
|
11897
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11898
11731
|
return;
|
|
11899
11732
|
}
|
|
11900
11733
|
let agent = wr.agents.get(payload.agentId);
|
|
@@ -11902,16 +11735,14 @@ var CompanionSupervisor = class {
|
|
|
11902
11735
|
agent = await this.recoverRacedAgent(wr, payload);
|
|
11903
11736
|
if (!agent) {
|
|
11904
11737
|
if (this.deviceId && payload.deviceId === this.deviceId) {
|
|
11905
|
-
|
|
11906
|
-
if (!reported) return;
|
|
11738
|
+
await this.reportUnrunnableDispatch(wr.workspaceId, payload);
|
|
11907
11739
|
}
|
|
11908
|
-
if (cursorId) wr.cursor.settle(cursorId);
|
|
11909
11740
|
return;
|
|
11910
11741
|
}
|
|
11911
11742
|
}
|
|
11912
11743
|
const chainKey = `${payload.conversationId}|${payload.agentId}`;
|
|
11913
11744
|
const prev = wr.chains.get(chainKey) ?? Promise.resolve();
|
|
11914
|
-
const tail = prev.then(() => this.runDispatch(wr,
|
|
11745
|
+
const tail = prev.then(() => this.runDispatch(wr, wire.id, payload, agent)).catch((err) => {
|
|
11915
11746
|
this.log.warn(
|
|
11916
11747
|
{
|
|
11917
11748
|
workspaceId: wr.workspaceId,
|
|
@@ -11928,8 +11759,7 @@ var CompanionSupervisor = class {
|
|
|
11928
11759
|
}
|
|
11929
11760
|
// CT1146: tell the server this device cannot run a dispatch it was addressed to.
|
|
11930
11761
|
//
|
|
11931
|
-
// Returns whether the report LANDED
|
|
11932
|
-
// caller may settle the cursor. The server checks the recorded dispatch
|
|
11762
|
+
// Returns whether the report LANDED. The server checks the recorded dispatch
|
|
11933
11763
|
// destination even if the agent's assignment has moved. Reporting promptly
|
|
11934
11764
|
// explains the roster miss; the thirty-minute unanswered-offer deadline is
|
|
11935
11765
|
// only the backstop when no report lands.
|
|
@@ -12023,10 +11853,10 @@ var CompanionSupervisor = class {
|
|
|
12023
11853
|
};
|
|
12024
11854
|
const tail = this.runDispatch(
|
|
12025
11855
|
wr,
|
|
12026
|
-
// No frame delivered this one — it came off `/runs` — so there is
|
|
12027
|
-
//
|
|
12028
|
-
//
|
|
12029
|
-
|
|
11856
|
+
// No frame delivered this one — it came off `/runs` — so there is no
|
|
11857
|
+
// dispatch id to dedupe on. The `recoveredTurns` set above is this
|
|
11858
|
+
// path's own guard.
|
|
11859
|
+
"",
|
|
12030
11860
|
payload,
|
|
12031
11861
|
agent,
|
|
12032
11862
|
run.turnId
|
|
@@ -12048,61 +11878,55 @@ var CompanionSupervisor = class {
|
|
|
12048
11878
|
}
|
|
12049
11879
|
}
|
|
12050
11880
|
// One turn: the SJ516 F7 / CT26 skip-vs-resume dedupe, the dispatch, then
|
|
12051
|
-
//
|
|
11881
|
+
// the console summary. Mirrors the old per-workspace `handleEvent`, now
|
|
12052
11882
|
// scoped to one agent.
|
|
12053
|
-
async runDispatch(wr,
|
|
11883
|
+
async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
|
|
12054
11884
|
const workspaceId = wr.workspaceId;
|
|
12055
|
-
|
|
12056
|
-
if (
|
|
12057
|
-
};
|
|
12058
|
-
if (ids.dispatchId && hasDispatched(workspaceId, ids.dispatchId)) {
|
|
12059
|
-
if (hasCompleted(workspaceId, ids.dispatchId)) {
|
|
11885
|
+
if (dispatchId && hasDispatched(workspaceId, dispatchId)) {
|
|
11886
|
+
if (hasCompleted(workspaceId, dispatchId)) {
|
|
12060
11887
|
this.log.debug(
|
|
12061
|
-
{ workspaceId, eventId:
|
|
11888
|
+
{ workspaceId, eventId: dispatchId },
|
|
12062
11889
|
"companion: skipping already-completed event (resume after restart)"
|
|
12063
11890
|
);
|
|
12064
|
-
settleCursor();
|
|
12065
11891
|
return;
|
|
12066
11892
|
}
|
|
12067
11893
|
if (noResume()) {
|
|
12068
11894
|
this.log.debug(
|
|
12069
|
-
{ workspaceId, eventId:
|
|
11895
|
+
{ workspaceId, eventId: dispatchId },
|
|
12070
11896
|
"companion: skipping interrupted turn (CABANE_COMPANION_NO_RESUME=1) \u2014 resume disabled for this boot"
|
|
12071
11897
|
);
|
|
12072
|
-
markCompleted(workspaceId,
|
|
12073
|
-
settleCursor();
|
|
11898
|
+
markCompleted(workspaceId, dispatchId);
|
|
12074
11899
|
return;
|
|
12075
11900
|
}
|
|
12076
|
-
const attempt = bumpResumeAttempt(workspaceId,
|
|
11901
|
+
const attempt = bumpResumeAttempt(workspaceId, dispatchId);
|
|
12077
11902
|
if (attempt > MAX_RESUME_ATTEMPTS) {
|
|
12078
11903
|
this.log.error(
|
|
12079
|
-
{ workspaceId, eventId:
|
|
11904
|
+
{ workspaceId, eventId: dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
12080
11905
|
"An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
|
|
12081
11906
|
);
|
|
12082
|
-
markCompleted(workspaceId,
|
|
12083
|
-
settleCursor();
|
|
11907
|
+
markCompleted(workspaceId, dispatchId);
|
|
12084
11908
|
return;
|
|
12085
11909
|
}
|
|
12086
11910
|
this.log.debug(
|
|
12087
|
-
{ workspaceId, eventId:
|
|
11911
|
+
{ workspaceId, eventId: dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
|
|
12088
11912
|
"companion: re-dispatching interrupted turn (resume after restart)"
|
|
12089
11913
|
);
|
|
12090
11914
|
}
|
|
12091
|
-
const resumedTurnId = recoveredTurnId ?? (
|
|
11915
|
+
const resumedTurnId = recoveredTurnId ?? (dispatchId ? turnIdForEvent(workspaceId, dispatchId) : null);
|
|
12092
11916
|
const turnId = resumedTurnId ?? randomUUID4();
|
|
12093
|
-
if (
|
|
12094
|
-
const liveTurnIds = rememberTurnId(workspaceId,
|
|
11917
|
+
if (dispatchId) {
|
|
11918
|
+
const liveTurnIds = rememberTurnId(workspaceId, dispatchId, turnId);
|
|
12095
11919
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
12096
11920
|
this.log.debug(
|
|
12097
11921
|
{ workspaceId, liveTurnIds, threshold: TURN_ID_OVERFLOW_WARN },
|
|
12098
11922
|
"companion: in-flight turn-id map is implausibly large \u2014 completion pruning is likely broken. Keeping every mapping; dropping one would silently lose a resumable turn."
|
|
12099
11923
|
);
|
|
12100
11924
|
}
|
|
12101
|
-
markDispatched(workspaceId,
|
|
11925
|
+
markDispatched(workspaceId, dispatchId);
|
|
12102
11926
|
}
|
|
12103
11927
|
if (resumedTurnId) {
|
|
12104
11928
|
this.log.debug(
|
|
12105
|
-
{ workspaceId, eventId:
|
|
11929
|
+
{ workspaceId, eventId: dispatchId, turnId },
|
|
12106
11930
|
"companion: resuming an interrupted turn under its original id"
|
|
12107
11931
|
);
|
|
12108
11932
|
}
|
|
@@ -12117,8 +11941,7 @@ var CompanionSupervisor = class {
|
|
|
12117
11941
|
turnId,
|
|
12118
11942
|
resumed: resumedTurnId !== null
|
|
12119
11943
|
});
|
|
12120
|
-
if (
|
|
12121
|
-
settleCursor();
|
|
11944
|
+
if (dispatchId) markCompleted(workspaceId, dispatchId);
|
|
12122
11945
|
const durationS = (result.durationMs / 1e3).toFixed(1);
|
|
12123
11946
|
const bindings = {
|
|
12124
11947
|
...turnBindings,
|
|
@@ -12367,12 +12190,10 @@ var CompanionSupervisor = class {
|
|
|
12367
12190
|
this.deviceSub?.stop();
|
|
12368
12191
|
for (const wr of this.workspaces.values()) {
|
|
12369
12192
|
for (const a of wr.agents.values()) a.cancelDrain();
|
|
12370
|
-
if (wr.sub) wr.sub.stop();
|
|
12371
12193
|
}
|
|
12372
12194
|
await Promise.all([
|
|
12373
12195
|
...heartbeat ? [waitForBoundedHeartbeat(heartbeat)] : [],
|
|
12374
|
-
...this.deviceSub ? [this.deviceSub.finished] : []
|
|
12375
|
-
...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
|
|
12196
|
+
...this.deviceSub ? [this.deviceSub.finished] : []
|
|
12376
12197
|
]);
|
|
12377
12198
|
}
|
|
12378
12199
|
async requestStop() {
|
|
@@ -12469,22 +12290,22 @@ function handleUncaught(log, err, origin) {
|
|
|
12469
12290
|
}
|
|
12470
12291
|
|
|
12471
12292
|
// src/crash-marker.ts
|
|
12472
|
-
import { existsSync as
|
|
12473
|
-
import { join as
|
|
12293
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
12294
|
+
import { join as join16 } from "path";
|
|
12474
12295
|
function crashMarkerPath() {
|
|
12475
|
-
return
|
|
12296
|
+
return join16(cabaneDir(), "last-error.json");
|
|
12476
12297
|
}
|
|
12477
12298
|
function recordCrash(rec2) {
|
|
12478
12299
|
try {
|
|
12479
|
-
|
|
12480
|
-
|
|
12300
|
+
mkdirSync10(cabaneDir(), { recursive: true });
|
|
12301
|
+
writeFileSync7(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
|
|
12481
12302
|
} catch {
|
|
12482
12303
|
}
|
|
12483
12304
|
}
|
|
12484
12305
|
function clearCrash() {
|
|
12485
12306
|
try {
|
|
12486
12307
|
const path = crashMarkerPath();
|
|
12487
|
-
if (
|
|
12308
|
+
if (existsSync12(path)) rmSync8(path, { force: true });
|
|
12488
12309
|
} catch {
|
|
12489
12310
|
}
|
|
12490
12311
|
}
|
|
@@ -12655,14 +12476,14 @@ async function closeSurfaces(control, dashboard) {
|
|
|
12655
12476
|
|
|
12656
12477
|
// src/commands/daemon.ts
|
|
12657
12478
|
import { spawn as spawn6 } from "child_process";
|
|
12658
|
-
import { closeSync as closeSync3, mkdirSync as
|
|
12479
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync11, openSync as openSync3 } from "fs";
|
|
12659
12480
|
|
|
12660
12481
|
// src/cli-entry.ts
|
|
12661
|
-
import { existsSync as
|
|
12482
|
+
import { existsSync as existsSync13 } from "fs";
|
|
12662
12483
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
12663
12484
|
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
12664
12485
|
function companionCliEntry(deps = {}) {
|
|
12665
|
-
const exists = deps.exists ??
|
|
12486
|
+
const exists = deps.exists ?? existsSync13;
|
|
12666
12487
|
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath3(new URL(rel, import.meta.url)));
|
|
12667
12488
|
for (const candidate of candidates) {
|
|
12668
12489
|
if (exists(candidate)) return candidate;
|
|
@@ -12763,7 +12584,7 @@ Stop: cabane-companion stop
|
|
|
12763
12584
|
}
|
|
12764
12585
|
function defaultSpawnDetached(args) {
|
|
12765
12586
|
const cliPath = companionCliEntry();
|
|
12766
|
-
|
|
12587
|
+
mkdirSync11(cabaneDir(), { recursive: true });
|
|
12767
12588
|
const logFd = openSync3(companionLogPath(), "a");
|
|
12768
12589
|
try {
|
|
12769
12590
|
return spawn6(process.execPath, [cliPath, ...args], {
|
|
@@ -13192,8 +13013,8 @@ function isAlive(kill, pid) {
|
|
|
13192
13013
|
}
|
|
13193
13014
|
|
|
13194
13015
|
// src/commands/transcript.ts
|
|
13195
|
-
import { existsSync as
|
|
13196
|
-
import { isAbsolute, join as
|
|
13016
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, readdirSync as readdirSync5 } from "fs";
|
|
13017
|
+
import { isAbsolute, join as join17 } from "path";
|
|
13197
13018
|
async function transcript(opts = {}) {
|
|
13198
13019
|
const dir2 = transcriptsDir();
|
|
13199
13020
|
if (opts.follow) {
|
|
@@ -13210,7 +13031,7 @@ async function transcript(opts = {}) {
|
|
|
13210
13031
|
process.stdout.write(emptyMessage(dir2));
|
|
13211
13032
|
return;
|
|
13212
13033
|
}
|
|
13213
|
-
process.stdout.write(renderFile(
|
|
13034
|
+
process.stdout.write(renderFile(join17(dir2, newest)) + "\n");
|
|
13214
13035
|
return;
|
|
13215
13036
|
}
|
|
13216
13037
|
printList(dir2);
|
|
@@ -13293,7 +13114,7 @@ function isComplete(content) {
|
|
|
13293
13114
|
async function followTranscripts(dir2) {
|
|
13294
13115
|
const follower = new TranscriptFollower({
|
|
13295
13116
|
listFiles: () => listFiles(dir2),
|
|
13296
|
-
read: (f) =>
|
|
13117
|
+
read: (f) => readFileSync10(join17(dir2, f), "utf8"),
|
|
13297
13118
|
write: (s) => process.stdout.write(s),
|
|
13298
13119
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
13299
13120
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -13333,7 +13154,7 @@ function printList(dir2) {
|
|
|
13333
13154
|
|
|
13334
13155
|
`);
|
|
13335
13156
|
for (const f of files.slice(0, 20)) {
|
|
13336
|
-
const { meta, outcome } = peek(
|
|
13157
|
+
const { meta, outcome } = peek(join17(dir2, f));
|
|
13337
13158
|
const when = fmtTime(rec(meta)?.ts);
|
|
13338
13159
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
13339
13160
|
const o = rec(outcome);
|
|
@@ -13354,7 +13175,7 @@ function peek(path) {
|
|
|
13354
13175
|
let meta;
|
|
13355
13176
|
let outcome;
|
|
13356
13177
|
try {
|
|
13357
|
-
for (const line of
|
|
13178
|
+
for (const line of readFileSync10(path, "utf8").split("\n")) {
|
|
13358
13179
|
if (!line.trim()) continue;
|
|
13359
13180
|
const o = safeParse(line);
|
|
13360
13181
|
const t = str2(rec(o)?.type);
|
|
@@ -13367,13 +13188,13 @@ function peek(path) {
|
|
|
13367
13188
|
}
|
|
13368
13189
|
function resolveTarget(dir2, target) {
|
|
13369
13190
|
if (isAbsolute(target) || target.includes("/")) {
|
|
13370
|
-
if (
|
|
13191
|
+
if (existsSync14(target)) return target;
|
|
13371
13192
|
throw new CompanionError(`no transcript at ${target}.`);
|
|
13372
13193
|
}
|
|
13373
|
-
const exact =
|
|
13374
|
-
if (
|
|
13194
|
+
const exact = join17(dir2, target);
|
|
13195
|
+
if (existsSync14(exact)) return exact;
|
|
13375
13196
|
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
13376
|
-
if (matches.length === 1) return
|
|
13197
|
+
if (matches.length === 1) return join17(dir2, matches[0]);
|
|
13377
13198
|
if (matches.length === 0) {
|
|
13378
13199
|
throw new CompanionError(
|
|
13379
13200
|
`no transcript matching "${target}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
|
|
@@ -13387,7 +13208,7 @@ function resolveTarget(dir2, target) {
|
|
|
13387
13208
|
function renderFile(path) {
|
|
13388
13209
|
let content;
|
|
13389
13210
|
try {
|
|
13390
|
-
content =
|
|
13211
|
+
content = readFileSync10(path, "utf8");
|
|
13391
13212
|
} catch (err) {
|
|
13392
13213
|
throw new CompanionError(
|
|
13393
13214
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -13547,7 +13368,7 @@ program.command("pair").description(
|
|
|
13547
13368
|
});
|
|
13548
13369
|
});
|
|
13549
13370
|
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
13550
|
-
writeCompletedPairing(
|
|
13371
|
+
writeCompletedPairing(readFileSync11(0, "utf8"));
|
|
13551
13372
|
});
|
|
13552
13373
|
program.command("start").description(
|
|
13553
13374
|
"pair this device if needed, connect a coding agent on it, and run in the background."
|