@cabane/companion 0.6.122 → 0.6.123

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 +113 -23
  2. package/dist/runtime.js +108 -18
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1334,6 +1334,13 @@ var init_api = __esm({
1334
1334
  (e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
1335
1335
  );
1336
1336
  }
1337
+ // CT1680: the launcher mints the runner credential for one dispatch, on its
1338
+ // PAT and from its device, and hands it to the runner in place of both. A
1339
+ // retry is safe: the server returns the same runner id and retires the
1340
+ // credential the lost response carried.
1341
+ mintRunnerCredential(body) {
1342
+ return this.request("POST", "/api/agent/runner-credentials", body, { retry: true });
1343
+ }
1337
1344
  // Read the skip intent recorded by the URL turn-control MCP surface.
1338
1345
  // Codex/opencode use this server-side state; settle imports skipped/skipReason
1339
1346
  // into the same local skip state used by claude-code's in-process tools.
@@ -7049,8 +7056,7 @@ function parseRunnerInput(env, stdin) {
7049
7056
  const credentials = credentialsSchema.parse(JSON.parse(stdin || "{}"));
7050
7057
  return {
7051
7058
  baseUrl: required(env, RUNNER_ENV.url),
7052
- agentToken: credentials.agentToken,
7053
- ...credentials.deviceToken ? { deviceToken: credentials.deviceToken } : {},
7059
+ credential: credentials.credential,
7054
7060
  dispatch,
7055
7061
  turnId: required(env, RUNNER_ENV.turnId),
7056
7062
  resumed: env[RUNNER_ENV.resumed] === "1",
@@ -7067,13 +7073,7 @@ function runnerEnv(input) {
7067
7073
  };
7068
7074
  }
7069
7075
  function runnerStdin(input) {
7070
- return `${JSON.stringify({
7071
- // Phase 1 (CT1680) replaces the agent PAT with a runner credential bound to
7072
- // this one dispatch; until then the child holds what the in-process turn
7073
- // held, on the same machine.
7074
- agentToken: input.agentToken,
7075
- ...input.deviceToken ? { deviceToken: input.deviceToken } : {}
7076
- })}
7076
+ return `${JSON.stringify({ credential: input.credential })}
7077
7077
  `;
7078
7078
  }
7079
7079
  function encodeLauncherMessage(message) {
@@ -7157,7 +7157,10 @@ var init_protocol = __esm({
7157
7157
  connectorHealth: z14.record(z14.string(), runtimeHealthSchema),
7158
7158
  // True when this (conversation, agent) pair was already told its session
7159
7159
  // could not be saved: the notice is once per pair, not once per process.
7160
- sessionWriteNoticeClaimed: z14.boolean()
7160
+ sessionWriteNoticeClaimed: z14.boolean(),
7161
+ // CT1680: the runner's identity, minted with its credential. The start
7162
+ // PATCH carries it so the lease knows which runner holds the span.
7163
+ runnerId: z14.string()
7161
7164
  });
7162
7165
  dispatchSchema = z14.object({
7163
7166
  type: z14.literal("device:dispatch_requested"),
@@ -7168,8 +7171,7 @@ var init_protocol = __esm({
7168
7171
  episode: z14.number().int().optional()
7169
7172
  });
7170
7173
  credentialsSchema = z14.object({
7171
- agentToken: z14.string().min(1),
7172
- deviceToken: z14.string().min(1).optional()
7174
+ credential: z14.string().startsWith("cabrn_")
7173
7175
  });
7174
7176
  }
7175
7177
  });
@@ -7192,8 +7194,9 @@ function healthSnapshot(store) {
7192
7194
  }
7193
7195
  return out;
7194
7196
  }
7195
- function runnerSpec(opts, launch, duties) {
7197
+ function runnerSpec(opts, launch, duties, runnerId) {
7196
7198
  return {
7199
+ runnerId,
7197
7200
  ...opts.deviceId ? { deviceId: opts.deviceId } : {},
7198
7201
  bootId,
7199
7202
  workspaceId: opts.workspaceId,
@@ -7274,6 +7277,43 @@ function giveBackLocks(locks, prefix, remote, launch, log) {
7274
7277
  })();
7275
7278
  }
7276
7279
  }
7280
+ async function mintForRunner(opts, payload, handleOpts, signal, log) {
7281
+ try {
7282
+ const minted = await opts.api.mintRunnerCredential({
7283
+ conversationId: payload.conversationId,
7284
+ messageId: payload.messageId,
7285
+ ...payload.episode !== void 0 ? { episode: payload.episode } : {},
7286
+ // The one turn the credential will operate: this run's.
7287
+ turnId: handleOpts.turnId
7288
+ });
7289
+ return { ok: true, credential: minted.credential, runnerId: minted.runnerId };
7290
+ } catch (err) {
7291
+ if (signal.aborted) return { ok: false, reason: "cancelled" };
7292
+ const status2 = err instanceof ApiError ? err.status : 0;
7293
+ if (status2 === 404) return { ok: false, reason: "runner_credential_not_found" };
7294
+ if (status2 === 409 && apiErrorCode(err) === "dispatch_not_admitted")
7295
+ return { ok: false, reason: "runner_credential_not_admitted" };
7296
+ log.warn(apiErrorLogFields(err), "runner: couldn't get a credential for the turn's runner");
7297
+ const reason = `fetch_failed: ${describeError(err)}`;
7298
+ return { ok: false, reason, errorReason: reason };
7299
+ }
7300
+ }
7301
+ async function settleUnstarted(opts, payload, turnId, errorReason, log) {
7302
+ try {
7303
+ await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
7304
+ activeRunStartedAt: null,
7305
+ turnId,
7306
+ outcome: "failed",
7307
+ ...errorReason ? { errorReason: errorReason.slice(0, 200) } : {},
7308
+ readThrough: payload.messageId
7309
+ });
7310
+ } catch (err) {
7311
+ log.warn(
7312
+ { ...apiErrorLogFields(err), turnId },
7313
+ "Couldn't update the turn's status in Cabane. Check the conversation before trying again."
7314
+ );
7315
+ }
7316
+ }
7277
7317
  async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7278
7318
  const startedAt = Date.now();
7279
7319
  const { turnId } = handleOpts;
@@ -7297,6 +7337,15 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7297
7337
  agentId: payload.agentId,
7298
7338
  turnId
7299
7339
  };
7340
+ const minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log);
7341
+ if (!minted.ok) {
7342
+ controller.signal.removeEventListener("abort", forwardStop);
7343
+ duties.releaseAbort(turnId, controller);
7344
+ const durationMs2 = Date.now() - startedAt;
7345
+ await settleUnstarted(opts, payload, turnId, minted.errorReason, log);
7346
+ duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: minted.reason });
7347
+ return { ok: false, durationMs: durationMs2, reason: minted.reason };
7348
+ }
7300
7349
  let result;
7301
7350
  let thrown;
7302
7351
  let code;
@@ -7314,12 +7363,11 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7314
7363
  }
7315
7364
  const input = {
7316
7365
  baseUrl: opts.baseUrl,
7317
- agentToken: opts.credential,
7318
- ...launch.deviceToken ? { deviceToken: launch.deviceToken } : {},
7366
+ credential: minted.credential,
7319
7367
  dispatch: payload,
7320
7368
  turnId,
7321
7369
  resumed: handleOpts.resumed,
7322
- spec: runnerSpec(opts, launch, duties)
7370
+ spec: runnerSpec(opts, launch, duties, minted.runnerId)
7323
7371
  };
7324
7372
  Object.assign(env, runnerEnv(input));
7325
7373
  const running = launch.provider.exec(handle, launch.command, env, {
@@ -7431,6 +7479,14 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7431
7479
  "Couldn't update the turn's status in Cabane. Check the conversation before trying again."
7432
7480
  );
7433
7481
  }
7482
+ await launch.reportRunnerGone?.({
7483
+ workspaceId: opts.workspaceId,
7484
+ conversationId: payload.conversationId,
7485
+ agentId: payload.agentId,
7486
+ messageId: payload.messageId,
7487
+ runnerId: minted.runnerId,
7488
+ reason: "exited"
7489
+ });
7434
7490
  if (thrown !== void 0) throw new Error(thrown);
7435
7491
  log.warn({ turnId, code }, "runner: exited without reporting a result");
7436
7492
  duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
@@ -7442,6 +7498,7 @@ var init_launch = __esm({
7442
7498
  "use strict";
7443
7499
  init_opencode_lock();
7444
7500
  init_api_error_shape();
7501
+ init_errors();
7445
7502
  init_boot_id();
7446
7503
  init_turn_containment();
7447
7504
  init_protocol();
@@ -8769,14 +8826,15 @@ var init_turn_execution = __esm({
8769
8826
  const reportPreparing = (phase) => {
8770
8827
  preparing ??= { id: `prepare:${randomUUID3()}`, seq: this.nextSeq() };
8771
8828
  const { id, seq } = preparing;
8772
- void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, {
8829
+ const body = {
8773
8830
  turnId,
8774
8831
  toolUseId: id,
8775
8832
  toolName: PREPARING_TOOL_NAME,
8776
8833
  summary: "",
8777
8834
  phase,
8778
8835
  seq
8779
- }).catch((err) => {
8836
+ };
8837
+ void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, body).catch((err) => {
8780
8838
  turnLog.debug(
8781
8839
  { err: err instanceof Error ? err.message : String(err), phase },
8782
8840
  "dispatcher: preparing-activity report failed (continuing with the hook)"
@@ -8852,6 +8910,8 @@ var init_turn_execution = __esm({
8852
8910
  // CT1679: a runner leases under its launcher's boot id — the one the
8853
8911
  // device heartbeats — or the lost-run sweep reads it as a restart.
8854
8912
  bootId: this.opts.inRunner?.bootId ?? bootId,
8913
+ // CT1680: the runner holding the span, beside the device's boot id.
8914
+ ...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
8855
8915
  // CT1380: the durable record that this turn resumed, and from where.
8856
8916
  ...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
8857
8917
  // CT1454: the issuance the context THIS attempt runs on came from. Read
@@ -9219,7 +9279,10 @@ var init_turn_execution = __esm({
9219
9279
  workspaceId,
9220
9280
  payload.conversationId,
9221
9281
  payload.agentId,
9222
- { agentSessionId: event.state }
9282
+ // CT1680: named for its turn, which a runner credential
9283
+ // requires once its start is admitted; the server then binds
9284
+ // the capture to that turn's attempt rather than the pair's.
9285
+ { agentSessionId: event.state, turnId }
9223
9286
  );
9224
9287
  } catch (err) {
9225
9288
  const status2 = err instanceof ApiError ? err.status : void 0;
@@ -10028,8 +10091,7 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
10028
10091
  const outbox = new Outbox(spec.agentId, log);
10029
10092
  const api = new CabaneApi({
10030
10093
  baseUrl: input.baseUrl,
10031
- token: input.agentToken,
10032
- ...input.deviceToken ? { deviceToken: input.deviceToken } : {},
10094
+ token: input.credential,
10033
10095
  outbox,
10034
10096
  log
10035
10097
  });
@@ -10051,7 +10113,7 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
10051
10113
  workspaceSlug: spec.workspaceSlug,
10052
10114
  agentId: spec.agentId,
10053
10115
  agentUsername: spec.agentUsername,
10054
- credential: input.agentToken,
10116
+ credential: input.credential,
10055
10117
  local: spec.local,
10056
10118
  runConfig: spec.runConfig,
10057
10119
  log,
@@ -10067,6 +10129,7 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
10067
10129
  connectorHealth,
10068
10130
  inRunner: {
10069
10131
  bootId: spec.bootId,
10132
+ runnerId: spec.runnerId,
10070
10133
  opencodeTransport: (serverUrl) => runnerOpencodeTransport(
10071
10134
  serverUrl,
10072
10135
  acquireLock,
@@ -13433,7 +13496,7 @@ var CompanionSupervisor = class {
13433
13496
  runner: {
13434
13497
  provider: this.runnerProvider(),
13435
13498
  command: this.runnerCommand ?? selfRunnerCommand(),
13436
- ...this.config.deviceToken ? { deviceToken: this.config.deviceToken } : {},
13499
+ reportRunnerGone: (report) => this.reportRunnerGone(report),
13437
13500
  connectorHealth: this.connectorHealth,
13438
13501
  locks: this.runnerLocks,
13439
13502
  sandbox: this.runnerSandbox()
@@ -13623,6 +13686,33 @@ var CompanionSupervisor = class {
13623
13686
  );
13624
13687
  return false;
13625
13688
  }
13689
+ // CT1680: a runner is gone without having settled its turn. Same bounded
13690
+ // retry as the unrunnable report above, on the device credential, which is
13691
+ // the one the launcher still holds whatever became of the runner.
13692
+ async reportRunnerGone(report) {
13693
+ if (!this.deviceApi) return;
13694
+ let lastErr;
13695
+ for (const wait of this.unrunnableRetryDelaysMs) {
13696
+ if (this.stopped) return;
13697
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
13698
+ try {
13699
+ await this.deviceApi.reportDispatchDrop(report);
13700
+ return;
13701
+ } catch (err) {
13702
+ lastErr = err;
13703
+ }
13704
+ }
13705
+ this.log.error(
13706
+ {
13707
+ workspaceId: report.workspaceId,
13708
+ conversationId: report.conversationId,
13709
+ agentId: report.agentId,
13710
+ runnerId: report.runnerId,
13711
+ err: lastErr instanceof Error ? lastErr.message : String(lastErr)
13712
+ },
13713
+ "Couldn't tell Cabane a turn's runner stopped; its credential expires on its own."
13714
+ );
13715
+ }
13626
13716
  // Pull-on-miss for the assignment-change race: a dispatch arrived for an agent
13627
13717
  // we don't yet run. If it's addressed to THIS device, the assignment just
13628
13718
  // changed under us (the poll hasn't caught it) — force a fresh assignments pull
package/dist/runtime.js CHANGED
@@ -2516,6 +2516,13 @@ var CabaneApi = class {
2516
2516
  (e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
2517
2517
  );
2518
2518
  }
2519
+ // CT1680: the launcher mints the runner credential for one dispatch, on its
2520
+ // PAT and from its device, and hands it to the runner in place of both. A
2521
+ // retry is safe: the server returns the same runner id and retires the
2522
+ // credential the lost response carried.
2523
+ mintRunnerCredential(body) {
2524
+ return this.request("POST", "/api/agent/runner-credentials", body, { retry: true });
2525
+ }
2519
2526
  // Read the skip intent recorded by the URL turn-control MCP surface.
2520
2527
  // Codex/opencode use this server-side state; settle imports skipped/skipReason
2521
2528
  // into the same local skip state used by claude-code's in-process tools.
@@ -8243,7 +8250,10 @@ var runnerSpecSchema = z14.object({
8243
8250
  connectorHealth: z14.record(z14.string(), runtimeHealthSchema),
8244
8251
  // True when this (conversation, agent) pair was already told its session
8245
8252
  // could not be saved: the notice is once per pair, not once per process.
8246
- sessionWriteNoticeClaimed: z14.boolean()
8253
+ sessionWriteNoticeClaimed: z14.boolean(),
8254
+ // CT1680: the runner's identity, minted with its credential. The start
8255
+ // PATCH carries it so the lease knows which runner holds the span.
8256
+ runnerId: z14.string()
8247
8257
  });
8248
8258
  var dispatchSchema = z14.object({
8249
8259
  type: z14.literal("device:dispatch_requested"),
@@ -8254,8 +8264,7 @@ var dispatchSchema = z14.object({
8254
8264
  episode: z14.number().int().optional()
8255
8265
  });
8256
8266
  var credentialsSchema = z14.object({
8257
- agentToken: z14.string().min(1),
8258
- deviceToken: z14.string().min(1).optional()
8267
+ credential: z14.string().startsWith("cabrn_")
8259
8268
  });
8260
8269
  function runnerEnv(input) {
8261
8270
  return {
@@ -8267,13 +8276,7 @@ function runnerEnv(input) {
8267
8276
  };
8268
8277
  }
8269
8278
  function runnerStdin(input) {
8270
- return `${JSON.stringify({
8271
- // Phase 1 (CT1680) replaces the agent PAT with a runner credential bound to
8272
- // this one dispatch; until then the child holds what the in-process turn
8273
- // held, on the same machine.
8274
- agentToken: input.agentToken,
8275
- ...input.deviceToken ? { deviceToken: input.deviceToken } : {}
8276
- })}
8279
+ return `${JSON.stringify({ credential: input.credential })}
8277
8280
  `;
8278
8281
  }
8279
8282
  function encodeLauncherMessage(message) {
@@ -8309,8 +8312,9 @@ function healthSnapshot(store) {
8309
8312
  }
8310
8313
  return out;
8311
8314
  }
8312
- function runnerSpec(opts, launch, duties) {
8315
+ function runnerSpec(opts, launch, duties, runnerId) {
8313
8316
  return {
8317
+ runnerId,
8314
8318
  ...opts.deviceId ? { deviceId: opts.deviceId } : {},
8315
8319
  bootId,
8316
8320
  workspaceId: opts.workspaceId,
@@ -8399,6 +8403,43 @@ function giveBackLocks(locks, prefix, remote, launch, log) {
8399
8403
  })();
8400
8404
  }
8401
8405
  }
8406
+ async function mintForRunner(opts, payload, handleOpts, signal, log) {
8407
+ try {
8408
+ const minted = await opts.api.mintRunnerCredential({
8409
+ conversationId: payload.conversationId,
8410
+ messageId: payload.messageId,
8411
+ ...payload.episode !== void 0 ? { episode: payload.episode } : {},
8412
+ // The one turn the credential will operate: this run's.
8413
+ turnId: handleOpts.turnId
8414
+ });
8415
+ return { ok: true, credential: minted.credential, runnerId: minted.runnerId };
8416
+ } catch (err) {
8417
+ if (signal.aborted) return { ok: false, reason: "cancelled" };
8418
+ const status = err instanceof ApiError ? err.status : 0;
8419
+ if (status === 404) return { ok: false, reason: "runner_credential_not_found" };
8420
+ if (status === 409 && apiErrorCode(err) === "dispatch_not_admitted")
8421
+ return { ok: false, reason: "runner_credential_not_admitted" };
8422
+ log.warn(apiErrorLogFields(err), "runner: couldn't get a credential for the turn's runner");
8423
+ const reason = `fetch_failed: ${describeError(err)}`;
8424
+ return { ok: false, reason, errorReason: reason };
8425
+ }
8426
+ }
8427
+ async function settleUnstarted(opts, payload, turnId, errorReason, log) {
8428
+ try {
8429
+ await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
8430
+ activeRunStartedAt: null,
8431
+ turnId,
8432
+ outcome: "failed",
8433
+ ...errorReason ? { errorReason: errorReason.slice(0, 200) } : {},
8434
+ readThrough: payload.messageId
8435
+ });
8436
+ } catch (err) {
8437
+ log.warn(
8438
+ { ...apiErrorLogFields(err), turnId },
8439
+ "Couldn't update the turn's status in Cabane. Check the conversation before trying again."
8440
+ );
8441
+ }
8442
+ }
8402
8443
  async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
