@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/runtime.js CHANGED
@@ -2281,6 +2281,7 @@ var bootId = randomUUID();
2281
2281
  import { randomUUID as randomUUID4 } from "crypto";
2282
2282
 
2283
2283
  // src/api.ts
2284
+ var DEVICE_HEADER = "x-cabane-device";
2284
2285
  var RETRY_BACKOFF_MS = [250, 750];
2285
2286
  var TURN_CONTEXT_BACKOFF_MS = [250, 750, 2e3, 5e3];
2286
2287
  var ACTIVE_RUN_OUTBOX_SEQ = 0;
@@ -2310,6 +2311,7 @@ var CabaneApi = class {
2310
2311
  method,
2311
2312
  headers: {
2312
2313
  Authorization: `Bearer ${this.opts.token}`,
2314
+ ...this.opts.deviceToken ? { [DEVICE_HEADER]: this.opts.deviceToken } : {},
2313
2315
  ...body !== void 0 ? { "content-type": "application/json" } : {},
2314
2316
  accept: "application/json"
2315
2317
  },
@@ -2444,27 +2446,6 @@ var CabaneApi = class {
2444
2446
  outboxSize() {
2445
2447
  return this.opts.outbox?.size() ?? 0;
2446
2448
  }
2447
- getConversation(workspaceId, conversationId) {
2448
- return this.request("GET", `/api/workspaces/${workspaceId}/conversations/${conversationId}`);
2449
- }
2450
- // SJ383: companion-only participant ops. All three authenticate with the
2451
- // workspace's agent-bound PAT (passed as `token` on this client) — never
2452
- // the user PAT.
2453
- // Recovery-path read: returns the companion's `agentSessionId` for this
2454
- // conversation so the dispatcher can pass it as `Options.resume`, plus
2455
- // the current `activeRunStartedAt` so a freshly-reconnected companion can
2456
- // see whether a prior run is still flagged in-flight.
2457
- // SJ383: recovery-path read for a participant row — the `agentSessionId` a
2458
- // freshly-reconnected companion resumes on. CT262: the per-turn piggybacks this
2459
- // fetch grew (the ABOUT charters / visionBlocks / channel / members / conversationContext)
2460
- // are gone — `getTurnContext` composes the whole turn now — so this is back to
2461
- // the plain recovery read, with no `messageId` param.
2462
- getParticipantAgent(workspaceId, conversationId, agentId) {
2463
- return this.request(
2464
- "GET",
2465
- `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}`
2466
- );
2467
- }
2468
2449
  // CT262: the ONE turn-context fetch. Collapses the companion's old four-fetch
2469
2450
  // choreography (getConversation + getMessage + getAgentSelf + getParticipantAgent)
2470
2451
  // into a single call: the server composes the whole server portion of the
@@ -2744,34 +2725,6 @@ var CabaneApi = class {
2744
2725
  signal
2745
2726
  );
2746
2727
  }
2747
- // SJ493: fetch the agent's self-view — identity + operating context. The
2748
- // companion calls this on each dispatch to get its `systemPrompt` (composed
2749
- // server-side from the bundled default + the agent's charter) rather than
2750
- // baking a copy of the prompt into the download. Cabane is the control plane
2751
- // for the prompt, so changing it (or the per-agent charter) takes effect
2752
- // without shipping a new companion. Agent-PAT authed; the workspace is implied
2753
- // by the PAT, so no workspace arg.
2754
- // CT245: pass the triggering turn's `conversationId` so the server returns the
2755
- // run-config RESOLVED for this conversation (agent default + that
2756
- // conversation-agent's per-conversation override), computed with the same
2757
- // shared resolver the in-app path uses. Omitting it (older call sites) returns
2758
- // the agent default — graceful degradation, no version coupling.
2759
- getAgentSelf(conversationId) {
2760
- const path = conversationId ? `/api/agent/me?conversationId=${encodeURIComponent(conversationId)}` : `/api/agent/me`;
2761
- return this.request("GET", path);
2762
- }
2763
- // The companion fetches the triggering message body by listing the
2764
- // conversation's messages and finding the one with `id === messageId`.
2765
- // Cabane has no single-message GET endpoint; for v0 this is fine because
2766
- // the companion only reaches for the specific row immediately after the
2767
- // event fires (the thread is small at that point).
2768
- async getMessage(workspaceId, conversationId, messageId2) {
2769
- const res = await this.request(
2770
- "GET",
2771
- `/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`
2772
- );
2773
- return res.messages.find((m) => m.id === messageId2) ?? null;
2774
- }
2775
2728
  };
2776
2729
  function isRetryable(err) {
2777
2730
  if (err instanceof ApiError) return err.status >= 500;
@@ -2965,78 +2918,21 @@ function pruneCredentials(keepAgentIds) {
2965
2918
  return removed;
2966
2919
  }
2967
2920
 
2968
- // src/cursor.ts
2969
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
2970
- import { join as join9 } from "path";
2971
- function pathFor(workspaceId) {
2972
- return join9(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
2973
- }
2974
- function readCursor(workspaceId) {
2975
- const path = pathFor(workspaceId);
2976
- if (!existsSync6(path)) return null;
2977
- const raw = readFileSync4(path, "utf8").trim();
2978
- return raw.length > 0 ? raw : null;
2979
- }
2980
- function writeCursor(workspaceId, eventId) {
2981
- const path = pathFor(workspaceId);
2982
- mkdirSync6(join9(cabaneDir(), "cursors"), { recursive: true });
2983
- writeFileSync4(path, eventId + "\n", "utf8");
2984
- }
2985
-
2986
- // src/cursor-tracker.ts
2987
- var CursorTracker = class {
2988
- constructor(workspaceId) {
2989
- this.workspaceId = workspaceId;
2990
- }
2991
- workspaceId;
2992
- // Event ids in arrival order that haven't yet flushed to the persisted
2993
- // cursor. Holds the in-flight (un-settled) dispatch events plus any later
2994
- // events that have settled but are blocked behind an earlier in-flight one.
2995
- pending = [];
2996
- // Ids observed as settled. An entry is dropped the moment it flushes, so the
2997
- // set only ever holds settled-but-blocked ids — bounded by concurrent
2998
- // in-flight turns and the traffic that arrived during them.
2999
- settled = /* @__PURE__ */ new Set();
3000
- // Record an event id in arrival order. Must be called synchronously, in
3001
- // stream order, for every event the supervisor will later `settle` — so the
3002
- // cumulative prefix reflects the true stream sequence. Idempotent: an id
3003
- // already tracked (an in-flight event, or a settled-but-blocked one) is
3004
- // ignored, so a process-restart replay can re-observe the same ids without
3005
- // double-counting.
3006
- observe(eventId) {
3007
- if (this.pending.includes(eventId) || this.settled.has(eventId)) return;
3008
- this.pending.push(eventId);
3009
- }
3010
- // Mark an event settled — for a dispatch turn, only once it has completed;
3011
- // for everything else, immediately after it's handled. Flushes the persisted
3012
- // cursor forward through the now-longer contiguous settled prefix.
3013
- settle(eventId) {
3014
- this.settled.add(eventId);
3015
- let advanced = null;
3016
- while (this.pending.length > 0 && this.settled.has(this.pending[0])) {
3017
- const id = this.pending.shift();
3018
- this.settled.delete(id);
3019
- advanced = id;
3020
- }
3021
- if (advanced) writeCursor(this.workspaceId, advanced);
3022
- }
3023
- };
3024
-
3025
2921
  // src/dispatch-dedupe.ts
3026
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync7 } from "fs";
3027
- import { join as join10 } from "path";
2922
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6 } from "fs";
2923
+ import { join as join9 } from "path";
3028
2924
  var MAX_IDS = 256;
