@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.
Files changed (3) hide show
  1. package/dist/cli.js +163 -342
  2. package/dist/runtime.js +146 -325
  3. 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 readFileSync12 } from "fs";
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 mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync7 } from "fs";
3616
- import { join as join10 } from "path";
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 join10(cabaneDir(), log);
3513
+ return join9(cabaneDir(), log);
3620
3514
  }
3621
- function pathFor2(log, workspaceId) {
3622
- return join10(dir(log), encodeURIComponent(workspaceId));
3515
+ function pathFor(log, workspaceId) {
3516
+ return join9(dir(log), encodeURIComponent(workspaceId));
3623
3517
  }
3624
3518
  function readIds(log, workspaceId) {
3625
- const path = pathFor2(log, workspaceId);
3626
- if (!existsSync7(path)) return [];
3519
+ const path = pathFor(log, workspaceId);
3520
+ if (!existsSync6(path)) return [];
3627
3521
  try {
3628
- return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
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
- mkdirSync7(dir(log), { recursive: true });
3642
- writeFileSync5(pathFor2(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
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 join10(cabaneDir(), "resume-attempts");
3553
+ return join9(cabaneDir(), "resume-attempts");
3660
3554
  }
3661
3555
  function resumePathFor(workspaceId) {
3662
- return join10(resumeDir(), encodeURIComponent(workspaceId));
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 (!existsSync7(path)) return out;
3561
+ if (!existsSync6(path)) return out;
3668
3562
  try {
3669
- for (const line of readFileSync5(path, "utf8").split("\n")) {
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
- mkdirSync7(resumeDir(), { recursive: true });
3690
- writeFileSync5(
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 join10(cabaneDir(), "turns");
3592
+ return join9(cabaneDir(), "turns");
3699
3593
  }
3700
3594
  function turnPathFor(workspaceId) {
3701
- return join10(turnDir(), encodeURIComponent(workspaceId));
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 (!existsSync7(path)) return out;
3601
+ if (!existsSync6(path)) return out;
3708
3602
  try {
3709
- for (const line of readFileSync5(path, "utf8").split("\n")) {
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
- mkdirSync7(turnDir(), { recursive: true });
3623
+ mkdirSync6(turnDir(), { recursive: true });
3730
3624
  const path = turnPathFor(workspaceId);
3731
3625
  const tmp = `${path}.${process.pid}.tmp`;
3732
- writeFileSync5(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
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 existsSync11, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
8508
- import { join as join15 } from "path";
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 existsSync10 } from "fs";
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
- // CT306: prefer the per-turn OBO credential; fall back to the companion PAT
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
- // CT714: mount the turn-control surface ONLY when a real turn token backs
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 join11 } from "path";
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(join11(tmpdir(), PREFIX));
8793
- const path = join11(dir2, "instructions.md");
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 mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
8900
- import { join as join12 } from "path";
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 join12(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8782
+ return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8903
8783
  }
8904
8784
  function conversationDir(workspaceId, conversationId) {
8905
- return join12(dirFor(workspaceId), encodeURIComponent(conversationId));
8785
+ return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
8906
8786
  }
8907
- function pathFor3(workspaceId, conversationId, agentId) {
8908
- return join12(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
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 = pathFor3(workspaceId, conversationId, agentId);
8912
- if (!existsSync8(path)) return null;
8791
+ const path = pathFor2(workspaceId, conversationId, agentId);
8792
+ if (!existsSync7(path)) return null;
8913
8793
  try {
8914
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
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
- mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
8928
- writeFileSync6(
8929
- pathFor3(workspaceId, conversationId, agentId),
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(pathFor3(workspaceId, conversationId, agentId), { force: true });
8815
+ rmSync5(pathFor2(workspaceId, conversationId, agentId), { force: true });
8936
8816
  }
8937
8817
 
8938
8818
  // src/secrets.ts
8939
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
8940
- import { join as join13 } from "path";
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 join13(cabaneDir(), "secrets.json");
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 (!existsSync9(path)) return makeStore({});
8829
+ if (!existsSync8(path)) return makeStore({});
8950
8830
  let raw;
8951
8831
  try {
8952
- raw = readFileSync7(path, "utf8");
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 mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
9031
- import { basename, dirname as dirname6, join as join14 } from "path";
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 join14(cabaneDir(), "transcripts");
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 = join14(dir2, fileName(meta));
8923
+ this.path = join13(dir2, fileName(meta));
9044
8924
  try {
9045
- mkdirSync9(dir2, { recursive: true });
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 = join14(dirname6(this.path), "anomalies");
9073
- mkdirSync9(dir2, { recursive: true, mode: 448 });
8952
+ const dir2 = join13(dirname6(this.path), "anomalies");
8953
+ mkdirSync8(dir2, { recursive: true, mode: 448 });
9074
8954
  chmodSync4(dir2, 448);
9075
- const target = join14(dir2, basename(this.path));
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(join14(dir2, f), { force: true });
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 readFileSync8 } from "fs";
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(readFileSync8("/proc/self/cgroup", "utf8"));
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 && !existsSync10(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
- agentPat: this.opts.credential,
10131
- // CT306: the per-turn OBO credential when the API minted one; falls back to
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 (!existsSync11(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
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 = join15(cwd, ".git");
10656
- if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
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() && !existsSync11(join15(gitPath, "HEAD")))
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 existsSync12,
10824
- mkdirSync as mkdirSync10,
10706
+ existsSync as existsSync11,
10707
+ mkdirSync as mkdirSync9,
10825
10708
  readdirSync as readdirSync3,
10826
- readFileSync as readFileSync9,
10709
+ readFileSync as readFileSync8,
10827
10710
  renameSync as renameSync4,
10828
10711
  rmSync as rmSync7,
10829
- writeFileSync as writeFileSync7
10712
+ writeFileSync as writeFileSync6
10830
10713
  } from "fs";
10831
- import { join as join16 } from "path";
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 join16(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
10727
+ return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
10845
10728
  }
10846
10729
  fileFor(turnId, seq) {
10847
- return join16(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
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
- mkdirSync10(dir2, { recursive: true });
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
- writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
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 (!existsSync12(dir2)) return [];
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 = join16(dir2, name);
10772
+ const full = join15(dir2, name);
10890
10773
  try {
10891
- const parsed = JSON.parse(readFileSync9(full, "utf8"));
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 (!existsSync12(dir2)) return 0;
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 (!existsSync12(dir2)) return 0;
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 = this.opts.path ? `${base}${this.opts.path}` : `${base}/api/workspaces/${this.opts.workspaceId}/events`;
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: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname3()}`),
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.ensureWorkspaceSse(wr);
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. Same frames and the same routing as above;
11798
- // what differs is that this stream keeps no cursor (it replays what is still
11799
- // offered on every connect instead of resuming from a `Last-Event-ID`), so
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, "", wire);
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, and the only place that exists because there are two
11837
- // streams. Both carry the same frame with the same dispatch id; the first copy
11838
- // to arrive runs the turn, and the second waits for it and then settles its own
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, cursorId, wire) {
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, cursorId, wire);
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, cursorId, wire) {
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
- const reported = await this.reportUnrunnableDispatch(wr.workspaceId, payload);
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, { dispatchId: wire.id, cursorId }, payload, agent)).catch((err) => {
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, because that answer decides whether the
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
- // neither a dispatch id to dedupe on nor a cursor to settle. The
12028
- // `recoveredTurns` set above is this path's own guard.
12029
- { dispatchId: "", cursorId: "" },
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
- // cursor + console summary. Mirrors the old per-workspace `handleEvent`, now
11881
+ // the console summary. Mirrors the old per-workspace `handleEvent`, now
12052
11882
  // scoped to one agent.
12053
- async runDispatch(wr, ids, payload, agent, recoveredTurnId) {
11883
+ async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
12054
11884
  const workspaceId = wr.workspaceId;
12055
- const settleCursor = () => {
12056
- if (ids.cursorId) wr.cursor.settle(ids.cursorId);
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: ids.dispatchId },
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: ids.dispatchId },
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, ids.dispatchId);
12073
- settleCursor();
11898
+ markCompleted(workspaceId, dispatchId);
12074
11899
  return;
12075
11900
  }
12076
- const attempt = bumpResumeAttempt(workspaceId, ids.dispatchId);
11901
+ const attempt = bumpResumeAttempt(workspaceId, dispatchId);
12077
11902
  if (attempt > MAX_RESUME_ATTEMPTS) {
12078
11903
  this.log.error(
12079
- { workspaceId, eventId: ids.dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
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, ids.dispatchId);
12083
- settleCursor();
11907
+ markCompleted(workspaceId, dispatchId);
12084
11908
  return;
12085
11909
  }
12086
11910
  this.log.debug(
12087
- { workspaceId, eventId: ids.dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
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 ?? (ids.dispatchId ? turnIdForEvent(workspaceId, ids.dispatchId) : null);
11915
+ const resumedTurnId = recoveredTurnId ?? (dispatchId ? turnIdForEvent(workspaceId, dispatchId) : null);
12092
11916
  const turnId = resumedTurnId ?? randomUUID4();
12093
- if (ids.dispatchId) {
12094
- const liveTurnIds = rememberTurnId(workspaceId, ids.dispatchId, turnId);
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, ids.dispatchId);
11925
+ markDispatched(workspaceId, dispatchId);
12102
11926
  }
12103
11927
  if (resumedTurnId) {
12104
11928
  this.log.debug(
12105
- { workspaceId, eventId: ids.dispatchId, turnId },
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 (ids.dispatchId) markCompleted(workspaceId, ids.dispatchId);
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 existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync10, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
12473
- import { join as join17 } from "path";
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 join17(cabaneDir(), "last-error.json");
12296
+ return join16(cabaneDir(), "last-error.json");
12476
12297
  }
12477
12298
  function recordCrash(rec2) {
12478
12299
  try {
12479
- mkdirSync11(cabaneDir(), { recursive: true });
12480
- writeFileSync8(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
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 (existsSync13(path)) rmSync8(path, { force: true });
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 mkdirSync12, openSync as openSync3 } from "fs";
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 existsSync14 } from "fs";
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 ?? existsSync14;
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
- mkdirSync12(cabaneDir(), { recursive: true });
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 existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync5 } from "fs";
13196
- import { isAbsolute, join as join18 } from "path";
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(join18(dir2, newest)) + "\n");
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) => readFileSync11(join18(dir2, f), "utf8"),
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(join18(dir2, f));
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 readFileSync11(path, "utf8").split("\n")) {
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 (existsSync15(target)) return target;
13191
+ if (existsSync14(target)) return target;
13371
13192
  throw new CompanionError(`no transcript at ${target}.`);
13372
13193
  }
13373
- const exact = join18(dir2, target);
13374
- if (existsSync15(exact)) return exact;
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 join18(dir2, matches[0]);
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 = readFileSync11(path, "utf8");
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(readFileSync12(0, "utf8"));
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."