8403
8444
  const startedAt = Date.now();
8404
8445
  const { turnId } = handleOpts;
@@ -8422,6 +8463,15 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
8422
8463
  agentId: payload.agentId,
8423
8464
  turnId
8424
8465
  };
8466
+ const minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log);
8467
+ if (!minted.ok) {
8468
+ controller.signal.removeEventListener("abort", forwardStop);
8469
+ duties.releaseAbort(turnId, controller);
8470
+ const durationMs2 = Date.now() - startedAt;
8471
+ await settleUnstarted(opts, payload, turnId, minted.errorReason, log);
8472
+ duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: minted.reason });
8473
+ return { ok: false, durationMs: durationMs2, reason: minted.reason };
8474
+ }
8425
8475
  let result;
8426
8476
  let thrown;
8427
8477
  let code;
@@ -8439,12 +8489,11 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
8439
8489
  }
8440
8490
  const input = {
8441
8491
  baseUrl: opts.baseUrl,
8442
- agentToken: opts.credential,
8443
- ...launch.deviceToken ? { deviceToken: launch.deviceToken } : {},
8492
+ credential: minted.credential,
8444
8493
  dispatch: payload,
8445
8494
  turnId,
8446
8495
  resumed: handleOpts.resumed,
8447
- spec: runnerSpec(opts, launch, duties)
8496
+ spec: runnerSpec(opts, launch, duties, minted.runnerId)
8448
8497
  };
