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