@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.
Files changed (3) hide show
  1. package/dist/cli.js +166 -342
  2. package/dist/runtime.js +149 -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";
@@ -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 mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync7 } from "fs";
3614
- 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";
3615
3511
  var MAX_IDS = 256;
3616
3512
  function dir(log) {
3617
- return join10(cabaneDir(), log);
3513
+ return join9(cabaneDir(), log);
3618
3514
  }
3619
- function pathFor2(log, workspaceId) {
3620
- return join10(dir(log), encodeURIComponent(workspaceId));
3515
+ function pathFor(log, workspaceId) {
3516
+ return join9(dir(log), encodeURIComponent(workspaceId));
3621
3517
  }
3622
3518
  function readIds(log, workspaceId) {
3623
- const path = pathFor2(log, workspaceId);
3624
- if (!existsSync7(path)) return [];
3519
+ const path = pathFor(log, workspaceId);
3520
+ if (!existsSync6(path)) return [];
3625
3521
  try {
3626
- 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);
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
- mkdirSync7(dir(log), { recursive: true });
3640
- 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");
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 join10(cabaneDir(), "resume-attempts");
3553
+ return join9(cabaneDir(), "resume-attempts");
3658
3554
  }
3659
3555
  function resumePathFor(workspaceId) {
3660
- return join10(resumeDir(), encodeURIComponent(workspaceId));
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 (!existsSync7(path)) return out;
3561
+ if (!existsSync6(path)) return out;
3666
3562
  try {
3667
- for (const line of readFileSync5(path, "utf8").split("\n")) {
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
- mkdirSync7(resumeDir(), { recursive: true });
3688
- writeFileSync5(
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 join10(cabaneDir(), "turns");
3592
+ return join9(cabaneDir(), "turns");
3697
3593
  }
3698
3594
  function turnPathFor(workspaceId) {
3699
- return join10(turnDir(), encodeURIComponent(workspaceId));
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 (!existsSync7(path)) return out;
3601
+ if (!existsSync6(path)) return out;
3706
3602
  try {
3707
- for (const line of readFileSync5(path, "utf8").split("\n")) {
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
- mkdirSync7(turnDir(), { recursive: true });
3623
+ mkdirSync6(turnDir(), { recursive: true });
3728
3624
  const path = turnPathFor(workspaceId);
3729
3625
  const tmp = `${path}.${process.pid}.tmp`;
3730
- 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");
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 existsSync11, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
8506
- 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";
8507
8403
 
8508
8404
  // src/turn-execution.ts
8509
8405
  import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
8510
- import { existsSync as existsSync10 } from "fs";
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
- // CT306: prefer the per-turn OBO credential; fall back to the companion PAT
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
- // CT714: mount the turn-control surface ONLY when a real turn token backs
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 join11 } from "path";
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(join11(tmpdir(), PREFIX));
8791
- const path = join11(dir2, "instructions.md");
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 mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
8898
- 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";
8899
8781
  function dirFor(workspaceId) {
8900
- return join12(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8782
+ return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8901
8783
  }
8902
8784
  function conversationDir(workspaceId, conversationId) {
8903
- return join12(dirFor(workspaceId), encodeURIComponent(conversationId));
8785
+ return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
8904
8786
  }
8905
- function pathFor3(workspaceId, conversationId, agentId) {
8906
- return join12(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
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 = pathFor3(workspaceId, conversationId, agentId);
8910
- if (!existsSync8(path)) return null;
8791
+ const path = pathFor2(workspaceId, conversationId, agentId);
8792
+ if (!existsSync7(path)) return null;
8911
8793
  try {
8912
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
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
- mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
8926
- writeFileSync6(
8927
- pathFor3(workspaceId, conversationId, agentId),
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(pathFor3(workspaceId, conversationId, agentId), { force: true });
8815
+ rmSync5(pathFor2(workspaceId, conversationId, agentId), { force: true });
8934
8816
  }
8935
8817
 
8936
8818
  // src/secrets.ts
8937
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
8938
- import { join as join13 } from "path";
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 join13(cabaneDir(), "secrets.json");
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 (!existsSync9(path)) return makeStore({});
8829
+ if (!existsSync8(path)) return makeStore({});
8948
8830
  let raw;
8949
8831
  try {
8950
- raw = readFileSync7(path, "utf8");
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 mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
9029
- 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";
9030
8912
  function transcriptsDir() {
9031
- return join14(cabaneDir(), "transcripts");
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 = join14(dir2, fileName(meta));
8923
+ this.path = join13(dir2, fileName(meta));
9042
8924
  try {
9043
- mkdirSync9(dir2, { recursive: true });
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 = join14(dirname6(this.path), "anomalies");
9071
- mkdirSync9(dir2, { recursive: true, mode: 448 });
8952
+ const dir2 = join13(dirname6(this.path), "anomalies");
8953
+ mkdirSync8(dir2, { recursive: true, mode: 448 });
9072
8954
  chmodSync4(dir2, 448);
9073
- const target = join14(dir2, basename(this.path));
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(join14(dir2, f), { force: true });
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 readFileSync8 } from "fs";
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(readFileSync8("/proc/self/cgroup", "utf8"));
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 && !existsSync10(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
- agentPat: this.opts.credential,
10129
- // CT306: the per-turn OBO credential when the API minted one; falls back to
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 (!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})` };
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 = join15(cwd, ".git");
10654
- if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
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() && !existsSync11(join15(gitPath, "HEAD")))
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 existsSync12,
10822
- mkdirSync as mkdirSync10,
10706
+ existsSync as existsSync11,
10707
+ mkdirSync as mkdirSync9,
10823
10708
  readdirSync as readdirSync3,
10824
- readFileSync as readFileSync9,
10709
+ readFileSync as readFileSync8,
10825
10710
  renameSync as renameSync4,
10826
10711
  rmSync as rmSync7,
10827
- writeFileSync as writeFileSync7
10712
+ writeFileSync as writeFileSync6
10828
10713
  } from "fs";
10829
- import { join as join16 } from "path";
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 join16(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
10727
+ return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
10843
10728
  }
10844
10729
  fileFor(turnId, seq) {
10845
- return join16(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
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
- mkdirSync10(dir2, { recursive: true });
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
- writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
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 (!existsSync12(dir2)) return [];
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 = join16(dir2, name);
10772
+ const full = join15(dir2, name);
10888
10773
  try {
10889
- const parsed = JSON.parse(readFileSync9(full, "utf8"));
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 (!existsSync12(dir2)) return 0;
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 (!existsSync12(dir2)) return 0;
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 = this.opts.path ? `${base}${this.opts.path}` : `${base}/api/workspaces/${this.opts.workspaceId}/events`;
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: () => 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
+ },
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.ensureWorkspaceSse(wr);
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. Same frames and the same routing as above;
11795
- // what differs is that this stream keeps no cursor (it replays what is still
11796
- // offered on every connect instead of resuming from a `Last-Event-ID`), so
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, "", wire);
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, and the only place that exists because there are two
11834
- // streams. Both carry the same frame with the same dispatch id; the first copy
11835
- // to arrive runs the turn, and the second waits for it and then settles its own
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, cursorId, wire) {
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, cursorId, wire);
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, cursorId, wire) {
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
- const reported = await this.reportUnrunnableDispatch(wr.workspaceId, payload);
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, { dispatchId: wire.id, cursorId }, payload, agent)).catch((err) => {
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, because that answer decides whether the
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
- // neither a dispatch id to dedupe on nor a cursor to settle. The
12025
- // `recoveredTurns` set above is this path's own guard.
12026
- { 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
+ "",
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
- // cursor + console summary. Mirrors the old per-workspace `handleEvent`, now
11881
+ // the console summary. Mirrors the old per-workspace `handleEvent`, now
12049
11882
  // scoped to one agent.
12050
- async runDispatch(wr, ids, payload, agent, recoveredTurnId) {
11883
+ async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
12051
11884
  const workspaceId = wr.workspaceId;
12052
- const settleCursor = () => {
12053
- if (ids.cursorId) wr.cursor.settle(ids.cursorId);
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: ids.dispatchId },
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: ids.dispatchId },
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, ids.dispatchId);
12070
- settleCursor();
11898
+ markCompleted(workspaceId, dispatchId);
12071
11899
  return;
12072
11900
  }
12073
- const attempt = bumpResumeAttempt(workspaceId, ids.dispatchId);
11901
+ const attempt = bumpResumeAttempt(workspaceId, dispatchId);
12074
11902
  if (attempt > MAX_RESUME_ATTEMPTS) {
12075
11903
  this.log.error(
12076
- { workspaceId, eventId: ids.dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
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, ids.dispatchId);
12080
- settleCursor();
11907
+ markCompleted(workspaceId, dispatchId);
12081
11908
  return;
12082
11909
  }
12083
11910
  this.log.debug(
12084
- { workspaceId, eventId: ids.dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
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 ?? (ids.dispatchId ? turnIdForEvent(workspaceId, ids.dispatchId) : null);
11915
+ const resumedTurnId = recoveredTurnId ?? (dispatchId ? turnIdForEvent(workspaceId, dispatchId) : null);
12089
11916
  const turnId = resumedTurnId ?? randomUUID4();
12090
- if (ids.dispatchId) {
12091
- const liveTurnIds = rememberTurnId(workspaceId, ids.dispatchId, turnId);
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, ids.dispatchId);
11925
+ markDispatched(workspaceId, dispatchId);
12099
11926
  }
12100
11927
  if (resumedTurnId) {
12101
11928
  this.log.debug(
12102
- { workspaceId, eventId: ids.dispatchId, turnId },
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 (ids.dispatchId) markCompleted(workspaceId, ids.dispatchId);
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 existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync10, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
12470
- 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";
12471
12295
  function crashMarkerPath() {
12472
- return join17(cabaneDir(), "last-error.json");
12296
+ return join16(cabaneDir(), "last-error.json");
12473
12297
  }
12474
12298
  function recordCrash(rec2) {
12475
12299
  try {
12476
- mkdirSync11(cabaneDir(), { recursive: true });
12477
- writeFileSync8(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
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 (existsSync13(path)) rmSync8(path, { force: true });
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 mkdirSync12, openSync as openSync3 } from "fs";
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 existsSync14 } from "fs";
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 ?? existsSync14;
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
- mkdirSync12(cabaneDir(), { recursive: true });
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 existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync5 } from "fs";
13193
- 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";
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(join18(dir2, newest)) + "\n");
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) => readFileSync11(join18(dir2, f), "utf8"),
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(join18(dir2, f));
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 readFileSync11(path, "utf8").split("\n")) {
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 (existsSync15(target)) return target;
13191
+ if (existsSync14(target)) return target;
13368
13192
  throw new CompanionError(`no transcript at ${target}.`);
13369
13193
  }
13370
- const exact = join18(dir2, target);
13371
- if (existsSync15(exact)) return exact;
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 join18(dir2, matches[0]);
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 = readFileSync11(path, "utf8");
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(readFileSync12(0, "utf8"));
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."