3029
2925
  function dir(log) {
3030
- return join10(cabaneDir(), log);
2926
+ return join9(cabaneDir(), log);
3031
2927
  }
3032
- function pathFor2(log, workspaceId) {
3033
- return join10(dir(log), encodeURIComponent(workspaceId));
2928
+ function pathFor(log, workspaceId) {
2929
+ return join9(dir(log), encodeURIComponent(workspaceId));
3034
2930
  }
3035
2931
  function readIds(log, workspaceId) {
3036
- const path = pathFor2(log, workspaceId);
3037
- if (!existsSync7(path)) return [];
2932
+ const path = pathFor(log, workspaceId);
2933
+ if (!existsSync6(path)) return [];
3038
2934
  try {
3039
- return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
2935
+ return readFileSync4(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
3040
2936
  } catch {
3041
2937
  return [];
3042
2938
  }
@@ -3049,8 +2945,8 @@ function mark(log, workspaceId, eventId) {
3049
2945
  if (ids.includes(eventId)) return;
3050
2946
  ids.push(eventId);
3051
2947
  const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
3052
- mkdirSync7(dir(log), { recursive: true });
3053
- writeFileSync5(pathFor2(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
2948
+ mkdirSync6(dir(log), { recursive: true });
2949
+ writeFileSync4(pathFor(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
3054
2950
  }
3055
2951
  function hasDispatched(workspaceId, eventId) {
3056
2952
  return has("dispatched", workspaceId, eventId);
@@ -3067,17 +2963,17 @@ function markCompleted(workspaceId, eventId) {
3067
2963
  }
3068
2964
  var MAX_RESUME_ATTEMPTS = 3;
3069
2965
  function resumeDir() {
3070
- return join10(cabaneDir(), "resume-attempts");
2966
+ return join9(cabaneDir(), "resume-attempts");
3071
2967
  }
3072
2968
  function resumePathFor(workspaceId) {
3073
- return join10(resumeDir(), encodeURIComponent(workspaceId));
2969
+ return join9(resumeDir(), encodeURIComponent(workspaceId));
3074
2970
  }
3075
2971
  function readResumeCounts(workspaceId) {
3076
2972
  const out = /* @__PURE__ */ new Map();
3077
2973
  const path = resumePathFor(workspaceId);
3078
- if (!existsSync7(path)) return out;
2974
+ if (!existsSync6(path)) return out;
3079
2975
  try {
3080
- for (const line of readFileSync5(path, "utf8").split("\n")) {
2976
+ for (const line of readFileSync4(path, "utf8").split("\n")) {
3081
2977
  const trimmed = line.trim();
3082
2978
  if (!trimmed) continue;
3083
2979
  const tab = trimmed.lastIndexOf(" ");
@@ -3097,8 +2993,8 @@ function bumpResumeAttempt(workspaceId, eventId) {
3097
2993
  counts.set(eventId, next);
3098
2994
  const entries = [...counts.entries()];
3099
2995
  const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
3100
- mkdirSync7(resumeDir(), { recursive: true });
3101
- writeFileSync5(
2996
+ mkdirSync6(resumeDir(), { recursive: true });
2997
+ writeFileSync4(
3102
2998
  resumePathFor(workspaceId),
3103
2999
  trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
3104
3000
  "utf8"
@@ -3106,18 +3002,18 @@ function bumpResumeAttempt(workspaceId, eventId) {
3106
3002
  return next;
3107
3003
  }
3108
3004
  function turnDir() {
3109
- return join10(cabaneDir(), "turns");
3005
+ return join9(cabaneDir(), "turns");
3110
3006
  }
3111
3007
  function turnPathFor(workspaceId) {
3112
- return join10(turnDir(), encodeURIComponent(workspaceId));
3008
+ return join9(turnDir(), encodeURIComponent(workspaceId));
3113
3009
  }
3114
3010
  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;
3115
3011
  function readTurnIds(workspaceId) {
3116
3012
  const out = /* @__PURE__ */ new Map();
3117
3013
  const path = turnPathFor(workspaceId);
3118
- if (!existsSync7(path)) return out;
3014
+ if (!existsSync6(path)) return out;
3119
3015
  try {
3120
- for (const line of readFileSync5(path, "utf8").split("\n")) {
3016
+ for (const line of readFileSync4(path, "utf8").split("\n")) {
3121
3017
  const trimmed = line.trim();
3122
3018
  if (!trimmed) continue;
3123
3019
  const tab = trimmed.lastIndexOf(" ");
@@ -3137,10 +3033,10 @@ function turnIdForEvent(workspaceId, eventId) {
3137
3033
  var TURN_ID_OVERFLOW_WARN = 8192;
3138
3034
  function writeTurnIds(workspaceId, turns) {
3139
3035
  const entries = [...turns.entries()];
3140
- mkdirSync7(turnDir(), { recursive: true });
3036
+ mkdirSync6(turnDir(), { recursive: true });
3141
3037
  const path = turnPathFor(workspaceId);
3142
3038
  const tmp = `${path}.${process.pid}.tmp`;
3143
- writeFileSync5(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
3039
+ writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
3144
3040
  renameSync3(tmp, path);
3145
3041
  return entries.length;
3146
3042
  }
@@ -7915,8 +7811,8 @@ var ConnectorHealthStore = class {
7915
7811
  };
7916
7812
 
7917
7813
  // src/dispatcher.ts
7918
- import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
7919
- import { join as join15 } from "path";
7814
+ import { existsSync as existsSync10, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
7815
+ import { join as join14 } from "path";
7920
7816
 
7921
7817
  // src/term.ts
7922
7818
  import { createInterface } from "readline";
@@ -7930,7 +7826,7 @@ function tildePath(path) {
7930
7826
 
7931
7827
  // src/turn-execution.ts
7932
7828
  import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
7933
- import { existsSync as existsSync10 } from "fs";
7829
+ import { existsSync as existsSync9 } from "fs";
7934
7830
 
7935
7831
  // src/turn-control-tools.ts
7936
7832
  import { randomUUID as randomUUID2 } from "crypto";
@@ -8166,24 +8062,10 @@ function buildCompanionTurnRequest(params) {
8166
8062
  session: t.session,
8167
8063
  cabane: {
8168
8064
  mcpUrl: cabaneMcpUrl(params.baseUrl),
8169
- // CT306: prefer the per-turn OBO credential; fall back to the companion PAT
8170
- // when the API didn't mint one (older API / unresolvable delegation).
8171
- bearer: params.turnToken ?? params.agentPat,
8065
+ bearer: params.turnToken,
8172
8066
  activeConversationId: params.activeConversationId,
8173
8067
  workspaceId: params.workspaceId,
8174
- // CT714: mount the turn-control surface ONLY when a real turn token backs
8175
- // this turn — the surface admits `turn_token` auth exclusively, so a
8176
- // PAT-fallback bearer would be rejected there. Absent it, external adapters
8177
- // simply don't mount it that turn.
8178
- //
8179
- // CT1074: that fallback is now an OLD-API case only. The server mints a token
8180
- // for every turn it dispatched, so "no token" no longer means "no human behind
8181
- // the turn" — it means the API predates this change. The condition is right as
8182
- // it stands and stays where the failure is honest (a rejected bearer is worse
8183
- // than an unmounted server); what was wrong was upstream, where an undelegated
8184
- // turn minted nothing and this silently dropped all five verbs on Codex and
8185
- // opencode with no refusal anyone could see.
8186
- ...params.turnToken ? { turnControlUrl: turnControlMcpUrl(params.baseUrl) } : {}
8068
+ turnControlUrl: turnControlMcpUrl(params.baseUrl)
8187
8069
  },
8188
8070
  local: {
8189
8071
  ...params.cwd ? { cwd: params.cwd } : {},
@@ -8207,11 +8089,11 @@ function trimSlash2(s) {
8207
8089
  // src/codex-instructions.ts
8208
8090
  import { mkdtemp, rm, writeFile } from "fs/promises";
8209
8091
  import { tmpdir } from "os";
8210
- import { join as join11 } from "path";
8092
+ import { join as join10 } from "path";
8211
8093
  var PREFIX = "cabane-codex-instructions-";
8212
8094
  async function writeCodexInstructionsFile(contents) {
8213
- const dir2 = await mkdtemp(join11(tmpdir(), PREFIX));
8214
- const path = join11(dir2, "instructions.md");
8095
+ const dir2 = await mkdtemp(join10(tmpdir(), PREFIX));
8096
+ const path = join10(dir2, "instructions.md");
8215
8097
  await writeFile(path, contents, { encoding: "utf8", mode: 384 });
8216
8098
  return {
8217
8099
  path,
@@ -8317,22 +8199,22 @@ function reportFailure(config, body) {
8317
8199
  }
8318
8200
 
8319
8201
  // src/prepared.ts
8320
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
8321
- import { join as join12 } from "path";
8202
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
8203
+ import { join as join11 } from "path";
8322
8204
  function dirFor(workspaceId) {
8323
- return join12(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8205
+ return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8324
8206
  }
8325
8207
  function conversationDir(workspaceId, conversationId) {
8326
- return join12(dirFor(workspaceId), encodeURIComponent(conversationId));
8208
+ return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
8327
8209
  }
8328
- function pathFor3(workspaceId, conversationId, agentId) {
8329
- return join12(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8210
+ function pathFor2(workspaceId, conversationId, agentId) {
8211
+ return join11(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8330
8212
  }
8331
8213
  function readPrepared(workspaceId, conversationId, agentId) {
8332
- const path = pathFor3(workspaceId, conversationId, agentId);
8333
- if (!existsSync8(path)) return null;
8214
+ const path = pathFor2(workspaceId, conversationId, agentId);
8215
+ if (!existsSync7(path)) return null;
8334
8216
  try {
8335
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
8217
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
8336
8218
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
8337
8219
  return {
8338
8220
  cwd: parsed.cwd,
@@ -8345,32 +8227,32 @@ function readPrepared(workspaceId, conversationId, agentId) {
8345
8227
  }
8346
8228
  }
8347
8229
  function writePrepared(workspaceId, conversationId, agentId, result) {
8348
- mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
8349
- writeFileSync6(
8350
- pathFor3(workspaceId, conversationId, agentId),
8230
+ mkdirSync7(conversationDir(workspaceId, conversationId), { recursive: true });
8231
+ writeFileSync5(
8232
+ pathFor2(workspaceId, conversationId, agentId),
8351
8233
  JSON.stringify(result) + "\n",
8352
8234
  "utf8"
8353
8235
  );
8354
8236
  }
8355
8237
  function clearPrepared(workspaceId, conversationId, agentId) {
8356
- rmSync5(pathFor3(workspaceId, conversationId, agentId), { force: true });
8238
+ rmSync5(pathFor2(workspaceId, conversationId, agentId), { force: true });
8357
8239
  }
8358
8240
 
8359
8241
  // src/secrets.ts
8360
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
8361
- import { join as join13 } from "path";
8242
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
8243
+ import { join as join12 } from "path";
8362
8244
  import { z as z14 } from "zod";
8363
8245
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
8364
8246
  function secretsPath() {
8365
- return join13(cabaneDir(), "secrets.json");
8247
+ return join12(cabaneDir(), "secrets.json");
8366
8248
  }
8367
8249
  var secretStoreSchema = z14.record(z14.string(), z14.string());
8368
8250
  function loadSecretStore() {
8369
8251
  const path = secretsPath();
8370
- if (!existsSync9(path)) return makeStore({});
8252
+ if (!existsSync8(path)) return makeStore({});
8371
8253
  let raw;
8372
8254
  try {
8373
- raw = readFileSync7(path, "utf8");
8255
+ raw = readFileSync6(path, "utf8");
8374
8256
  } catch (err) {
8375
8257
  throw new ConfigError(
8376
8258
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -8448,10 +8330,10 @@ function resolveMcpSecrets(mcpServers, store) {
8448
8330
  }
8449
8331
 
8450
8332
  // src/transcript-writer.ts
8451
- import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
8452
- import { basename, dirname as dirname6, join as join14 } from "path";
8333
+ import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
8334
+ import { basename, dirname as dirname6, join as join13 } from "path";
8453
8335
  function transcriptsDir() {
8454
- return join14(cabaneDir(), "transcripts");
8336
+ return join13(cabaneDir(), "transcripts");
8455
8337
  }
8456
8338
  var RETAIN = 200;
8457
8339
  var ANOMALY_RETAIN = 50;
@@ -8461,9 +8343,9 @@ var TranscriptWriter = class {
8461
8343
  onWarn;
8462
8344
  constructor(dir2, meta, onWarn) {
8463
8345
  this.onWarn = onWarn;
8464
- this.path = join14(dir2, fileName(meta));
8346
+ this.path = join13(dir2, fileName(meta));
8465
8347
  try {
8466
- mkdirSync9(dir2, { recursive: true });
8348
+ mkdirSync8(dir2, { recursive: true });
8467
8349
  try {
8468
8350
  chmodSync4(dir2, 448);
8469
8351
  } catch {
@@ -8490,10 +8372,10 @@ var TranscriptWriter = class {
8490
8372
  preserveAnomaly() {
8491
8373
  if (this.broken) return;
8492
8374
  try {
8493
- const dir2 = join14(dirname6(this.path), "anomalies");
8494
- mkdirSync9(dir2, { recursive: true, mode: 448 });
8375
+ const dir2 = join13(dirname6(this.path), "anomalies");
8376
+ mkdirSync8(dir2, { recursive: true, mode: 448 });
8495
8377
  chmodSync4(dir2, 448);
8496
- const target = join14(dir2, basename(this.path));
8378
+ const target = join13(dir2, basename(this.path));
8497
8379
  copyFileSync(this.path, target);
8498
8380
  chmodSync4(target, 384);
8499
8381
  pruneOld(dir2, ANOMALY_RETAIN);
@@ -8539,7 +8421,7 @@ function pruneOld(dir2, retain) {
8539
8421
  const drop = files.sort().slice(0, files.length - retain);
8540
8422
  for (const f of drop) {
8541
8423
  try {
8542
- rmSync6(join14(dir2, f), { force: true });
8424
+ rmSync6(join13(dir2, f), { force: true });
8543
8425
  } catch {
8544
8426
  }
8545
8427
  }
@@ -8547,7 +8429,7 @@ function pruneOld(dir2, retain) {
8547
8429
 
8548
8430
  // src/turn-containment.ts
8549
8431
  import { execFile, execFileSync, spawn as spawn5 } from "child_process";
8550
- import { readFileSync as readFileSync8 } from "fs";
8432
+ import { readFileSync as readFileSync7 } from "fs";
8551
8433
  import { promisify } from "util";
8552
8434
  var execFileAsync = promisify(execFile);
8553
8435
  var asContained = (child) => child;
@@ -8592,7 +8474,7 @@ function currentSystemdUnit(cgroupText) {
8592
8474
  }
8593
8475
  function readCurrentSystemdUnit() {
8594
8476
  try {
8595
- return currentSystemdUnit(readFileSync8("/proc/self/cgroup", "utf8"));
8477
+ return currentSystemdUnit(readFileSync7("/proc/self/cgroup", "utf8"));
8596
8478
  } catch {
8597
8479
  return null;
8598
8480
  }
@@ -9018,6 +8900,7 @@ function missingSecretReason(missing) {
9018
8900
  return `${prefix}+${missing.length}`;
9019
8901
  }
9020
8902
  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:";
8903
+ var NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
9021
8904
  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`;
9022
8905
  var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
9023
8906
  var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 15 * 6e4;
@@ -9297,6 +9180,10 @@ var TurnExecution = class {
9297
9180
  const fetchReason = `fetch_failed: ${describeError(err)}`;
9298
9181
  throw this.concluded(fetchReason, fetchReason);
9299
9182
  }
9183
+ if (!turnContext.turnToken) {
9184
+ turnLog.warn({ turnId }, "dispatcher: turn context carried no turn token");
9185
+ throw this.concluded(NO_TURN_TOKEN_REASON, NO_TURN_TOKEN_REASON);
9186
+ }
9300
9187
  this.turnContext = turnContext;
9301
9188
  if (this.resumed) {
9302
9189
  const sources = { serverFloor: turnContext.turnSeqFloor, outboxFloor };
@@ -9344,7 +9231,7 @@ var TurnExecution = class {
9344
9231
  const prepareHook = this.opts.local.prepareHook;
9345
9232
  const cabaneCwd = this.turnContext.cwd;
9346
9233
  let effectiveCwd = localCwd ?? cabaneCwd;
9347
- if (effectiveCwd && !existsSync10(effectiveCwd)) {
9234
+ if (effectiveCwd && !existsSync9(effectiveCwd)) {
9348
9235
  turnLog.warn(
9349
9236
  { cwd: effectiveCwd },
9350
9237
  "The working directory is missing; using the companion's directory instead. Check this agent's working directory setting."
@@ -9548,10 +9435,8 @@ var TurnExecution = class {
9548
9435
  this.request = buildCompanionTurnRequest({
9549
9436
  turnContext: this.turnContext,
9550
9437
  baseUrl: this.opts.baseUrl,
9551
- agentPat: this.opts.credential,
9552
- // CT306: the per-turn OBO credential when the API minted one; falls back to
9553
- // the companion PAT (`agentPat`) inside `buildCompanionTurnRequest` otherwise.
9554
- ...this.turnContext.turnToken ? { turnToken: this.turnContext.turnToken } : {},
9438
+ // ACC8: always present; `fetchTurnContext` refused a context without one.
9439
+ turnToken: this.turnContext.turnToken,
9555
9440
  // SJ524: the hook-resolved cwd overrides the static local cwd.
9556
9441
  ...this.effectiveCwd ? { cwd: this.effectiveCwd } : {},
9557
9442
  // CT1103: the prepare-hook env plus the authoritative host-access token.
@@ -10063,7 +9948,7 @@ function fingerprintSessionState(state) {
10063
9948
  // src/dispatcher.ts
10064
9949
  function checkoutState(cwd) {
10065
9950
  if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
10066
- if (!existsSync11(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
9951
+ if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
10067
9952
  let entries;
10068
9953
  try {
10069
9954
  entries = readdirSync2(cwd);
@@ -10073,15 +9958,15 @@ function checkoutState(cwd) {
10073
9958
  if (entries.length === 0) {
10074
9959
  return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
10075
9960
  }
10076
- const gitPath = join15(cwd, ".git");
10077
- if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
9961
+ const gitPath = join14(cwd, ".git");
9962
+ if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
10078
9963
  let stat;
10079
9964
  try {
10080
9965
  stat = statSync2(gitPath);
10081
9966
  } catch (error) {
10082
9967
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
10083
9968
  }
10084
- if (stat.isDirectory() && !existsSync11(join15(gitPath, "HEAD")))
9969
+ if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
10085
9970
  return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
10086
9971
  return { ok: true, reason: "usable" };
10087
9972
  }
@@ -10241,15 +10126,15 @@ var DispatchClaims = class {
10241
10126
 
10242
10127
  // src/outbox.ts
10243
10128
  import {
10244
- existsSync as existsSync12,
10245
- mkdirSync as mkdirSync10,
10129
+ existsSync as existsSync11,
10130
+ mkdirSync as mkdirSync9,
10246
10131
  readdirSync as readdirSync3,
10247
- readFileSync as readFileSync9,
10132
+ readFileSync as readFileSync8,
10248
10133
  renameSync as renameSync4,
10249
10134
  rmSync as rmSync7,
10250
- writeFileSync as writeFileSync7
10135
+ writeFileSync as writeFileSync6
10251
10136
  } from "fs";
10252
- import { join as join16 } from "path";
10137
+ import { join as join15 } from "path";
10253
10138
  var MAX_ENTRIES = 2e3;
10254
10139
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
10255
10140
  var Outbox = class {
@@ -10262,21 +10147,21 @@ var Outbox = class {
10262
10147
  // Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
10263
10148
  // cases route writes at the right tmpdir.
10264
10149
  dir() {
10265
- return join16(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
10150
+ return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
10266
10151
  }
10267
10152
  fileFor(turnId, seq) {
10268
- return join16(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
10153
+ return join15(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
10269
10154
  }
10270
10155
  // Persist a commit for later draining. Atomic (temp file + rename) so a
10271
10156
  // concurrent `list()` never reads a half-written entry, then enforces the
10272
10157
  // per-workspace bounds.
10273
10158
  persist(entry) {
10274
10159
  const dir2 = this.dir();
10275
- mkdirSync10(dir2, { recursive: true });
10160
+ mkdirSync9(dir2, { recursive: true });
10276
10161
  const target = this.fileFor(entry.turnId, entry.seq);
10277
10162
  const tmp = `${target}.${process.pid}.tmp`;
10278
10163
  try {
10279
- writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
10164
+ writeFileSync6(tmp, JSON.stringify(entry) + "\n", "utf8");
10280
10165
  renameSync4(tmp, target);
10281
10166
  } catch (err) {
10282
10167
  try {
@@ -10297,7 +10182,7 @@ var Outbox = class {
10297
10182
  // wedging the drain.
10298
10183
  list() {
10299
10184
  const dir2 = this.dir();
10300
- if (!existsSync12(dir2)) return [];
10185
+ if (!existsSync11(dir2)) return [];
10301
10186
  let names;
10302
10187
  try {
10303
10188
  names = readdirSync3(dir2);
@@ -10307,9 +10192,9 @@ var Outbox = class {
10307
10192
  const entries = [];
10308
10193
  for (const name of names) {
10309
10194
  if (!name.endsWith(".json")) continue;
10310
- const full = join16(dir2, name);
10195
+ const full = join15(dir2, name);
10311
10196
  try {
10312
- const parsed = JSON.parse(readFileSync9(full, "utf8"));
10197
+ const parsed = JSON.parse(readFileSync8(full, "utf8"));
10313
10198
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
10314
10199
  entries.push(parsed);
10315
10200
  } else {
@@ -10343,7 +10228,7 @@ var Outbox = class {
10343
10228
  // different: it is positive evidence that nothing was ever queued here.
10344
10229
  maxSeqForTurn(turnId) {
10345
10230
  const dir2 = this.dir();
10346
- if (!existsSync12(dir2)) return 0;
10231
+ if (!existsSync11(dir2)) return 0;
10347
10232
  const names = readdirSync3(dir2);
10348
10233
  const prefix = `${encodeURIComponent(turnId)}__`;
10349
10234
  let max = 0;
@@ -10363,7 +10248,7 @@ var Outbox = class {
10363
10248
  }
10364
10249
  size() {
10365
10250
  const dir2 = this.dir();
10366
- if (!existsSync12(dir2)) return 0;
10251
+ if (!existsSync11(dir2)) return 0;
10367
10252
  try {
10368
10253
  return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
10369
10254
  } catch {
@@ -10517,7 +10402,7 @@ var SseSubscriber = class {
10517
10402
  }
10518
10403
  async connect() {
10519
10404
  const base = this.opts.baseUrl.endsWith("/") ? this.opts.baseUrl.slice(0, -1) : this.opts.baseUrl;
10520
- const url = this.opts.path ? `${base}${this.opts.path}` : `${base}/api/workspaces/${this.opts.workspaceId}/events`;
10405
+ const url = `${base}${this.opts.path}`;
10521
10406
  this.controller = new AbortController();
10522
10407
  const headers = {
10523
10408
  Authorization: `Bearer ${this.opts.token}`,
@@ -10624,6 +10509,9 @@ var CompanionSupervisor = class {
10624
10509
  unrunnableRetryDelaysMs;
10625
10510
  deviceApi = null;
10626
10511
  deviceSub = null;
10512
+ // ACC8: whether the device stream is open, which is what every workspace's
10513
+ // link on the dashboard reports.
10514
+ deviceConnected = false;
10627
10515
  recoveryChecked = false;
10628
10516
  recovering = false;
10629
10517
  heartbeated = false;
@@ -10690,7 +10578,18 @@ var CompanionSupervisor = class {
10690
10578
  token: this.config.deviceToken,
10691
10579
  log: this.log,
10692
10580
  lastEventId: null,
10693
- onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`),
10581
+ onOpen: () => {
10582
+ this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`);
10583
+ this.deviceConnected = true;
10584
+ for (const wr of this.workspaces.values()) {
10585
+ this.hub.setConnected(wr.workspaceId, true);
10586
+ for (const a of wr.agents.values()) a.kickDrain();
10587
+ }
10588
+ },
10589
+ onClose: () => {
10590
+ this.deviceConnected = false;
10591
+ for (const wr of this.workspaces.values()) this.hub.setConnected(wr.workspaceId, false);
10592
+ },
10694
10593
  onMessage: async (ev) => {
10695
10594
  if (ev.event === "assignments_changed") {
10696
10595
  if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
@@ -10722,10 +10621,7 @@ var CompanionSupervisor = class {
10722
10621
  // at least the loops keeping the event loop alive; the SSE promises let a
10723
10622
  // clean teardown resolve.
10724
10623
  finishedPromises() {
10725
- return [
10726
- ...this.deviceSub ? [this.deviceSub.finished] : [],
10727
- ...[...this.workspaces.values()].flatMap((w) => w.sub ? [w.sub.finished] : [])
10728
- ];
10624
+ return [...this.deviceSub ? [this.deviceSub.finished] : []];
10729
10625
  }
10730
10626
  // ---- device-level loops ----
10731
10627
  kickHeartbeat() {
@@ -10873,11 +10769,8 @@ var CompanionSupervisor = class {
10873
10769
  workspaceId,
10874
10770
  slug: first.workspaceSlug,
10875
10771
  name: first.workspaceName,
10876
- sub: null,
10877
- sseAgentId: null,
10878
10772
  agents: /* @__PURE__ */ new Map(),
10879
- chains: /* @__PURE__ */ new Map(),
10880
- cursor: new CursorTracker(workspaceId)
10773
+ chains: /* @__PURE__ */ new Map()
10881
10774
  };
10882
10775
  this.workspaces.set(workspaceId, wr);
10883
10776
  this.hub.registerWorkspace({
@@ -10885,6 +10778,7 @@ var CompanionSupervisor = class {
10885
10778
  slug: first.workspaceSlug,
10886
10779
  name: first.workspaceName
10887
10780
  });
10781
+ this.hub.setConnected(workspaceId, this.deviceConnected);
10888
10782
  }
10889
10783
  const store = loadSecretStoreTolerant();
10890
10784
  const exposed = new Set(store.names());
@@ -10944,7 +10838,7 @@ var CompanionSupervisor = class {
10944
10838
  );
10945
10839
  }
10946
10840
  }
10947
- this.ensureWorkspaceSse(wr);
10841
+ if (wr.agents.size === 0) void this.removeWorkspace(wr.workspaceId);
10948
10842
  }
10949
10843
  addAgent(wr, it, credential, runConfig) {
10950
10844
  const agentLog = this.log.child({ agentName: it.agentDisplayName });
@@ -10952,6 +10846,7 @@ var CompanionSupervisor = class {
10952
10846
  const api = new CabaneApi({
10953
10847
  baseUrl: this.config.baseUrl,
10954
10848
  token: credential,
10849
+ ...this.config.deviceToken ? { deviceToken: this.config.deviceToken } : {},
10955
10850
  outbox,
10956
10851
  log: agentLog
10957
10852
  });
@@ -11153,71 +11048,18 @@ var CompanionSupervisor = class {
11153
11048
  connectorHealth: this.connectorHealth
11154
11049
  });
11155
11050
  }
11156
- // Stand up (or re-point) the workspace's single shared SSE subscription. It is
11157
- // authed with one assigned agent's PAT; if that agent is unassigned, re-pick
11158
- // another. With no agents left the workspace runner is torn down.
11159
- ensureWorkspaceSse(wr) {
11160
- if (wr.agents.size === 0) {
11161
- void this.removeWorkspace(wr.workspaceId);
11162
- return;
11163
- }
11164
- if (wr.sub && wr.sseAgentId && wr.agents.has(wr.sseAgentId)) return;
11165
- const sseAgent = wr.agents.get(wr.sseAgentId ?? "") ?? [...wr.agents.values()][0];
11166
- if (wr.sub) wr.sub.stop();
11167
- wr.sseAgentId = sseAgent.agentId;
11168
- wr.sub = new SseSubscriber({
11169
- baseUrl: this.config.baseUrl,
11170
- workspaceId: wr.workspaceId,
11171
- token: sseAgent.credential,
11172
- log: this.log,
11173
- lastEventId: readCursor(wr.workspaceId),
11174
- onOpen: () => {
11175
- this.hub.setConnected(wr.workspaceId, true);
11176
- for (const a of wr.agents.values()) a.kickDrain();
11177
- },
11178
- onClose: () => this.hub.setConnected(wr.workspaceId, false),
11179
- onMessage: (ev) => this.handleWorkspaceMessage(wr, ev),
11180
- onAuthFailure: (status) => {
11181
- this.hub.setAuthFailed(wr.workspaceId);
11182
- this.log.debug(
11183
- { workspaceId: wr.workspaceId, status, sseAgentId: wr.sseAgentId },
11184
- "companion: workspace stream auth failed \u2014 re-pulling assignments"
11185
- );
11186
- wr.sseAgentId = null;
11187
- void this.refreshAssignments();
11188
- }
11189
- });
11190
- wr.sub.start();
11191
- this.log.debug({ workspaceId: wr.workspaceId }, "companion: subscribed");
11192
- }
11193
11051
  async removeWorkspace(workspaceId) {
11194
11052
  const wr = this.workspaces.get(workspaceId);
11195
11053
  if (!wr) return;
11196
11054
  for (const agentId of [...wr.agents.keys()]) this.removeAgent(wr, agentId);
11197
- if (wr.sub) {
11198
- wr.sub.stop();
11199
- await wr.sub.finished;
11200
- }
11201
11055
  this.workspaces.delete(workspaceId);
11202
11056
  this.hub.removeWorkspace(workspaceId);
11203
11057
  }
11204
11058
  // ---- SSE event routing (per workspace, BY agentId) ----
11205
- // Fire-and-forget per event (the subscriber doesn't await us). Cancel events
11206
- // route to the matching agent immediately; dispatch events serialize per
11207
- // (conversation, agent); everything else just advances the cursor.
11208
- async handleWorkspaceMessage(wr, ev) {
11209
- if (ev.event !== "message") return;
11210
- this.hub.recordEvent(wr.workspaceId);
11211
- const wire = this.parseFrame(ev);
11212
- if (!wire) return;
11213
- if (ev.id) wr.cursor.observe(ev.id);
11214
- await this.deliverFrame(wr, ev.id, wire);
11215
- }
11216
11059
  // ACC10: the device stream's frames — dispatch and cancel, addressed to this
11217
- // device and authenticated as it. Same frames and the same routing as above;
11218
- // what differs is that this stream keeps no cursor (it replays what is still
11219
- // offered on every connect instead of resuming from a `Last-Event-ID`), so
11220
- // nothing here observes or settles one.
11060
+ // device and authenticated as it. ACC8: the only stream this companion opens.
11061
+ // It keeps no cursor: it replays what is still offered on every connect
11062
+ // instead of resuming from a `Last-Event-ID`.
11221
11063
  async handleDeviceMessage(ev) {
11222
11064
  const wire = this.parseFrame(ev);
11223
11065
  if (!wire) return;
@@ -11240,7 +11082,7 @@ var CompanionSupervisor = class {
11240
11082
  }
11241
11083
  }
11242
11084
  this.hub.recordEvent(wr.workspaceId);
11243
- await this.deliverFrame(wr, "", wire);
11085
+ await this.deliverFrame(wr, wire);
11244
11086
  }
11245
11087
  parseFrame(ev) {
11246
11088
  try {
@@ -11253,30 +11095,27 @@ var CompanionSupervisor = class {
11253
11095
  return null;
11254
11096
  }
11255
11097
  }
11256
- // ACC10: the dedupe gate, and the only place that exists because there are two
11257
- // streams. Both carry the same frame with the same dispatch id; the first copy
11258
- // to arrive runs the turn, and the second waits for it and then settles its own
11259
- // stream's cursor. Settling a duplicate earlier than that would advance the
11260
- // low-water mark past an in-flight turn, which is exactly the CT282 bug.
11098
+ // ACC10: the dedupe gate. A reconnect replays every dispatch still offered,
11099
+ // including one whose turn is already running here; the first copy runs the
11100
+ // turn and a later one waits for it and then does nothing.
11261
11101
  //
11262
11102
  // A cancel needs no gate: `dispatcher.cancel` on an already-aborted run is a
11263
11103
  // no-op that reports `false`, so a second copy is inert.
11264
- async deliverFrame(wr, cursorId, wire) {
11104
+ async deliverFrame(wr, wire) {
11265
11105
  const claim = wire.type === "device:dispatch_requested" && wire.id ? this.claims.claim(wire.id) : null;
11266
11106
  if (claim && !claim.first) {
11267
11107
  await claim.done;
11268
- if (cursorId) wr.cursor.settle(cursorId);
11269
11108
  return;
11270
11109
  }
11271
11110
  try {
11272
- await this.routeFrame(wr, cursorId, wire);
11111
+ await this.routeFrame(wr, wire);
11273
11112
  } finally {
11274
11113
  if (claim) this.claims.release(wire.id);
11275
11114
  }
11276
11115
  }
11277
11116
  // Everything below here is the routing as it was: it takes a frame and knows
11278
11117
  // nothing about which stream carried it.
11279
- async routeFrame(wr, cursorId, wire) {
11118
+ async routeFrame(wr, wire) {
11280
11119
  if (wire.type === "device:cancel_requested") {
11281
11120
  const payload2 = {
11282
11121
  type: "device:cancel_requested",
@@ -11296,12 +11135,10 @@ var CompanionSupervisor = class {
11296
11135
  );
11297
11136
  }
11298
11137
  }
11299
- if (cursorId) wr.cursor.settle(cursorId);
11300
11138
  return;
11301
11139
  }
11302
11140
  const payload = dispatchPayload(wire);
11303
11141
  if (!payload) {
11304
- if (cursorId) wr.cursor.settle(cursorId);
11305
11142
  return;
11306
11143
  }
11307
11144
  if (this.deviceId && payload.deviceId && payload.deviceId !== this.deviceId) {
@@ -11314,7 +11151,6 @@ var CompanionSupervisor = class {
11314
11151
  },
11315
11152
  "companion: dispatch addressed to another device; ignoring"
11316
11153
  );
11317
- if (cursorId) wr.cursor.settle(cursorId);
11318
11154
  return;
11319
11155
  }
11320
11156
  let agent = wr.agents.get(payload.agentId);
@@ -11322,16 +11158,14 @@ var CompanionSupervisor = class {
11322
11158
  agent = await this.recoverRacedAgent(wr, payload);
11323
11159
  if (!agent) {
11324
11160
  if (this.deviceId && payload.deviceId === this.deviceId) {
11325
- const reported = await this.reportUnrunnableDispatch(wr.workspaceId, payload);
11326
- if (!reported) return;
11161
+ await this.reportUnrunnableDispatch(wr.workspaceId, payload);
11327
11162
  }
11328
- if (cursorId) wr.cursor.settle(cursorId);
11329
11163
  return;
11330
11164
  }
11331
11165
  }
11332
11166
  const chainKey = `${payload.conversationId}|${payload.agentId}`;
11333
11167
  const prev = wr.chains.get(chainKey) ?? Promise.resolve();
11334
- const tail = prev.then(() => this.runDispatch(wr, { dispatchId: wire.id, cursorId }, payload, agent)).catch((err) => {
11168
+ const tail = prev.then(() => this.runDispatch(wr, wire.id, payload, agent)).catch((err) => {
11335
11169
  this.log.warn(
11336
11170
  {
11337
11171
  workspaceId: wr.workspaceId,
@@ -11348,8 +11182,7 @@ var CompanionSupervisor = class {
11348
11182
  }
11349
11183
  // CT1146: tell the server this device cannot run a dispatch it was addressed to.
11350
11184
  //
11351
- // Returns whether the report LANDED, because that answer decides whether the
11352
- // caller may settle the cursor. The server checks the recorded dispatch
11185
+ // Returns whether the report LANDED. The server checks the recorded dispatch
11353
11186
  // destination even if the agent's assignment has moved. Reporting promptly
11354
11187
  // explains the roster miss; the thirty-minute unanswered-offer deadline is
11355
11188
  // only the backstop when no report lands.
@@ -11443,10 +11276,10 @@ var CompanionSupervisor = class {
11443
11276
  };
11444
11277
  const tail = this.runDispatch(
11445
11278
  wr,
11446
- // No frame delivered this one — it came off `/runs` — so there is
11447
- // neither a dispatch id to dedupe on nor a cursor to settle. The
11448
- // `recoveredTurns` set above is this path's own guard.
11449
- { dispatchId: "", cursorId: "" },
11279
+ // No frame delivered this one — it came off `/runs` — so there is no
11280
+ // dispatch id to dedupe on. The `recoveredTurns` set above is this
11281
+ // path's own guard.
11282
+ "",
11450
11283
  payload,
11451
11284
  agent,
11452
11285
  run.turnId
@@ -11468,61 +11301,55 @@ var CompanionSupervisor = class {
11468
11301
  }
11469
11302
  }
11470
11303
  // One turn: the SJ516 F7 / CT26 skip-vs-resume dedupe, the dispatch, then
11471
- // cursor + console summary. Mirrors the old per-workspace `handleEvent`, now
11304
+ // the console summary. Mirrors the old per-workspace `handleEvent`, now
11472
11305
  // scoped to one agent.
11473
- async runDispatch(wr, ids, payload, agent, recoveredTurnId) {
11306
+ async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
11474
11307
  const workspaceId = wr.workspaceId;
11475
- const settleCursor = () => {
11476
- if (ids.cursorId) wr.cursor.settle(ids.cursorId);
11477
- };
11478
- if (ids.dispatchId && hasDispatched(workspaceId, ids.dispatchId)) {
11479
- if (hasCompleted(workspaceId, ids.dispatchId)) {
11308
+ if (dispatchId && hasDispatched(workspaceId, dispatchId)) {
11309
+ if (hasCompleted(workspaceId, dispatchId)) {
11480
11310
  this.log.debug(
11481
- { workspaceId, eventId: ids.dispatchId },
11311
+ { workspaceId, eventId: dispatchId },
11482
11312
  "companion: skipping already-completed event (resume after restart)"
11483
11313
  );
11484
- settleCursor();
11485
11314
  return;
11486
11315
  }
11487
11316
  if (noResume()) {
11488
11317
  this.log.debug(
11489
- { workspaceId, eventId: ids.dispatchId },
11318
+ { workspaceId, eventId: dispatchId },
11490
11319
  "companion: skipping interrupted turn (CABANE_COMPANION_NO_RESUME=1) \u2014 resume disabled for this boot"
11491
11320
  );
11492
- markCompleted(workspaceId, ids.dispatchId);
11493
- settleCursor();
11321
+ markCompleted(workspaceId, dispatchId);
11494
11322
  return;
11495
11323
  }
11496
- const attempt = bumpResumeAttempt(workspaceId, ids.dispatchId);
11324
+ const attempt = bumpResumeAttempt(workspaceId, dispatchId);
11497
11325
  if (attempt > MAX_RESUME_ATTEMPTS) {
11498
11326
  this.log.error(
11499
- { workspaceId, eventId: ids.dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
11327
+ { workspaceId, eventId: dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
11500
11328
  "An interrupted turn couldn't finish after several attempts. Reply in Cabane to try again."
11501
11329
  );
11502
- markCompleted(workspaceId, ids.dispatchId);
11503
- settleCursor();
11330
+ markCompleted(workspaceId, dispatchId);
11504
11331
  return;
11505
11332
  }
11506
11333
  this.log.debug(
11507
- { workspaceId, eventId: ids.dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
11334
+ { workspaceId, eventId: dispatchId, attempt, cap: MAX_RESUME_ATTEMPTS },
11508
11335
  "companion: re-dispatching interrupted turn (resume after restart)"
11509
11336
  );
11510
11337
  }
11511
- const resumedTurnId = recoveredTurnId ?? (ids.dispatchId ? turnIdForEvent(workspaceId, ids.dispatchId) : null);
11338
+ const resumedTurnId = recoveredTurnId ?? (dispatchId ? turnIdForEvent(workspaceId, dispatchId) : null);
11512
11339
  const turnId = resumedTurnId ?? randomUUID4();
11513
- if (ids.dispatchId) {
11514
- const liveTurnIds = rememberTurnId(workspaceId, ids.dispatchId, turnId);
11340
+ if (dispatchId) {
11341
+ const liveTurnIds = rememberTurnId(workspaceId, dispatchId, turnId);
11515
11342
  if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
11516
11343
  this.log.debug(
11517
11344
  { workspaceId, liveTurnIds, threshold: TURN_ID_OVERFLOW_WARN },
11518
11345
  "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."
11519
11346
  );
11520
11347
  }
11521
- markDispatched(workspaceId, ids.dispatchId);
11348
+ markDispatched(workspaceId, dispatchId);
11522
11349
  }
11523
11350
  if (resumedTurnId) {
11524
11351
  this.log.debug(
11525
- { workspaceId, eventId: ids.dispatchId, turnId },
11352
+ { workspaceId, eventId: dispatchId, turnId },
11526
11353
  "companion: resuming an interrupted turn under its original id"
11527
11354
  );
11528
11355
  }
@@ -11537,8 +11364,7 @@ var CompanionSupervisor = class {
11537
11364
  turnId,
11538
11365
  resumed: resumedTurnId !== null
11539
11366
  });
11540
- if (ids.dispatchId) markCompleted(workspaceId, ids.dispatchId);
11541
- settleCursor();
11367
+ if (dispatchId) markCompleted(workspaceId, dispatchId);
11542
11368
  const durationS = (result.durationMs / 1e3).toFixed(1);
11543
11369
  const bindings = {
11544
11370
  ...turnBindings,
@@ -11787,12 +11613,10 @@ var CompanionSupervisor = class {
11787
11613
  this.deviceSub?.stop();
11788
11614
  for (const wr of this.workspaces.values()) {
11789
11615
  for (const a of wr.agents.values()) a.cancelDrain();
11790
- if (wr.sub) wr.sub.stop();
11791
11616
  }
11792
11617
  await Promise.all([
11793
11618
  ...heartbeat ? [waitForBoundedHeartbeat(heartbeat)] : [],
11794
- ...this.deviceSub ? [this.deviceSub.finished] : [],
11795
- ...[...this.workspaces.values()].flatMap((wr) => wr.sub ? [wr.sub.finished] : [])
11619
+ ...this.deviceSub ? [this.deviceSub.finished] : []
11796
11620
  ]);
11797
11621
  }
11798
11622
  async requestStop() {
@@ -11889,22 +11713,22 @@ function handleUncaught(log, err, origin) {
11889
11713
  }
11890
11714
 
11891
11715
  // src/crash-marker.ts
11892
- import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync10, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
11893
- import { join as join17 } from "path";
11716
+ import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync7 } from "fs";
11717
+ import { join as join16 } from "path";
11894
11718
  function crashMarkerPath() {
11895
- return join17(cabaneDir(), "last-error.json");
11719
+ return join16(cabaneDir(), "last-error.json");
11896
11720
  }
11897
11721
  function recordCrash(rec) {
11898
11722
  try {
11899
- mkdirSync11(cabaneDir(), { recursive: true });
11900
- writeFileSync8(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
11723
+ mkdirSync10(cabaneDir(), { recursive: true });
11724
+ writeFileSync7(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
11901
11725
  } catch {
11902
11726
  }
11903
11727
  }
11904
11728
  function clearCrash() {
11905
11729
  try {
11906
11730
  const path = crashMarkerPath();
11907
- if (existsSync13(path)) rmSync8(path, { force: true });
11731
+ if (existsSync12(path)) rmSync8(path, { force: true });
11908
11732
  } catch {
11909
11733
  }
11910
11734
  }