@autohq/cli 0.1.449 → 0.1.451

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/index.js CHANGED
@@ -49211,7 +49211,7 @@ var init_package = __esm({
49211
49211
  "package.json"() {
49212
49212
  package_default = {
49213
49213
  name: "@autohq/cli",
49214
- version: "0.1.449",
49214
+ version: "0.1.451",
49215
49215
  license: "SEE LICENSE IN README.md",
49216
49216
  publishConfig: {
49217
49217
  access: "public"
@@ -61297,8 +61297,7 @@ var AgentBridgeOutputBuffer = class {
61297
61297
  await this.emitUiMessagePartSnapshots(context, projection.chunk, {
61298
61298
  coveredSeq: this.outputSeq
61299
61299
  });
61300
- const toolEntry = legacyToolEntry(projection.chunk);
61301
- if (toolEntry) {
61300
+ for (const toolEntry of legacyToolEntries(projection.chunk)) {
61302
61301
  await this.enqueueProjectionAndDrain(context, {
61303
61302
  type: "entry",
61304
61303
  entry: toolEntry
@@ -61777,73 +61776,112 @@ var UiMessageAssembler = class {
61777
61776
  function isTerminalUiMessageChunk(chunk) {
61778
61777
  return chunk.type === "finish" || chunk.type === "error" || chunk.type === "abort";
61779
61778
  }
61780
- function legacyToolEntry(chunk) {
61779
+ function legacyToolEntries(chunk) {
61781
61780
  switch (chunk.type) {
61782
61781
  case "tool-input-available":
61783
61782
  if (chunk.toolName === ASK_USER_QUESTION_TOOL_NAME) {
61784
- return null;
61783
+ return [];
61785
61784
  }
61786
- return {
61787
- role: "assistant",
61788
- kind: "tool_call",
61789
- content: {
61790
- parts: [
61791
- {
61792
- type: "tool_call",
61793
- toolCallId: legacyToolCallId(chunk.toolCallId),
61794
- name: chunk.toolName,
61795
- input: toJsonValue(chunk.input)
61796
- }
61797
- ]
61785
+ return [
61786
+ {
61787
+ role: "assistant",
61788
+ kind: "tool_call",
61789
+ content: {
61790
+ parts: [
61791
+ {
61792
+ type: "tool_call",
61793
+ toolCallId: legacyToolCallId(chunk.toolCallId),
61794
+ name: chunk.toolName,
61795
+ input: toJsonValue(chunk.input)
61796
+ }
61797
+ ]
61798
+ }
61798
61799
  }
61799
- };
61800
+ ];
61801
+ case "tool-input-error":
61802
+ return [
61803
+ {
61804
+ role: "assistant",
61805
+ kind: "tool_call",
61806
+ content: {
61807
+ parts: [
61808
+ {
61809
+ type: "tool_call",
61810
+ toolCallId: legacyToolCallId(chunk.toolCallId),
61811
+ name: chunk.toolName,
61812
+ input: toJsonValue(chunk.input)
61813
+ }
61814
+ ]
61815
+ }
61816
+ },
61817
+ {
61818
+ role: "tool",
61819
+ kind: "tool_result",
61820
+ content: {
61821
+ parts: [
61822
+ {
61823
+ type: "tool_result",
61824
+ toolUseId: legacyToolCallId(chunk.toolCallId),
61825
+ output: chunk.errorText,
61826
+ isError: true
61827
+ }
61828
+ ]
61829
+ }
61830
+ }
61831
+ ];
61800
61832
  case "tool-output-available":
61801
- return {
61802
- role: "tool",
61803
- kind: "tool_result",
61804
- content: {
61805
- parts: [
61806
- {
61807
- type: "tool_result",
61808
- toolUseId: legacyToolCallId(chunk.toolCallId),
61809
- output: toJsonValue(chunk.output),
61810
- isError: false
61811
- }
61812
- ]
61833
+ return [
61834
+ {
61835
+ role: "tool",
61836
+ kind: "tool_result",
61837
+ content: {
61838
+ parts: [
61839
+ {
61840
+ type: "tool_result",
61841
+ toolUseId: legacyToolCallId(chunk.toolCallId),
61842
+ output: toJsonValue(chunk.output),
61843
+ isError: false
61844
+ }
61845
+ ]
61846
+ }
61813
61847
  }
61814
- };
61848
+ ];
61815
61849
  case "tool-output-error":
61816
- return {
61817
- role: "tool",
61818
- kind: "tool_result",
61819
- content: {
61820
- parts: [
61821
- {
61822
- type: "tool_result",
61823
- toolUseId: legacyToolCallId(chunk.toolCallId),
61824
- output: errorOutput(chunk.errorText),
61825
- isError: true
61826
- }
61827
- ]
61850
+ return [
61851
+ {
61852
+ role: "tool",
61853
+ kind: "tool_result",
61854
+ content: {
61855
+ parts: [
61856
+ {
61857
+ type: "tool_result",
61858
+ toolUseId: legacyToolCallId(chunk.toolCallId),
61859
+ output: errorOutput(chunk.errorText),
61860
+ isError: true
61861
+ }
61862
+ ]
61863
+ }
61828
61864
  }
61829
- };
61865
+ ];
61830
61866
  case "tool-output-denied":
61831
- return {
61832
- role: "tool",
61833
- kind: "tool_result",
61834
- content: {
61835
- parts: [
61836
- {
61837
- type: "tool_result",
61838
- toolUseId: legacyToolCallId(chunk.toolCallId),
61839
- output: { denied: true },
61840
- isError: true
61841
- }
61842
- ]
61867
+ return [
61868
+ {
61869
+ role: "tool",
61870
+ kind: "tool_result",
61871
+ content: {
61872
+ parts: [
61873
+ {
61874
+ type: "tool_result",
61875
+ toolUseId: legacyToolCallId(chunk.toolCallId),
61876
+ output: { denied: true },
61877
+ isError: true
61878
+ }
61879
+ ]
61880
+ }
61843
61881
  }
61844
- };
61882
+ ];
61845
61883
  default:
61846
- return null;
61884
+ return [];
61847
61885
  }
61848
61886
  }
61849
61887
  function legacyToolCallId(toolCallId) {
@@ -62052,6 +62090,10 @@ function jsonRecordString2(value, key) {
62052
62090
  // src/commands/agent-bridge/harness/claude-code/projector.ts
62053
62091
  init_src();
62054
62092
  var ClaudeCodeProjector = class {
62093
+ constructor(input = {}) {
62094
+ this.input = input;
62095
+ }
62096
+ input;
62055
62097
  currentMessageId = null;
62056
62098
  // The turn's UI message id. Claude emits a `message_start` per assistant
62057
62099
  // message, but a tool-using turn spans several (message A with the tool call,
@@ -62070,13 +62112,17 @@ var ClaudeCodeProjector = class {
62070
62112
  // real turn failure (FRA-3548). Cleared on every result and on the next
62071
62113
  // message_start so a stale marker can never reclassify a later failure.
62072
62114
  interruptMarkerSeen = false;
62073
- project(message) {
62115
+ project(message, context = {}) {
62074
62116
  if (isClaudeCodeInterruptMarker(message)) {
62075
62117
  this.interruptMarkerSeen = true;
62076
62118
  }
62077
62119
  if (message.type === "stream_event") {
62078
62120
  return this.projectStreamEvent(message);
62079
62121
  }
62122
+ const interruptContext = {
62123
+ marker: this.interruptMarkerSeen,
62124
+ bridge: context.bridgeInterruptSettling === true
62125
+ };
62080
62126
  const parsed = parseClaudeCodeStreamRecord(message);
62081
62127
  const snapshotMessageId = assistantSnapshotMessageId(message);
62082
62128
  const suppressStreamed = snapshotMessageId !== null && this.streamedMessageIds.has(snapshotMessageId);
@@ -62088,18 +62134,28 @@ var ClaudeCodeProjector = class {
62088
62134
  return [];
62089
62135
  }
62090
62136
  return conversationProjectionToUiChunks(entry, {
62091
- interrupted: this.interruptMarkerSeen
62137
+ interruptContext,
62138
+ emitDiagnostic: this.input.writeOutput
62092
62139
  });
62093
62140
  });
62094
62141
  if (parsed.result) {
62095
- outputs.push(...this.endActiveParts(), ...this.finishActiveToolInputs());
62096
- this.currentMessageId = null;
62097
- this.activeResponseMessageId = null;
62142
+ const interrupted = this.isInterruptCancellation(
62143
+ parsed.result,
62144
+ interruptContext
62145
+ );
62146
+ const failActiveInputs = parsed.result.isError && !interrupted;
62098
62147
  outputs.push(
62099
- projectClaudeCodeResult(parsed.result, {
62100
- interrupted: this.isInterruptCancellation(parsed.result)
62101
- })
62148
+ ...this.endActiveParts(),
62149
+ ...failActiveInputs ? this.failActiveToolInputs() : this.finishActiveToolInputs()
62102
62150
  );
62151
+ this.currentMessageId = null;
62152
+ this.activeResponseMessageId = null;
62153
+ if (interrupted) {
62154
+ this.input.writeOutput?.(
62155
+ `agent_bridge_claude_result_interrupt_cancellation gate=${interruptGate(interruptContext)}`
62156
+ );
62157
+ }
62158
+ outputs.push(projectClaudeCodeResult(parsed.result, { interrupted }));
62103
62159
  this.interruptMarkerSeen = false;
62104
62160
  }
62105
62161
  return outputs;
@@ -62109,13 +62165,16 @@ var ClaudeCodeProjector = class {
62109
62165
  // surfaces as a diagnostic-only `error_during_execution`. Treating that as a
62110
62166
  // turn failure fed the FRA-3548 redelivery loop (the bridge redelivered a
62111
62167
  // message the SDK was actively answering), so a failed result bracketed by
62112
- // the SDK's own interrupt marker is classified as a cancellation instead.
62113
- isInterruptCancellation(result) {
62114
- return result.isError && this.interruptMarkerSeen && isClaudeCodeEdeDiagnosticOnlyError(result.errorMessage);
62168
+ // interrupt context — the SDK's own marker, or the session's knowledge that
62169
+ // a bridge interrupt is settling (which covers a missing or late marker)
62170
+ // is classified as a cancellation instead.
62171
+ isInterruptCancellation(result, context) {
62172
+ return result.isError && (context.marker || context.bridge) && isClaudeCodeEdeDiagnosticOnlyError(result.errorMessage);
62115
62173
  }
62116
62174
  flushPendingAssistantMessages() {
62117
62175
  const outputs = [
62118
62176
  ...this.endActiveParts(),
62177
+ ...this.failActiveToolInputs(),
62119
62178
  uiChunk({ type: "finish", finishReason: "stop" })
62120
62179
  ];
62121
62180
  this.currentMessageId = null;
@@ -62219,6 +62278,23 @@ var ClaudeCodeProjector = class {
62219
62278
  (index) => this.finishActiveToolInput(index)
62220
62279
  );
62221
62280
  }
62281
+ // Settle every still-active tool input as an error part that preserves the
62282
+ // partial input text. Used when the turn ends in an error before the tool
62283
+ // block completed: the tool was never dispatched, and persisting whatever
62284
+ // input streamed keeps the transcript diagnosable.
62285
+ failActiveToolInputs() {
62286
+ const outputs = [...this.activeToolInputs.values()].map(
62287
+ (active) => uiChunk({
62288
+ type: "tool-input-error",
62289
+ toolCallId: active.toolCallId,
62290
+ toolName: active.toolName,
62291
+ input: partialToolInput(active),
62292
+ errorText: truncatedToolInputErrorText(active)
62293
+ })
62294
+ );
62295
+ this.activeToolInputs.clear();
62296
+ return outputs;
62297
+ }
62222
62298
  // A never-streamed assistant snapshot (an SDK-internal stream retry can
62223
62299
  // reissue the in-flight message under a fresh id, so suppression misses it;
62224
62300
  // pure snapshot delivery has no stream at all). Its content must respect the
@@ -62232,7 +62308,8 @@ var ClaudeCodeProjector = class {
62232
62308
  const messageChunks = this.assistantSnapshotMessageChunks(projections);
62233
62309
  const rest = projections.filter((projection) => !isAssistantMessageOrToolCall(projection)).flatMap(
62234
62310
  (projection) => conversationProjectionToUiChunks(projection, {
62235
- interrupted: this.interruptMarkerSeen
62311
+ interruptContext: { marker: this.interruptMarkerSeen, bridge: false },
62312
+ emitDiagnostic: this.input.writeOutput
62236
62313
  })
62237
62314
  );
62238
62315
  const last = messageChunks.at(-1);
@@ -62394,6 +62471,21 @@ function completeToolInput(input) {
62394
62471
  }
62395
62472
  return input.initialInput ?? {};
62396
62473
  }
62474
+ function partialToolInput(input) {
62475
+ const inputText = input.inputText.trim();
62476
+ if (inputText.length === 0) {
62477
+ return input.initialInput ?? {};
62478
+ }
62479
+ try {
62480
+ return JSON.parse(inputText);
62481
+ } catch {
62482
+ return input.inputText;
62483
+ }
62484
+ }
62485
+ function truncatedToolInputErrorText(input) {
62486
+ const bytes = Buffer.byteLength(input.inputText, "utf8");
62487
+ return `Tool input streaming was interrupted before the call completed; the tool was never dispatched (${bytes.toString()} bytes of partial input preserved).`;
62488
+ }
62397
62489
  function sourceChunkFromDelta(delta, index) {
62398
62490
  if (delta.type !== "citations_delta") {
62399
62491
  return null;
@@ -62456,7 +62548,7 @@ function textContentFromDelta(delta) {
62456
62548
  return null;
62457
62549
  }
62458
62550
  }
62459
- function conversationProjectionToUiChunks(projection, options = { interrupted: false }) {
62551
+ function conversationProjectionToUiChunks(projection, options) {
62460
62552
  if (projection.role === "assistant" && projection.kind === "message") {
62461
62553
  return messageContentChunks(projection);
62462
62554
  }
@@ -62477,15 +62569,12 @@ function conversationProjectionToUiChunks(projection, options = { interrupted: f
62477
62569
  })
62478
62570
  ];
62479
62571
  }
62480
- const remapped = typeof part.output === "string" ? remapClaudeCodeInterruptToolResultErrorText(part.output, {
62481
- interrupted: options.interrupted
62482
- }) : null;
62483
62572
  return [
62484
- uiChunk({
62485
- type: "tool-output-error",
62486
- toolCallId: part.toolUseId ?? UNKNOWN_MESSAGE_ID,
62487
- errorText: remapped ?? JSON.stringify(part.output)
62488
- })
62573
+ toolResultErrorChunk(
62574
+ part.toolUseId ?? UNKNOWN_MESSAGE_ID,
62575
+ part.output,
62576
+ options
62577
+ )
62489
62578
  ];
62490
62579
  });
62491
62580
  }
@@ -62529,6 +62618,32 @@ function conversationProjectionToUiChunks(projection, options = { interrupted: f
62529
62618
  }
62530
62619
  ];
62531
62620
  }
62621
+ function toolResultErrorChunk(toolCallId, output, options) {
62622
+ const context = options.interruptContext;
62623
+ const remapped = typeof output === "string" ? remapClaudeCodeInterruptToolResultErrorText(output, {
62624
+ interrupted: context.marker || context.bridge
62625
+ }) : null;
62626
+ if (remapped !== null) {
62627
+ options.emitDiagnostic?.(
62628
+ `agent_bridge_claude_tool_cancellation_remapped tool_use_id=${toolCallId} gate=${interruptGate(context)}`
62629
+ );
62630
+ } else if (typeof output === "string" && isClaudeCodePermissionDenialText(output)) {
62631
+ options.emitDiagnostic?.(
62632
+ `agent_bridge_claude_permission_denial_passthrough tool_use_id=${toolCallId}`
62633
+ );
62634
+ }
62635
+ return uiChunk({
62636
+ type: "tool-output-error",
62637
+ toolCallId,
62638
+ errorText: remapped ?? JSON.stringify(output)
62639
+ });
62640
+ }
62641
+ function interruptGate(context) {
62642
+ if (context.marker && context.bridge) {
62643
+ return "marker+bridge";
62644
+ }
62645
+ return context.marker ? "marker" : "bridge";
62646
+ }
62532
62647
  function isAssistantMessageOrToolCall(projection) {
62533
62648
  return projection.role === "assistant" && (projection.kind === "message" || projection.kind === "tool_call");
62534
62649
  }
@@ -62807,6 +62922,7 @@ function parseReadStateRecord(raw) {
62807
62922
  }
62808
62923
 
62809
62924
  // src/commands/agent-bridge/harness/claude-code/session.ts
62925
+ init_src();
62810
62926
  import {
62811
62927
  startup as startupClaudeAgent
62812
62928
  } from "@anthropic-ai/claude-agent-sdk";
@@ -63001,7 +63117,7 @@ function withClaudeStderrDiagnosticsPointer(error51, capturedStderr) {
63001
63117
 
63002
63118
  // src/commands/agent-bridge/harness/claude-code/session.ts
63003
63119
  var CLAUDE_AGENT_STARTUP_TIMEOUT_MS = 3e4;
63004
- var CLAUDE_INTERRUPT_SETTLE_TIMEOUT_MS = 1e4;
63120
+ var CLAUDE_TOOL_BOUNDARY_WAIT_TIMEOUT_MS = 12e4;
63005
63121
  var CLAUDE_INTERRUPT_ACK_TIMEOUT_MS = 1e4;
63006
63122
  var CLAUDE_MCP_REGISTRATION_TIMEOUT_MS = 3e4;
63007
63123
  var CLAUDE_MCP_REGISTRATION_POLL_INTERVAL_MS = 100;
@@ -63077,16 +63193,33 @@ var ClaudeAgentBridgeSessionImpl = class {
63077
63193
  // flushed command id.
63078
63194
  openTurnCommandIds = [];
63079
63195
  // tool_use ids the assistant has emitted whose tool_result has not yet been
63080
- // observed. A non-empty set means a tool call is in flight, so an immediate
63081
- // interrupt would append the new user message after an unresolved
63196
+ // observed, with the name/detail needed to describe them in a cancellation
63197
+ // notice. A non-empty map means a tool call is in flight: interrupting now
63198
+ // would destroy its work and record a denial-worded cancellation, and
63199
+ // injecting now would append the new user message after an unresolved
63082
63200
  // stop_reason=tool_use — the invalid transcript that trips Claude Code's
63083
63201
  // ede_diagnostic and auto-restarts the session (FRA-3049).
63084
- pendingToolUseIds = /* @__PURE__ */ new Set();
63085
- // Incremented on every turn-terminal `result`. An interrupt that aborts an
63086
- // in-flight tool call waits for this to advance before injecting, so the
63087
- // message lands only once the interrupted turn has settled its tool_use.
63202
+ pendingToolUses = /* @__PURE__ */ new Map();
63203
+ // Incremented on every turn-terminal `result`; the interrupt-timeout latch
63204
+ // compares against it to tell whether the same wedged turn is still running.
63088
63205
  turnResultCount = 0;
63089
- turnSettlementWaiters = /* @__PURE__ */ new Set();
63206
+ // A steering message is waiting for the in-flight tool call to settle so the
63207
+ // turn can be interrupted at the tool boundary instead of mid-tool. Armed by
63208
+ // sendMessage, fired by the output pump when the pending tool_use set
63209
+ // drains, bounded by the budget timer below.
63210
+ toolBoundaryWait = null;
63211
+ // True from the moment a bridge-initiated interrupt control request fires
63212
+ // until the targeted turn emits its terminal result (or the stream ends).
63213
+ // Reported to the projector on every message so the SDK's denial-worded
63214
+ // cancellation records are classified as platform cancellations even though
63215
+ // the SDK's own interrupt marker arrives after them (probe-verified wire
63216
+ // order, 2026-07-13: tool_result → marker → error_during_execution result).
63217
+ interruptSettling = false;
63218
+ // Tool calls the SDK cancelled while a bridge-initiated interrupt was
63219
+ // settling. Consumed by the deferred flush: the injected message gets a
63220
+ // platform-cancellation notice naming this work so the model re-runs it
63221
+ // instead of treating the denial-worded tool_result as a human rejection.
63222
+ cancelledToolRuns = [];
63090
63223
  // Memoized MCP-registration gate, keyed by the attached query. A runtime
63091
63224
  // restart warms a brand-new session + query, so keying off the query object
63092
63225
  // (rather than a session-lifetime flag) re-arms the gate on every restart:
@@ -63145,18 +63278,34 @@ var ClaudeAgentBridgeSessionImpl = class {
63145
63278
  delivery_mode: mode,
63146
63279
  state: this.state.kind,
63147
63280
  active_turn_count: this.activeTurnCount,
63148
- pending_tool_use_count: this.pendingToolUseIds.size,
63281
+ pending_tool_use_count: this.pendingToolUses.size,
63149
63282
  deferred_count: this.deferredMessages.length
63150
63283
  });
63151
63284
  if (mode === "deferred" && this.hasInterruptibleTurn()) {
63152
63285
  this.deferredMessages.push({ text: message, commandIds });
63153
63286
  this.input.runtimeLogger?.info("agent_bridge_claude_message_deferred", {
63154
63287
  active_turn_count: this.activeTurnCount,
63155
- pending_tool_use_count: this.pendingToolUseIds.size,
63288
+ pending_tool_use_count: this.pendingToolUses.size,
63156
63289
  deferred_count: this.deferredMessages.length
63157
63290
  });
63158
63291
  return;
63159
63292
  }
63293
+ if (this.hasInterruptibleTurn() && this.hasInFlightToolUse()) {
63294
+ this.deferredMessages.push({ text: message, commandIds });
63295
+ this.armToolBoundaryWait();
63296
+ this.input.runtimeLogger?.info(
63297
+ "agent_bridge_claude_message_deferred_tool_boundary",
63298
+ {
63299
+ active_turn_count: this.activeTurnCount,
63300
+ pending_tool_use_count: this.pendingToolUses.size,
63301
+ pending_tools: [...this.pendingToolUses.values()].map(
63302
+ (tool) => tool.name
63303
+ ),
63304
+ deferred_count: this.deferredMessages.length
63305
+ }
63306
+ );
63307
+ return;
63308
+ }
63160
63309
  const interruption = await this.interruptActiveTurnBeforeMessage();
63161
63310
  if (interruption === "defer" && this.hasInterruptibleTurn()) {
63162
63311
  this.deferredMessages.push({ text: message, commandIds });
@@ -63164,7 +63313,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63164
63313
  "agent_bridge_claude_message_deferred_after_interrupt_timeout",
63165
63314
  {
63166
63315
  active_turn_count: this.activeTurnCount,
63167
- pending_tool_use_count: this.pendingToolUseIds.size,
63316
+ pending_tool_use_count: this.pendingToolUses.size,
63168
63317
  deferred_count: this.deferredMessages.length
63169
63318
  }
63170
63319
  );
@@ -63177,7 +63326,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63177
63326
  duration_ms: Date.now() - startedAt,
63178
63327
  state: this.state.kind,
63179
63328
  active_turn_count: this.activeTurnCount,
63180
- pending_tool_use_count: this.pendingToolUseIds.size,
63329
+ pending_tool_use_count: this.pendingToolUses.size,
63181
63330
  deferred_count: this.deferredMessages.length
63182
63331
  });
63183
63332
  }
@@ -63261,8 +63410,10 @@ var ClaudeAgentBridgeSessionImpl = class {
63261
63410
  }
63262
63411
  this.state = { kind: "closed" };
63263
63412
  this.activeTurnCount = 0;
63264
- this.pendingToolUseIds.clear();
63265
- this.resolveTurnSettlement(false);
63413
+ this.pendingToolUses.clear();
63414
+ this.cancelledToolRuns = [];
63415
+ this.interruptSettling = false;
63416
+ this.disarmToolBoundaryWait();
63266
63417
  if (this.deferredMessages.length > 0) {
63267
63418
  this.input.writeOutput?.(
63268
63419
  `agent_bridge_claude_deferred_dropped count=${this.deferredMessages.length} reason=session_closed`
@@ -63353,19 +63504,21 @@ var ClaudeAgentBridgeSessionImpl = class {
63353
63504
  void (async () => {
63354
63505
  try {
63355
63506
  for await (const message of query) {
63507
+ const interruptSettling = this.interruptSettling;
63356
63508
  this.trackToolUseLifecycle(message);
63357
- let meta3;
63509
+ let meta3 = { interruptSettling };
63358
63510
  if (isClaudeAgentTurnResult(message)) {
63359
63511
  this.activeTurnCount = Math.max(0, this.activeTurnCount - 1);
63360
63512
  meta3 = {
63513
+ interruptSettling,
63361
63514
  consumedCommandIds: this.openTurnCommandIds.shift() ?? []
63362
63515
  };
63363
- this.pendingToolUseIds.clear();
63516
+ this.pendingToolUses.clear();
63517
+ this.interruptSettling = false;
63364
63518
  this.turnResultCount += 1;
63365
- this.resolveTurnSettlement(true);
63366
63519
  this.input.runtimeLogger?.info("agent_bridge_claude_turn_result", {
63367
63520
  active_turn_count: this.activeTurnCount,
63368
- pending_tool_use_count: this.pendingToolUseIds.size,
63521
+ pending_tool_use_count: this.pendingToolUses.size,
63369
63522
  turn_result_count: this.turnResultCount,
63370
63523
  deferred_count: this.deferredMessages.length,
63371
63524
  consumed_command_ids: meta3.consumedCommandIds
@@ -63373,6 +63526,9 @@ var ClaudeAgentBridgeSessionImpl = class {
63373
63526
  if (this.activeTurnCount === 0) {
63374
63527
  this.flushDeferredMessages();
63375
63528
  }
63529
+ if (this.deferredMessages.length === 0) {
63530
+ this.cancelledToolRuns = [];
63531
+ }
63376
63532
  }
63377
63533
  const failure = probeClaudeAgentMessage(
63378
63534
  message,
@@ -63397,8 +63553,9 @@ var ClaudeAgentBridgeSessionImpl = class {
63397
63553
  } finally {
63398
63554
  this.activeTurnCount = 0;
63399
63555
  this.openTurnCommandIds.length = 0;
63400
- this.pendingToolUseIds.clear();
63401
- this.resolveTurnSettlement(false);
63556
+ this.pendingToolUses.clear();
63557
+ this.interruptSettling = false;
63558
+ this.disarmToolBoundaryWait();
63402
63559
  this.state = { kind: "closed" };
63403
63560
  this.reportExit();
63404
63561
  }
@@ -63415,7 +63572,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63415
63572
  this.input.runtimeLogger?.info("agent_bridge_claude_message_enqueued", {
63416
63573
  state: this.state.kind,
63417
63574
  active_turn_count: this.activeTurnCount,
63418
- pending_tool_use_count: this.pendingToolUseIds.size,
63575
+ pending_tool_use_count: this.pendingToolUses.size,
63419
63576
  deferred_count: this.deferredMessages.length
63420
63577
  });
63421
63578
  void this.ensureRunningQuery().catch((error51) => {
@@ -63428,75 +63585,105 @@ var ClaudeAgentBridgeSessionImpl = class {
63428
63585
  return this.activeTurnCount > 0 && this.state.kind === "running";
63429
63586
  }
63430
63587
  // True while the assistant has an emitted tool_use with no matching
63431
- // tool_result yet — the window where an immediate interrupt would strand an
63588
+ // tool_result yet — the window where an immediate interrupt would cancel
63589
+ // real tool work, and where an immediate injection would strand an
63432
63590
  // unresolved stop_reason=tool_use ahead of the injected user message.
63433
63591
  hasInFlightToolUse() {
63434
- return this.pendingToolUseIds.size > 0;
63592
+ return this.pendingToolUses.size > 0;
63435
63593
  }
63436
63594
  // Track tool_use/tool_result pairs as they stream so hasInFlightToolUse()
63437
63595
  // reflects the live transcript. A tool_use may appear first as a streaming
63438
63596
  // content block before the full assistant message is available; the matching
63439
63597
  // tool_result closes it; the turn's terminal result clears any remainder.
63598
+ // Two side jobs ride on the same observation point:
63599
+ // - a denial-worded error tool_result arriving while a bridge interrupt is
63600
+ // settling is that interrupt's cancellation of the tool — record it so the
63601
+ // flushed message can tell the model to re-run the cancelled work;
63602
+ // - a tool_result that drains the pending set is the tool boundary an armed
63603
+ // steering message has been waiting for — interrupt the turn now, while no
63604
+ // tool work is at risk.
63440
63605
  trackToolUseLifecycle(message) {
63441
- for (const id of toolUseIds(message)) {
63442
- this.pendingToolUseIds.add(id);
63606
+ for (const entry of toolUseEntries(message)) {
63607
+ this.pendingToolUses.set(entry.id, {
63608
+ name: entry.name,
63609
+ detail: entry.detail
63610
+ });
63611
+ }
63612
+ const hadInFlightToolUse = this.hasInFlightToolUse();
63613
+ for (const result of toolResultEntries(message)) {
63614
+ const pending = this.pendingToolUses.get(result.id);
63615
+ this.pendingToolUses.delete(result.id);
63616
+ if (pending && this.interruptSettling && result.isError && result.text !== null && isClaudeCodePermissionDenialText(result.text)) {
63617
+ this.cancelledToolRuns.push(pending);
63618
+ this.input.writeOutput?.(
63619
+ `agent_bridge_claude_tool_cancellation_observed tool=${pending.name}`
63620
+ );
63621
+ }
63443
63622
  }
63444
- for (const id of toolResultIds(message)) {
63445
- this.pendingToolUseIds.delete(id);
63623
+ if (hadInFlightToolUse && !this.hasInFlightToolUse()) {
63624
+ this.fireToolBoundaryInterrupt();
63446
63625
  }
63447
63626
  }
63448
- // Resolve once the interrupted turn emits its terminal `result` (turnResultCount
63449
- // advances past the pre-interrupt baseline), bounded so a tool that never
63450
- // returns after interrupt cannot wedge delivery. The caller reads the
63451
- // outcome: "timeout" means the turn is still running and the message must
63452
- // defer to the turn boundary rather than inject mid-turn.
63453
- awaitTurnSettlement(baseline, startedAt) {
63454
- if (this.turnResultCount > baseline) {
63455
- return Promise.resolve("ready");
63627
+ // The pending tool_use set just drained mid-turn. If a steering message is
63628
+ // waiting on that boundary, interrupt the turn now: nothing is executing, so
63629
+ // the cancellation costs only the model's remaining reasoning, and the
63630
+ // turn's terminal result flushes the message. Fire-and-forget the flush at
63631
+ // the turn result owns delivery whether or not this interrupt lands.
63632
+ fireToolBoundaryInterrupt() {
63633
+ if (!this.toolBoundaryWait || this.deferredMessages.length === 0 || !this.hasInterruptibleTurn()) {
63634
+ return;
63456
63635
  }
63457
- return new Promise((resolve4) => {
63458
- let done = false;
63459
- const finish = (outcome) => {
63460
- if (done) {
63461
- return;
63462
- }
63463
- done = true;
63464
- clearTimeout(timer);
63465
- this.turnSettlementWaiters.delete(waiter);
63466
- this.input.writeOutput?.(
63467
- `agent_bridge_claude_interrupt_settle_${outcome} duration_ms=${Date.now() - startedAt}`
63468
- );
63469
- resolve4(outcome);
63470
- };
63471
- const waiter = (settled) => finish(settled ? "ready" : "aborted");
63472
- const timer = setTimeout(
63473
- () => finish("timeout"),
63474
- CLAUDE_INTERRUPT_SETTLE_TIMEOUT_MS
63636
+ this.disarmToolBoundaryWait();
63637
+ this.input.writeOutput?.(
63638
+ `agent_bridge_claude_tool_boundary_interrupt deferred_count=${this.deferredMessages.length}`
63639
+ );
63640
+ void this.interruptActiveTurn();
63641
+ }
63642
+ // Bound the tool-boundary wait: past the budget the steering message is
63643
+ // judged more urgent than the in-flight tool, which is cancelled mid-run.
63644
+ // The cancellation is observed by trackToolUseLifecycle and reported to the
63645
+ // model in the flushed message's platform notice.
63646
+ armToolBoundaryWait() {
63647
+ if (this.toolBoundaryWait) {
63648
+ return;
63649
+ }
63650
+ const timer = setTimeout(() => {
63651
+ this.toolBoundaryWait = null;
63652
+ if (!this.hasInterruptibleTurn() || this.deferredMessages.length === 0) {
63653
+ return;
63654
+ }
63655
+ this.input.writeOutput?.(
63656
+ `agent_bridge_claude_tool_boundary_wait_timeout pending_tools=${[
63657
+ ...this.pendingToolUses.values()
63658
+ ].map((tool) => tool.name).join(",")} deferred_count=${this.deferredMessages.length}`
63475
63659
  );
63476
- timer.unref?.();
63477
- this.turnSettlementWaiters.add(waiter);
63478
- });
63660
+ void this.interruptActiveTurn();
63661
+ }, CLAUDE_TOOL_BOUNDARY_WAIT_TIMEOUT_MS);
63662
+ timer.unref?.();
63663
+ this.toolBoundaryWait = { timer };
63479
63664
  }
63480
- resolveTurnSettlement(settled) {
63481
- if (this.turnSettlementWaiters.size === 0) {
63665
+ disarmToolBoundaryWait() {
63666
+ if (!this.toolBoundaryWait) {
63482
63667
  return;
63483
63668
  }
63484
- const waiters = [...this.turnSettlementWaiters];
63485
- this.turnSettlementWaiters.clear();
63486
- for (const waiter of waiters) {
63487
- waiter(settled);
63488
- }
63669
+ clearTimeout(this.toolBoundaryWait.timer);
63670
+ this.toolBoundaryWait = null;
63489
63671
  }
63490
63672
  flushDeferredMessages() {
63491
63673
  if (this.deferredMessages.length === 0) {
63492
63674
  return;
63493
63675
  }
63494
63676
  const pending = this.deferredMessages.splice(0);
63677
+ const cancelledToolRuns = this.cancelledToolRuns.splice(0);
63678
+ this.disarmToolBoundaryWait();
63495
63679
  this.input.writeOutput?.(
63496
- `agent_bridge_claude_deferred_flush count=${pending.length}`
63680
+ `agent_bridge_claude_deferred_flush count=${pending.length} cancelled_tool_count=${cancelledToolRuns.length}`
63497
63681
  );
63682
+ const text = pending.map((entry) => entry.text).join("\n\n");
63498
63683
  this.enqueueUserMessage(
63499
- pending.map((entry) => entry.text).join("\n\n"),
63684
+ cancelledToolRuns.length > 0 ? `${text}
63685
+
63686
+ ${cancelledToolPlatformNotice(cancelledToolRuns)}` : text,
63500
63687
  pending.flatMap((entry) => entry.commandIds)
63501
63688
  );
63502
63689
  }
@@ -63517,11 +63704,10 @@ var ClaudeAgentBridgeSessionImpl = class {
63517
63704
  return "proceed";
63518
63705
  }
63519
63706
  const query = this.state.query;
63520
- const hadInFlightToolUse = this.hasInFlightToolUse();
63521
63707
  const settlementBaseline = this.turnResultCount;
63522
63708
  const startedAt = Date.now();
63523
63709
  this.input.writeOutput?.(
63524
- `agent_bridge_claude_mid_turn_interrupt_started at=${new Date(startedAt).toISOString()} in_flight_tool_use=${hadInFlightToolUse}`
63710
+ `agent_bridge_claude_mid_turn_interrupt_started at=${new Date(startedAt).toISOString()} in_flight_tool_use=${this.hasInFlightToolUse()}`
63525
63711
  );
63526
63712
  const interruptPromise = (async () => {
63527
63713
  const ack = await this.requestInterruptAck(query, startedAt);
@@ -63529,16 +63715,6 @@ var ClaudeAgentBridgeSessionImpl = class {
63529
63715
  this.interruptTimeoutTurn = settlementBaseline;
63530
63716
  return "defer";
63531
63717
  }
63532
- if (hadInFlightToolUse) {
63533
- const settle = await this.awaitTurnSettlement(
63534
- settlementBaseline,
63535
- startedAt
63536
- );
63537
- if (settle === "timeout") {
63538
- this.interruptTimeoutTurn = settlementBaseline;
63539
- return "defer";
63540
- }
63541
- }
63542
63718
  return "proceed";
63543
63719
  })();
63544
63720
  this.interruptInFlight = interruptPromise;
@@ -63575,6 +63751,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63575
63751
  );
63576
63752
  }, CLAUDE_INTERRUPT_ACK_TIMEOUT_MS);
63577
63753
  timer.unref?.();
63754
+ this.interruptSettling = true;
63578
63755
  query.interrupt().then(
63579
63756
  () => {
63580
63757
  if (done) {
@@ -63931,10 +64108,10 @@ function claudeAgentUserMessage(message) {
63931
64108
  function isClaudeAgentTurnResult(message) {
63932
64109
  return message.type === "result";
63933
64110
  }
63934
- function toolUseIds(message) {
64111
+ function toolUseEntries(message) {
63935
64112
  if (message.type === "assistant") {
63936
64113
  return contentBlocks2(message.message.content).flatMap(
63937
- (block2) => block2.type === "tool_use" && typeof block2.id === "string" ? [block2.id] : []
64114
+ (block2) => block2.type === "tool_use" && typeof block2.id === "string" ? [toolUseEntryFromBlock(block2)] : []
63938
64115
  );
63939
64116
  }
63940
64117
  if (message.type !== "stream_event") {
@@ -63945,16 +64122,54 @@ function toolUseIds(message) {
63945
64122
  return [];
63946
64123
  }
63947
64124
  const block = event.content_block;
63948
- return block.type === "tool_use" && typeof block.id === "string" ? [block.id] : [];
64125
+ return block.type === "tool_use" && typeof block.id === "string" ? [toolUseEntryFromBlock(block)] : [];
64126
+ }
64127
+ function toolUseEntryFromBlock(block) {
64128
+ return {
64129
+ id: String(block.id),
64130
+ name: typeof block.name === "string" ? block.name : "unknown_tool",
64131
+ detail: toolUseDetail(block.input)
64132
+ };
64133
+ }
64134
+ function toolUseDetail(input) {
64135
+ if (input === void 0 || input === null) {
64136
+ return "";
64137
+ }
64138
+ let text;
64139
+ try {
64140
+ text = JSON.stringify(input);
64141
+ } catch {
64142
+ return "";
64143
+ }
64144
+ if (!text || text === "{}") {
64145
+ return "";
64146
+ }
64147
+ return text.length > 160 ? `${text.slice(0, 160)}\u2026` : text;
63949
64148
  }
63950
- function toolResultIds(message) {
64149
+ function toolResultEntries(message) {
63951
64150
  if (message.type !== "user") {
63952
64151
  return [];
63953
64152
  }
63954
64153
  return contentBlocks2(message.message.content).flatMap(
63955
- (block) => block.type === "tool_result" && typeof block.tool_use_id === "string" ? [block.tool_use_id] : []
64154
+ (block) => block.type === "tool_result" && typeof block.tool_use_id === "string" ? [
64155
+ {
64156
+ id: block.tool_use_id,
64157
+ isError: block.is_error === true,
64158
+ text: typeof block.content === "string" ? block.content : null
64159
+ }
64160
+ ] : []
63956
64161
  );
63957
64162
  }
64163
+ function cancelledToolPlatformNotice(cancelled) {
64164
+ const lines = cancelled.map(
64165
+ (tool) => `- ${tool.name}${tool.detail ? ` ${tool.detail}` : ""}`
64166
+ );
64167
+ return [
64168
+ "[Auto platform notice] To deliver the message above, the platform cancelled the following in-flight tool call(s) mid-execution:",
64169
+ ...lines,
64170
+ "This was a platform-initiated cancellation for message delivery, NOT a rejection or permission denial by the user \u2014 ignore any tool result saying the user doesn't want to proceed. Re-run or resume that tool work if it is still needed once you have applied the message."
64171
+ ].join("\n");
64172
+ }
63958
64173
  function contentBlocks2(content) {
63959
64174
  if (!Array.isArray(content)) {
63960
64175
  return [];
@@ -63973,6 +64188,9 @@ var ClaudeCodeCommandHandler = class {
63973
64188
  this.input = input;
63974
64189
  this.claudeConfig = input.claude;
63975
64190
  this.outputBuffer = new AgentBridgeOutputBuffer(input);
64191
+ this.projector = new ClaudeCodeProjector({
64192
+ writeOutput: input.writeOutput
64193
+ });
63976
64194
  this.readState = input.readState ? new ClaudeReadStateTracker(input.readState) : null;
63977
64195
  }
63978
64196
  input;
@@ -63994,7 +64212,7 @@ var ClaudeCodeCommandHandler = class {
63994
64212
  inFlightMessageCommands = /* @__PURE__ */ new Map();
63995
64213
  pendingQuestions = /* @__PURE__ */ new Map();
63996
64214
  outputBuffer;
63997
- projector = new ClaudeCodeProjector();
64215
+ projector;
63998
64216
  readState;
63999
64217
  livenessTicker = null;
64000
64218
  // -----------------------------------------------------------------------------
@@ -64375,7 +64593,9 @@ var ClaudeCodeCommandHandler = class {
64375
64593
  this.trackReadState(activeContext, message);
64376
64594
  let projections;
64377
64595
  try {
64378
- projections = this.projector.project(message);
64596
+ projections = this.projector.project(message, {
64597
+ bridgeInterruptSettling: meta3?.interruptSettling === true
64598
+ });
64379
64599
  } catch (error51) {
64380
64600
  this.input.writeOutput?.(
64381
64601
  `agent_bridge_claude_parse_failed error=${error51 instanceof Error ? error51.message : String(error51)}`