@cabane/companion 0.6.118 → 0.6.120

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.
package/dist/cli.js CHANGED
@@ -2947,6 +2947,7 @@ var bootId = randomUUID();
2947
2947
  import { randomUUID as randomUUID4 } from "crypto";
2948
2948
 
2949
2949
  // src/api.ts
2950
+ var DEVICE_HEADER = "x-cabane-device";
2950
2951
  var RETRY_BACKOFF_MS = [250, 750];
2951
2952
  var TURN_CONTEXT_BACKOFF_MS = [250, 750, 2e3, 5e3];
2952
2953
  var ACTIVE_RUN_OUTBOX_SEQ = 0;
@@ -2976,6 +2977,7 @@ var CabaneApi = class {
2976
2977
  method,
2977
2978
  headers: {
2978
2979
  Authorization: `Bearer ${this.opts.token}`,
2980
+ ...this.opts.deviceToken ? { [DEVICE_HEADER]: this.opts.deviceToken } : {},
2979
2981
  ...body !== void 0 ? { "content-type": "application/json" } : {},
2980
2982
  accept: "application/json"
2981
2983
  },
@@ -4368,11 +4370,14 @@ var TurnPump = class {
4368
4370
  },
4369
4371
  onToolActivity: async (evt) => {
4370
4372
  if (opts.signal.aborted) return;
4371
- const existing = this.toolSeqByUseId.get(evt.toolUseId);
4372
- const seq = existing ?? opts.nextSeq();
4373
- if (existing === void 0) this.toolSeqByUseId.set(evt.toolUseId, seq);
4373
+ let call = this.toolCallByProviderId.get(evt.toolUseId);
4374
+ if (evt.phase === "start" || call === void 0) {
4375
+ call = { persistedId: crypto.randomUUID(), seq: opts.nextSeq() };
4376
+ this.toolCallByProviderId.set(evt.toolUseId, call);
4377
+ }
4378
+ const { persistedId, seq } = call;
4374
4379
  await opts.commit.reportTool({
4375
- toolUseId: evt.toolUseId,
4380
+ toolUseId: persistedId,
4376
4381
  toolName: evt.toolName,
4377
4382
  summary: evt.summary,
4378
4383
  input: evt.input,
@@ -4409,9 +4414,19 @@ var TurnPump = class {
4409
4414
  // The turn's final reply text, captured for the dashboard feed / logging.
4410
4415
  finalReplyBody = null;
4411
4416
  emittedFinalSource = "none";
4412
- // CT11: each tool's persisted seq so the `done`/`error` frame republishes the
4413
- // same seq the `start` row got (the host's upsert keeps the original seq).
4414
- toolSeqByUseId = /* @__PURE__ */ new Map();
4417
+ // CT1383: provider tool id → the persisted identity of its CURRENT call. The
4418
+ // provider's id is only a lookup key: it is unique within one adapter
4419
+ // invocation at best (Codex restarts at `item_0` on every invocation, and a
4420
+ // resumed turn keeps its turn id), so the row and the outbox entry are keyed
4421
+ // on what the pump mints — a host id for the host's `(turn_id, tool_use_id)`
4422
+ // upsert, and a seq for the outbox's `(turnId, seq)` file. Both are minted
4423
+ // together; minting only the id would let two calls overwrite each other's
4424
+ // queued body.
4425
+ //
4426
+ // In memory on purpose: it dies with the process, and so does the runtime.
4427
+ // After a crash no in-flight `done` awaits a pre-crash `start` — the resumed
4428
+ // invocation starts its calls afresh — so there is nothing to recover.
4429
+ toolCallByProviderId = /* @__PURE__ */ new Map();
4415
4430
  // The callback consumer the classification (via `sinkEmitter`) — and the
4416
4431
  // frozen committer test — drive. Each callback applies the choreography and
4417
4432
  // commits through the injected sink.
@@ -4806,11 +4821,7 @@ async function* decodeSdkStream(iter, ctx) {
4806
4821
  let usage;
4807
4822
  let lastRequestContextTokens;
4808
4823
  let resolvedModel;
4809
- let sawRejectedLimit = false;
4810
- let rateLimitResetIso;
4811
- let rateLimitType;
4812
- let authError;
4813
- let lastAssistantError;
4824
+ const signals = { sawRejectedLimit: false };
4814
4825
  try {
4815
4826
  for await (const msg of iter) {
4816
4827
  if (ctx.signal.aborted) return;
@@ -4833,12 +4844,14 @@ async function* decodeSdkStream(iter, ctx) {
4833
4844
  };
4834
4845
  }
4835
4846
  } else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
4836
- resultReason = "session_start_failed";
4847
+ if (preInitToolActivity(msg) && ctx.startEvidence) ctx.startEvidence.toolActivity = true;
4848
+ const failure = classifyPreInitFrame(msg, signals);
4849
+ resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
4837
4850
  break;
4838
4851
  } else if (msg.type === "assistant") {
4839
4852
  const assistantErr = msg.error;
4840
4853
  if (typeof assistantErr === "string" && assistantErr.length > 0) {
4841
- lastAssistantError = assistantErr;
4854
+ signals.lastAssistantError = assistantErr;
4842
4855
  }
4843
4856
  const reqContext = readRequestContextTokens(msg);
4844
4857
  if (reqContext !== void 0) lastRequestContextTokens = reqContext;
@@ -4850,13 +4863,13 @@ async function* decodeSdkStream(iter, ctx) {
4850
4863
  } else if (msg.type === "rate_limit_event") {
4851
4864
  const info = msg.rate_limit_info;
4852
4865
  if (info?.status === "rejected") {
4853
- sawRejectedLimit = true;
4854
- rateLimitResetIso = resetsAtToIso(info.resetsAt) ?? rateLimitResetIso;
4855
- if (typeof info.rateLimitType === "string") rateLimitType = info.rateLimitType;
4866
+ signals.sawRejectedLimit = true;
4867
+ signals.rateLimitResetIso = resetsAtToIso(info.resetsAt) ?? signals.rateLimitResetIso;
4868
+ if (typeof info.rateLimitType === "string") signals.rateLimitType = info.rateLimitType;
4856
4869
  }
4857
4870
  } else if (msg.type === "auth_status") {
4858
4871
  const err = msg.error;
4859
- if (typeof err === "string" && err.length > 0) authError = err;
4872
+ if (typeof err === "string" && err.length > 0) signals.authError = err;
4860
4873
  } else if (msg.type === "result") {
4861
4874
  const origin = msg.origin;
4862
4875
  if (origin?.kind === "task-notification") continue;
@@ -4873,16 +4886,7 @@ async function* decodeSdkStream(iter, ctx) {
4873
4886
  ok = true;
4874
4887
  } else {
4875
4888
  const resultText = msg.result ?? "";
4876
- const terminalReason = msg.terminal_reason;
4877
- const errors = msg.errors;
4878
- const errorText = [resultText, ...Array.isArray(errors) ? errors.map(String) : []].join(
4879
- " "
4880
- );
4881
- const rejectedIsCap = sawRejectedLimit && (rateLimitResetIso !== void 0 || isSubscriptionWindow(rateLimitType));
4882
- const failure = rejectedIsCap || terminalReason === "blocking_limit" ? {
4883
- kind: "usage_capped",
4884
- ...rateLimitResetIso ? { resetsAt: rateLimitResetIso } : {}
4885
- } : classifyAssistantError(lastAssistantError) ?? classifyErrorText([authError, errorText].filter(Boolean).join(" "));
4889
+ const failure = classifyResultFailure(msg, signals);
4886
4890
  resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
4887
4891
  ok = false;
4888
4892
  }
@@ -4891,16 +4895,25 @@ async function* decodeSdkStream(iter, ctx) {
4891
4895
  }
4892
4896
  } catch (err) {
4893
4897
  if (ctx.signal.aborted) throw err;
4894
- const failure = classifyErrorText(err instanceof Error ? err.message : String(err));
4895
- if (!failure) throw err;
4898
+ const message = err instanceof Error ? err.message : String(err);
4899
+ const failure = mcpInventory.initReceived ? classifyErrorText(message) : classifyFailure(signals, { text: message });
4900
+ if (!failure && (mcpInventory.initReceived || ctx.resumedSessionId === null)) throw err;
4896
4901
  ok = false;
4897
- resultReason = encodeFailureReason(failure);
4902
+ if (failure) {
4903
+ resultReason = encodeFailureReason(failure);
4904
+ } else {
4905
+ resultReason = "session_start_failed";
4906
+ if (ctx.startEvidence) ctx.startEvidence.thrown = message.slice(0, 200);
4907
+ }
4898
4908
  sawResult = true;
4899
4909
  }
4900
4910
  if (ctx.signal.aborted) return;
4901
4911
  if (!workspaceProven) {
4902
4912
  ok = false;
4903
- resultReason ??= "session_start_failed";
4913
+ if (resultReason === void 0) {
4914
+ const failure = classifyFailure(signals, { text: "" });
4915
+ resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
4916
+ }
4904
4917
  }
4905
4918
  const heldFailure = !ok ? classifyRuntimeNoticeText(buffer.held) : null;
4906
4919
  const terminalFailure = !ok ? decodeFailureReason(resultReason) : null;
@@ -4935,6 +4948,42 @@ function provesWorkspaceTools(msg) {
4935
4948
  const tools = msg.tools;
4936
4949
  return Array.isArray(tools) && tools.includes("mcp__cabane__query");
4937
4950
  }
4951
+ function classifyFailure(s, frame) {
4952
+ const rejectedIsCap = s.sawRejectedLimit && (s.rateLimitResetIso !== void 0 || isSubscriptionWindow(s.rateLimitType));
4953
+ if (rejectedIsCap || frame.terminalReason === "blocking_limit")
4954
+ return {
4955
+ kind: "usage_capped",
4956
+ ...s.rateLimitResetIso ? { resetsAt: s.rateLimitResetIso } : {}
4957
+ };
4958
+ return classifyAssistantError(s.lastAssistantError) ?? classifyErrorText([s.authError, frame.text].filter(Boolean).join(" "));
4959
+ }
4960
+ function classifyResultFailure(msg, s) {
4961
+ const resultText = msg.result ?? "";
4962
+ const terminalReason = msg.terminal_reason;
4963
+ const errors = msg.errors;
4964
+ const text = [resultText, ...Array.isArray(errors) ? errors.map(String) : []].join(" ");
4965
+ return classifyFailure(s, { terminalReason, text });
4966
+ }
4967
+ function classifyPreInitFrame(msg, s) {
4968
+ if (msg.type === "result") {
4969
+ const isError = msg.is_error === true;
4970
+ if (msg.subtype === "success" && !isError) return classifyFailure(s, { text: "" });
4971
+ return classifyResultFailure(msg, s);
4972
+ }
4973
+ if (msg.type !== "assistant") return classifyFailure(s, { text: "" });
4974
+ const error = msg.error;
4975
+ const signals = typeof error === "string" && error.length > 0 ? { ...s, lastAssistantError: error } : s;
4976
+ const text = contentBlocks(msg).flatMap((b) => b.type === "text" && typeof b.text === "string" ? [b.text] : []).join(" ");
4977
+ return classifyFailure(signals, { text });
4978
+ }
4979
+ function preInitToolActivity(msg) {
4980
+ const kind = msg.type === "assistant" ? "tool_use" : msg.type === "user" ? "tool_result" : null;
4981
+ return kind !== null && contentBlocks(msg).some((b) => b.type === kind);
4982
+ }
4983
+ function contentBlocks(msg) {
4984
+ const content = msg.message?.content;
4985
+ return Array.isArray(content) ? content : [];
4986
+ }
4938
4987
  function isSubscriptionWindow(value) {
4939
4988
  return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
4940
4989
  }
@@ -4994,15 +5043,18 @@ function attachAbortController(options, signal) {
4994
5043
  else signal.addEventListener("abort", () => sdkAbort.abort(), { once: true });
4995
5044
  options.abortController = sdkAbort;
4996
5045
  }
4997
- function sdkEvents(queryFn, req, signal, built, degraded) {
5046
+ function sdkEvents(queryFn, req, signal, built, degraded, startEvidence) {
4998
5047
  attachAbortController(built.options, signal);
4999
5048
  const prompt = buildQueryPrompt(req);
5000
- const iter = queryFn({ prompt, options: built.options });
5049
+ const iter = {
5050
+ [Symbol.asyncIterator]: () => queryFn({ prompt, options: built.options })[Symbol.asyncIterator]()
5051
+ };
5001
5052
  return decodeSdkStream(iter, {
5002
5053
  signal,
5003
5054
  cwd: req.local.cwd,
5004
5055
  resumedSessionId: built.resume,
5005
- degraded
5056
+ degraded,
5057
+ ...startEvidence ? { startEvidence } : {}
5006
5058
  });
5007
5059
  }
5008
5060
  async function freshRequest(req, reason, hooks) {
@@ -5011,8 +5063,9 @@ async function freshRequest(req, reason, hooks) {
5011
5063
  }
5012
5064
  async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOptions, onWarn, degraded, hooks) {
5013
5065
  let emitted = false;
5014
- for await (const event of sdkEvents(queryFn, req, signal, initial, degraded)) {
5015
- const recoverResume = initial.resume !== null && !emitted && event.type === "result" && !event.ok && event.reason === "session_start_failed" && event.mcpInventory?.initReceived === false;
5066
+ const evidence = { toolActivity: false };
5067
+ for await (const event of sdkEvents(queryFn, req, signal, initial, degraded, evidence)) {
5068
+ const recoverResume = initial.resume !== null && !emitted && !evidence.toolActivity && event.type === "result" && !event.ok && event.reason === "session_start_failed" && event.mcpInventory?.initReceived === false;
5016
5069
  if (recoverResume) {
5017
5070
  if (signal.aborted) return;
5018
5071
  onWarn?.(
@@ -5020,7 +5073,8 @@ async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOpti
5020
5073
  {
5021
5074
  cwd: req.local.cwd,
5022
5075
  reason: "session_start_failed",
5023
- discardedSessionId: initial.resume
5076
+ discardedSessionId: initial.resume,
5077
+ ...evidence.thrown ? { thrown: evidence.thrown } : {}
5024
5078
  }
5025
5079
  );
5026
5080
  const fresh = await freshRequest(req, "session_start_failed", hooks);
@@ -9925,13 +9979,13 @@ var TurnExecution = class {
9925
9979
  } else {
9926
9980
  const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
9927
9981
  let preparingStarted = false;
9928
- let preparingSeq = null;
9982
+ let preparing = null;
9929
9983
  const reportPreparing = (phase) => {
9930
- if (preparingSeq === null) preparingSeq = this.nextSeq();
9931
- const seq = preparingSeq;
9984
+ preparing ??= { id: `prepare:${randomUUID3()}`, seq: this.nextSeq() };
9985
+ const { id, seq } = preparing;
9932
9986
  void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, {
9933
9987
  turnId,
9934
- toolUseId: `prepare:${turnId}`,
9988
+ toolUseId: id,
9935
9989
  toolName: PREPARING_TOOL_NAME,
9936
9990
  summary: "",
9937
9991
  phase,
@@ -11477,6 +11531,7 @@ var CompanionSupervisor = class {
11477
11531
  const api = new CabaneApi({
11478
11532
  baseUrl: this.config.baseUrl,
11479
11533
  token: credential,
11534
+ ...this.config.deviceToken ? { deviceToken: this.config.deviceToken } : {},
11480
11535
  outbox,
11481
11536
  log: agentLog
11482
11537
  });
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
  },
@@ -3781,11 +3783,14 @@ var TurnPump = class {
3781
3783
  },
3782
3784
  onToolActivity: async (evt) => {
3783
3785
  if (opts.signal.aborted) return;
3784
- const existing = this.toolSeqByUseId.get(evt.toolUseId);
3785
- const seq = existing ?? opts.nextSeq();
3786
- if (existing === void 0) this.toolSeqByUseId.set(evt.toolUseId, seq);
3786
+ let call = this.toolCallByProviderId.get(evt.toolUseId);
3787
+ if (evt.phase === "start" || call === void 0) {
3788
+ call = { persistedId: crypto.randomUUID(), seq: opts.nextSeq() };
3789
+ this.toolCallByProviderId.set(evt.toolUseId, call);
3790
+ }
3791
+ const { persistedId, seq } = call;
3787
3792
  await opts.commit.reportTool({
3788
- toolUseId: evt.toolUseId,
3793
+ toolUseId: persistedId,
3789
3794
  toolName: evt.toolName,
3790
3795
  summary: evt.summary,
3791
3796
  input: evt.input,
@@ -3822,9 +3827,19 @@ var TurnPump = class {
3822
3827
  // The turn's final reply text, captured for the dashboard feed / logging.
3823
3828
  finalReplyBody = null;
3824
3829
  emittedFinalSource = "none";
3825
- // CT11: each tool's persisted seq so the `done`/`error` frame republishes the
3826
- // same seq the `start` row got (the host's upsert keeps the original seq).
3827
- toolSeqByUseId = /* @__PURE__ */ new Map();
3830
+ // CT1383: provider tool id → the persisted identity of its CURRENT call. The
3831
+ // provider's id is only a lookup key: it is unique within one adapter
3832
+ // invocation at best (Codex restarts at `item_0` on every invocation, and a
3833
+ // resumed turn keeps its turn id), so the row and the outbox entry are keyed
3834
+ // on what the pump mints — a host id for the host's `(turn_id, tool_use_id)`
3835
+ // upsert, and a seq for the outbox's `(turnId, seq)` file. Both are minted
3836
+ // together; minting only the id would let two calls overwrite each other's
3837
+ // queued body.
3838
+ //
3839
+ // In memory on purpose: it dies with the process, and so does the runtime.
3840
+ // After a crash no in-flight `done` awaits a pre-crash `start` — the resumed
3841
+ // invocation starts its calls afresh — so there is nothing to recover.
3842
+ toolCallByProviderId = /* @__PURE__ */ new Map();
3828
3843
  // The callback consumer the classification (via `sinkEmitter`) — and the
3829
3844
  // frozen committer test — drive. Each callback applies the choreography and
3830
3845
  // commits through the injected sink.
@@ -4219,11 +4234,7 @@ async function* decodeSdkStream(iter, ctx) {
4219
4234
  let usage;
4220
4235
  let lastRequestContextTokens;
4221
4236
  let resolvedModel;
4222
- let sawRejectedLimit = false;
4223
- let rateLimitResetIso;
4224
- let rateLimitType;
4225
- let authError;
4226
- let lastAssistantError;
4237
+ const signals = { sawRejectedLimit: false };
4227
4238
  try {
4228
4239
  for await (const msg of iter) {
4229
4240
  if (ctx.signal.aborted) return;
@@ -4246,12 +4257,14 @@ async function* decodeSdkStream(iter, ctx) {
4246
4257
  };
4247
4258
  }
4248
4259
  } else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
4249
- resultReason = "session_start_failed";
4260
+ if (preInitToolActivity(msg) && ctx.startEvidence) ctx.startEvidence.toolActivity = true;
4261
+ const failure = classifyPreInitFrame(msg, signals);
4262
+ resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
4250
4263
  break;
4251
4264
  } else if (msg.type === "assistant") {
4252
4265
  const assistantErr = msg.error;
4253
4266
  if (typeof assistantErr === "string" && assistantErr.length > 0) {
4254
- lastAssistantError = assistantErr;
4267
+ signals.lastAssistantError = assistantErr;
4255
4268
  }
4256
4269
  const reqContext = readRequestContextTokens(msg);
4257
4270
  if (reqContext !== void 0) lastRequestContextTokens = reqContext;
@@ -4263,13 +4276,13 @@ async function* decodeSdkStream(iter, ctx) {
4263
4276
  } else if (msg.type === "rate_limit_event") {
4264
4277
  const info = msg.rate_limit_info;
4265
4278
  if (info?.status === "rejected") {
4266
- sawRejectedLimit = true;
4267
- rateLimitResetIso = resetsAtToIso(info.resetsAt) ?? rateLimitResetIso;
4268
- if (typeof info.rateLimitType === "string") rateLimitType = info.rateLimitType;
4279
+ signals.sawRejectedLimit = true;
4280
+ signals.rateLimitResetIso = resetsAtToIso(info.resetsAt) ?? signals.rateLimitResetIso;
4281
+ if (typeof info.rateLimitType === "string") signals.rateLimitType = info.rateLimitType;
4269
4282
  }
4270
4283
  } else if (msg.type === "auth_status") {
4271
4284
  const err = msg.error;
4272
- if (typeof err === "string" && err.length > 0) authError = err;
4285
+ if (typeof err === "string" && err.length > 0) signals.authError = err;
4273
4286
  } else if (msg.type === "result") {
4274
4287
  const origin = msg.origin;
4275
4288
  if (origin?.kind === "task-notification") continue;
@@ -4286,16 +4299,7 @@ async function* decodeSdkStream(iter, ctx) {
4286
4299
  ok = true;
4287
4300
  } else {
4288
4301
  const resultText = msg.result ?? "";
4289
- const terminalReason = msg.terminal_reason;
4290
- const errors = msg.errors;
4291
- const errorText = [resultText, ...Array.isArray(errors) ? errors.map(String) : []].join(
4292
- " "
4293
- );
4294
- const rejectedIsCap = sawRejectedLimit && (rateLimitResetIso !== void 0 || isSubscriptionWindow(rateLimitType));
4295
- const failure = rejectedIsCap || terminalReason === "blocking_limit" ? {
4296
- kind: "usage_capped",
4297
- ...rateLimitResetIso ? { resetsAt: rateLimitResetIso } : {}
4298
- } : classifyAssistantError(lastAssistantError) ?? classifyErrorText([authError, errorText].filter(Boolean).join(" "));
4302
+ const failure = classifyResultFailure(msg, signals);
4299
4303
  resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
4300
4304
  ok = false;
4301
4305
  }
@@ -4304,16 +4308,25 @@ async function* decodeSdkStream(iter, ctx) {
4304
4308
  }
4305
4309
  } catch (err) {
4306
4310
  if (ctx.signal.aborted) throw err;
4307
- const failure = classifyErrorText(err instanceof Error ? err.message : String(err));
4308
- if (!failure) throw err;
4311
+ const message = err instanceof Error ? err.message : String(err);
4312
+ const failure = mcpInventory.initReceived ? classifyErrorText(message) : classifyFailure(signals, { text: message });
4313
+ if (!failure && (mcpInventory.initReceived || ctx.resumedSessionId === null)) throw err;
4309
4314
  ok = false;
4310
- resultReason = encodeFailureReason(failure);
4315
+ if (failure) {
4316
+ resultReason = encodeFailureReason(failure);
4317
+ } else {
4318
+ resultReason = "session_start_failed";
4319
+ if (ctx.startEvidence) ctx.startEvidence.thrown = message.slice(0, 200);
4320
+ }
4311
4321
  sawResult = true;
4312
4322
  }
4313
4323
  if (ctx.signal.aborted) return;
4314
4324
  if (!workspaceProven) {
4315
4325
  ok = false;
4316
- resultReason ??= "session_start_failed";
4326
+ if (resultReason === void 0) {
4327
+ const failure = classifyFailure(signals, { text: "" });
4328
+ resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
4329
+ }
4317
4330
  }
4318
4331
  const heldFailure = !ok ? classifyRuntimeNoticeText(buffer.held) : null;
4319
4332
  const terminalFailure = !ok ? decodeFailureReason(resultReason) : null;
@@ -4348,6 +4361,42 @@ function provesWorkspaceTools(msg) {
4348
4361
  const tools = msg.tools;
4349
4362
  return Array.isArray(tools) && tools.includes("mcp__cabane__query");
4350
4363
  }
4364
+ function classifyFailure(s, frame) {
4365
+ const rejectedIsCap = s.sawRejectedLimit && (s.rateLimitResetIso !== void 0 || isSubscriptionWindow(s.rateLimitType));
4366
+ if (rejectedIsCap || frame.terminalReason === "blocking_limit")
4367
+ return {
4368
+ kind: "usage_capped",
4369
+ ...s.rateLimitResetIso ? { resetsAt: s.rateLimitResetIso } : {}
4370
+ };
4371
+ return classifyAssistantError(s.lastAssistantError) ?? classifyErrorText([s.authError, frame.text].filter(Boolean).join(" "));
4372
+ }
4373
+ function classifyResultFailure(msg, s) {
4374
+ const resultText = msg.result ?? "";
4375
+ const terminalReason = msg.terminal_reason;
4376
+ const errors = msg.errors;
4377
+ const text = [resultText, ...Array.isArray(errors) ? errors.map(String) : []].join(" ");
4378
+ return classifyFailure(s, { terminalReason, text });
4379
+ }
4380
+ function classifyPreInitFrame(msg, s) {
4381
+ if (msg.type === "result") {
4382
+ const isError = msg.is_error === true;
4383
+ if (msg.subtype === "success" && !isError) return classifyFailure(s, { text: "" });
4384
+ return classifyResultFailure(msg, s);
4385
+ }
4386
+ if (msg.type !== "assistant") return classifyFailure(s, { text: "" });
4387
+ const error = msg.error;
4388
+ const signals = typeof error === "string" && error.length > 0 ? { ...s, lastAssistantError: error } : s;
4389
+ const text = contentBlocks(msg).flatMap((b) => b.type === "text" && typeof b.text === "string" ? [b.text] : []).join(" ");
4390
+ return classifyFailure(signals, { text });
4391
+ }
4392
+ function preInitToolActivity(msg) {
4393
+ const kind = msg.type === "assistant" ? "tool_use" : msg.type === "user" ? "tool_result" : null;
4394
+ return kind !== null && contentBlocks(msg).some((b) => b.type === kind);
4395
+ }
4396
+ function contentBlocks(msg) {
4397
+ const content = msg.message?.content;
4398
+ return Array.isArray(content) ? content : [];
4399
+ }
4351
4400
  function isSubscriptionWindow(value) {
4352
4401
  return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
4353
4402
  }
@@ -4407,15 +4456,18 @@ function attachAbortController(options, signal) {
4407
4456
  else signal.addEventListener("abort", () => sdkAbort.abort(), { once: true });
4408
4457
  options.abortController = sdkAbort;
4409
4458
  }
4410
- function sdkEvents(queryFn, req, signal, built, degraded) {
4459
+ function sdkEvents(queryFn, req, signal, built, degraded, startEvidence) {
4411
4460
  attachAbortController(built.options, signal);
4412
4461
  const prompt = buildQueryPrompt(req);
4413
- const iter = queryFn({ prompt, options: built.options });
4462
+ const iter = {
4463
+ [Symbol.asyncIterator]: () => queryFn({ prompt, options: built.options })[Symbol.asyncIterator]()
4464
+ };
4414
4465
  return decodeSdkStream(iter, {
4415
4466
  signal,
4416
4467
  cwd: req.local.cwd,
4417
4468
  resumedSessionId: built.resume,
4418
- degraded
4469
+ degraded,
4470
+ ...startEvidence ? { startEvidence } : {}
4419
4471
  });
4420
4472
  }
4421
4473
  async function freshRequest(req, reason, hooks) {
@@ -4424,8 +4476,9 @@ async function freshRequest(req, reason, hooks) {
4424
4476
  }
4425
4477
  async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOptions, onWarn, degraded, hooks) {
4426
4478
  let emitted = false;
4427
- for await (const event of sdkEvents(queryFn, req, signal, initial, degraded)) {
4428
- const recoverResume = initial.resume !== null && !emitted && event.type === "result" && !event.ok && event.reason === "session_start_failed" && event.mcpInventory?.initReceived === false;
4479
+ const evidence = { toolActivity: false };
4480
+ for await (const event of sdkEvents(queryFn, req, signal, initial, degraded, evidence)) {
4481
+ const recoverResume = initial.resume !== null && !emitted && !evidence.toolActivity && event.type === "result" && !event.ok && event.reason === "session_start_failed" && event.mcpInventory?.initReceived === false;
4429
4482
  if (recoverResume) {
4430
4483
  if (signal.aborted) return;
4431
4484
  onWarn?.(
@@ -4433,7 +4486,8 @@ async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOpti
4433
4486
  {
4434
4487
  cwd: req.local.cwd,
4435
4488
  reason: "session_start_failed",
4436
- discardedSessionId: initial.resume
4489
+ discardedSessionId: initial.resume,
4490
+ ...evidence.thrown ? { thrown: evidence.thrown } : {}
4437
4491
  }
4438
4492
  );
4439
4493
  const fresh = await freshRequest(req, "session_start_failed", hooks);
@@ -9348,13 +9402,13 @@ var TurnExecution = class {
9348
9402
  } else {
9349
9403
  const delayMs = this.opts.preparingRowDelayMs ?? DEFAULT_PREPARING_ROW_DELAY_MS;
9350
9404
  let preparingStarted = false;
9351
- let preparingSeq = null;
9405
+ let preparing = null;
9352
9406
  const reportPreparing = (phase) => {
9353
- if (preparingSeq === null) preparingSeq = this.nextSeq();
9354
- const seq = preparingSeq;
9407
+ preparing ??= { id: `prepare:${randomUUID3()}`, seq: this.nextSeq() };
9408
+ const { id, seq } = preparing;
9355
9409
  void this.opts.api.reportActivity(workspaceId, payload.conversationId, payload.agentId, {
9356
9410
  turnId,
9357
- toolUseId: `prepare:${turnId}`,
9411
+ toolUseId: id,
9358
9412
  toolName: PREPARING_TOOL_NAME,
9359
9413
  summary: "",
9360
9414
  phase,
@@ -10900,6 +10954,7 @@ var CompanionSupervisor = class {
10900
10954
  const api = new CabaneApi({
10901
10955
  baseUrl: this.config.baseUrl,
10902
10956
  token: credential,
10957
+ ...this.config.deviceToken ? { deviceToken: this.config.deviceToken } : {},
10903
10958
  outbox,
10904
10959
  log: agentLog
10905
10960
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.118",
3
+ "version": "0.6.120",
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",