@cabane/companion 0.6.118 → 0.6.119

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