8449
8498
  Object.assign(env, runnerEnv(input));
8450
8499
  const running = launch.provider.exec(handle, launch.command, env, {
@@ -8556,6 +8605,14 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
8556
8605
  "Couldn't update the turn's status in Cabane. Check the conversation before trying again."
8557
8606
  );
8558
8607
  }
8608
+ await launch.reportRunnerGone?.({
8609
+ workspaceId: opts.workspaceId,
8610
+ conversationId: payload.conversationId,
8611
+ agentId: payload.agentId,
8612
+ messageId: payload.messageId,
8613
+ runnerId: minted.runnerId,
8614
+ reason: "exited"
8615
+ });
8559
8616
  if (thrown !== void 0) throw new Error(thrown);
8560
8617
  log.warn({ turnId, code }, "runner: exited without reporting a result");
8561
8618
  duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
@@ -9783,14 +9840,15 @@ var TurnExecution = class {
9783
9840
  const reportPreparing = (phase) => {
9784
9841
  preparing ??= { id: `prepare:${randomUUID3()}`, seq: this.nextSeq() };
9785
9842
  const { id, seq } = preparing;
9786
- void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, {
9843
+ const body = {
9787
9844
  turnId,
9788
9845
  toolUseId: id,
9789
9846
  toolName: PREPARING_TOOL_NAME,
9790
9847
  summary: "",
9791
9848
  phase,
9792
9849
  seq
9793
- }).catch((err) => {
9850
+ };
9851
+ void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, body).catch((err) => {
9794
9852
  turnLog.debug(
9795
9853
  { err: err instanceof Error ? err.message : String(err), phase },
9796
9854
  "dispatcher: preparing-activity report failed (continuing with the hook)"
@@ -9866,6 +9924,8 @@ var TurnExecution = class {
9866
9924
  // CT1679: a runner leases under its launcher's boot id — the one the
9867
9925
  // device heartbeats — or the lost-run sweep reads it as a restart.
9868
9926
  bootId: this.opts.inRunner?.bootId ?? bootId,
9927
+ // CT1680: the runner holding the span, beside the device's boot id.
9928
+ ...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
9869
9929
  // CT1380: the durable record that this turn resumed, and from where.
9870
9930
  ...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
9871
9931
  // CT1454: the issuance the context THIS attempt runs on came from. Read
@@ -10233,7 +10293,10 @@ var TurnExecution = class {
10233
10293
  workspaceId,
10234
10294
  payload.conversationId,
10235
10295
  payload.agentId,
10236
- { agentSessionId: event.state }
10296
+ // CT1680: named for its turn, which a runner credential
10297
+ // requires once its start is admitted; the server then binds
10298
+ // the capture to that turn's attempt rather than the pair's.
10299
+ { agentSessionId: event.state, turnId }
10237
10300
  );
10238
10301
  } catch (err) {
10239
10302
  const status = err instanceof ApiError ? err.status : void 0;
@@ -11731,7 +11794,7 @@ var CompanionSupervisor = class {
11731
11794
  runner: {
11732
11795
  provider: this.runnerProvider(),
11733
11796
  command: this.runnerCommand ?? selfRunnerCommand(),
11734
- ...this.config.deviceToken ? { deviceToken: this.config.deviceToken } : {},
11797
+ reportRunnerGone: (report) => this.reportRunnerGone(report),
11735
11798
  connectorHealth: this.connectorHealth,
11736
11799
  locks: this.runnerLocks,
11737
11800
  sandbox: this.runnerSandbox()
@@ -11921,6 +11984,33 @@ var CompanionSupervisor = class {
11921
11984
  );
11922
11985
  return false;
11923
11986
  }
11987
+ // CT1680: a runner is gone without having settled its turn. Same bounded
11988
+ // retry as the unrunnable report above, on the device credential, which is
11989
+ // the one the launcher still holds whatever became of the runner.
11990
+ async reportRunnerGone(report) {
11991
+ if (!this.deviceApi) return;
11992
+ let lastErr;
11993
+ for (const wait of this.unrunnableRetryDelaysMs) {
11994
+ if (this.stopped) return;
11995
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
11996
+ try {
11997
+ await this.deviceApi.reportDispatchDrop(report);
11998
+ return;
11999
+ } catch (err) {
12000
+ lastErr = err;
12001
+ }
12002
+ }
12003
+ this.log.error(
12004
+ {
12005
+ workspaceId: report.workspaceId,
12006
+ conversationId: report.conversationId,
12007
+ agentId: report.agentId,
12008
+ runnerId: report.runnerId,
12009
+ err: lastErr instanceof Error ? lastErr.message : String(lastErr)
12010
+ },
12011
+ "Couldn't tell Cabane a turn's runner stopped; its credential expires on its own."
12012
+ );
12013
+ }
11924
12014
  // Pull-on-miss for the assignment-change race: a dispatch arrived for an agent
11925
12015
  // we don't yet run. If it's addressed to THIS device, the assignment just
11926
12016
  // changed under us (the poll hasn't caught it) — force a fresh assignments pull
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.122",
3
+ "version": "0.6.123",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",