@cabane/companion 0.6.120 → 0.6.121

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