@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.
@@ -30825,7 +30825,7 @@ Object.assign(lookup, {
30825
30825
  // package.json
30826
30826
  var package_default = {
30827
30827
  name: "@autohq/cli",
30828
- version: "0.1.449",
30828
+ version: "0.1.451",
30829
30829
  license: "SEE LICENSE IN README.md",
30830
30830
  publishConfig: {
30831
30831
  access: "public"
@@ -70697,8 +70697,7 @@ var AgentBridgeOutputBuffer = class {
70697
70697
  await this.emitUiMessagePartSnapshots(context, projection.chunk, {
70698
70698
  coveredSeq: this.outputSeq
70699
70699
  });
70700
- const toolEntry = legacyToolEntry(projection.chunk);
70701
- if (toolEntry) {
70700
+ for (const toolEntry of legacyToolEntries(projection.chunk)) {
70702
70701
  await this.enqueueProjectionAndDrain(context, {
70703
70702
  type: "entry",
70704
70703
  entry: toolEntry
@@ -71177,73 +71176,112 @@ var UiMessageAssembler = class {
71177
71176
  function isTerminalUiMessageChunk(chunk) {
71178
71177
  return chunk.type === "finish" || chunk.type === "error" || chunk.type === "abort";
71179
71178
  }
71180
- function legacyToolEntry(chunk) {
71179
+ function legacyToolEntries(chunk) {
71181
71180
  switch (chunk.type) {
71182
71181
  case "tool-input-available":
71183
71182
  if (chunk.toolName === ASK_USER_QUESTION_TOOL_NAME) {
71184
- return null;
71183
+ return [];
71185
71184
  }
71186
- return {
71187
- role: "assistant",
71188
- kind: "tool_call",
71189
- content: {
71190
- parts: [
71191
- {
71192
- type: "tool_call",
71193
- toolCallId: legacyToolCallId(chunk.toolCallId),
71194
- name: chunk.toolName,
71195
- input: toJsonValue(chunk.input)
71196
- }
71197
- ]
71185
+ return [
71186
+ {
71187
+ role: "assistant",
71188
+ kind: "tool_call",
71189
+ content: {
71190
+ parts: [
71191
+ {
71192
+ type: "tool_call",
71193
+ toolCallId: legacyToolCallId(chunk.toolCallId),
71194
+ name: chunk.toolName,
71195
+ input: toJsonValue(chunk.input)
71196
+ }
71197
+ ]
71198
+ }
71198
71199
  }
71199
- };
71200
+ ];
71201
+ case "tool-input-error":
71202
+ return [
71203
+ {
71204
+ role: "assistant",
71205
+ kind: "tool_call",
71206
+ content: {
71207
+ parts: [
71208
+ {
71209
+ type: "tool_call",
71210
+ toolCallId: legacyToolCallId(chunk.toolCallId),
71211
+ name: chunk.toolName,
71212
+ input: toJsonValue(chunk.input)
71213
+ }
71214
+ ]
71215
+ }
71216
+ },
71217
+ {
71218
+ role: "tool",
71219
+ kind: "tool_result",
71220
+ content: {
71221
+ parts: [
71222
+ {
71223
+ type: "tool_result",
71224
+ toolUseId: legacyToolCallId(chunk.toolCallId),
71225
+ output: chunk.errorText,
71226
+ isError: true
71227
+ }
71228
+ ]
71229
+ }
71230
+ }
71231
+ ];
71200
71232
  case "tool-output-available":
71201
- return {
71202
- role: "tool",
71203
- kind: "tool_result",
71204
- content: {
71205
- parts: [
71206
- {
71207
- type: "tool_result",
71208
- toolUseId: legacyToolCallId(chunk.toolCallId),
71209
- output: toJsonValue(chunk.output),
71210
- isError: false
71211
- }
71212
- ]
71233
+ return [
71234
+ {
71235
+ role: "tool",
71236
+ kind: "tool_result",
71237
+ content: {
71238
+ parts: [
71239
+ {
71240
+ type: "tool_result",
71241
+ toolUseId: legacyToolCallId(chunk.toolCallId),
71242
+ output: toJsonValue(chunk.output),
71243
+ isError: false
71244
+ }
71245
+ ]
71246
+ }
71213
71247
  }
71214
- };
71248
+ ];
71215
71249
  case "tool-output-error":
71216
- return {
71217
- role: "tool",
71218
- kind: "tool_result",
71219
- content: {
71220
- parts: [
71221
- {
71222
- type: "tool_result",
71223
- toolUseId: legacyToolCallId(chunk.toolCallId),
71224
- output: errorOutput(chunk.errorText),
71225
- isError: true
71226
- }
71227
- ]
71250
+ return [
71251
+ {
71252
+ role: "tool",
71253
+ kind: "tool_result",
71254
+ content: {
71255
+ parts: [
71256
+ {
71257
+ type: "tool_result",
71258
+ toolUseId: legacyToolCallId(chunk.toolCallId),
71259
+ output: errorOutput(chunk.errorText),
71260
+ isError: true
71261
+ }
71262
+ ]
71263
+ }
71228
71264
  }
71229
- };
71265
+ ];
71230
71266
  case "tool-output-denied":
71231
- return {
71232
- role: "tool",
71233
- kind: "tool_result",
71234
- content: {
71235
- parts: [
71236
- {
71237
- type: "tool_result",
71238
- toolUseId: legacyToolCallId(chunk.toolCallId),
71239
- output: { denied: true },
71240
- isError: true
71241
- }
71242
- ]
71267
+ return [
71268
+ {
71269
+ role: "tool",
71270
+ kind: "tool_result",
71271
+ content: {
71272
+ parts: [
71273
+ {
71274
+ type: "tool_result",
71275
+ toolUseId: legacyToolCallId(chunk.toolCallId),
71276
+ output: { denied: true },
71277
+ isError: true
71278
+ }
71279
+ ]
71280
+ }
71243
71281
  }
71244
- };
71282
+ ];
71245
71283
  default:
71246
- return null;
71284
+ return [];
71247
71285
  }
71248
71286
  }
71249
71287
  function legacyToolCallId(toolCallId) {
@@ -71451,6 +71489,10 @@ function jsonRecordString2(value2, key) {
71451
71489
 
71452
71490
  // src/commands/agent-bridge/harness/claude-code/projector.ts
71453
71491
  var ClaudeCodeProjector = class {
71492
+ constructor(input = {}) {
71493
+ this.input = input;
71494
+ }
71495
+ input;
71454
71496
  currentMessageId = null;
71455
71497
  // The turn's UI message id. Claude emits a `message_start` per assistant
71456
71498
  // message, but a tool-using turn spans several (message A with the tool call,
@@ -71469,13 +71511,17 @@ var ClaudeCodeProjector = class {
71469
71511
  // real turn failure (FRA-3548). Cleared on every result and on the next
71470
71512
  // message_start so a stale marker can never reclassify a later failure.
71471
71513
  interruptMarkerSeen = false;
71472
- project(message) {
71514
+ project(message, context = {}) {
71473
71515
  if (isClaudeCodeInterruptMarker(message)) {
71474
71516
  this.interruptMarkerSeen = true;
71475
71517
  }
71476
71518
  if (message.type === "stream_event") {
71477
71519
  return this.projectStreamEvent(message);
71478
71520
  }
71521
+ const interruptContext = {
71522
+ marker: this.interruptMarkerSeen,
71523
+ bridge: context.bridgeInterruptSettling === true
71524
+ };
71479
71525
  const parsed = parseClaudeCodeStreamRecord(message);
71480
71526
  const snapshotMessageId = assistantSnapshotMessageId(message);
71481
71527
  const suppressStreamed = snapshotMessageId !== null && this.streamedMessageIds.has(snapshotMessageId);
@@ -71487,18 +71533,28 @@ var ClaudeCodeProjector = class {
71487
71533
  return [];
71488
71534
  }
71489
71535
  return conversationProjectionToUiChunks(entry, {
71490
- interrupted: this.interruptMarkerSeen
71536
+ interruptContext,
71537
+ emitDiagnostic: this.input.writeOutput
71491
71538
  });
71492
71539
  });
71493
71540
  if (parsed.result) {
71494
- outputs.push(...this.endActiveParts(), ...this.finishActiveToolInputs());
71495
- this.currentMessageId = null;
71496
- this.activeResponseMessageId = null;
71541
+ const interrupted = this.isInterruptCancellation(
71542
+ parsed.result,
71543
+ interruptContext
71544
+ );
71545
+ const failActiveInputs = parsed.result.isError && !interrupted;
71497
71546
  outputs.push(
71498
- projectClaudeCodeResult(parsed.result, {
71499
- interrupted: this.isInterruptCancellation(parsed.result)
71500
- })
71547
+ ...this.endActiveParts(),
71548
+ ...failActiveInputs ? this.failActiveToolInputs() : this.finishActiveToolInputs()
71501
71549
  );
71550
+ this.currentMessageId = null;
71551
+ this.activeResponseMessageId = null;
71552
+ if (interrupted) {
71553
+ this.input.writeOutput?.(
71554
+ `agent_bridge_claude_result_interrupt_cancellation gate=${interruptGate(interruptContext)}`
71555
+ );
71556
+ }
71557
+ outputs.push(projectClaudeCodeResult(parsed.result, { interrupted }));
71502
71558
  this.interruptMarkerSeen = false;
71503
71559
  }
71504
71560
  return outputs;
@@ -71508,13 +71564,16 @@ var ClaudeCodeProjector = class {
71508
71564
  // surfaces as a diagnostic-only `error_during_execution`. Treating that as a
71509
71565
  // turn failure fed the FRA-3548 redelivery loop (the bridge redelivered a
71510
71566
  // message the SDK was actively answering), so a failed result bracketed by
71511
- // the SDK's own interrupt marker is classified as a cancellation instead.
71512
- isInterruptCancellation(result) {
71513
- return result.isError && this.interruptMarkerSeen && isClaudeCodeEdeDiagnosticOnlyError(result.errorMessage);
71567
+ // interrupt context — the SDK's own marker, or the session's knowledge that
71568
+ // a bridge interrupt is settling (which covers a missing or late marker)
71569
+ // is classified as a cancellation instead.
71570
+ isInterruptCancellation(result, context) {
71571
+ return result.isError && (context.marker || context.bridge) && isClaudeCodeEdeDiagnosticOnlyError(result.errorMessage);
71514
71572
  }
71515
71573
  flushPendingAssistantMessages() {
71516
71574
  const outputs = [
71517
71575
  ...this.endActiveParts(),
71576
+ ...this.failActiveToolInputs(),
71518
71577
  uiChunk({ type: "finish", finishReason: "stop" })
71519
71578
  ];
71520
71579
  this.currentMessageId = null;
@@ -71618,6 +71677,23 @@ var ClaudeCodeProjector = class {
71618
71677
  (index) => this.finishActiveToolInput(index)
71619
71678
  );
71620
71679
  }
71680
+ // Settle every still-active tool input as an error part that preserves the
71681
+ // partial input text. Used when the turn ends in an error before the tool
71682
+ // block completed: the tool was never dispatched, and persisting whatever
71683
+ // input streamed keeps the transcript diagnosable.
71684
+ failActiveToolInputs() {
71685
+ const outputs = [...this.activeToolInputs.values()].map(
71686
+ (active) => uiChunk({
71687
+ type: "tool-input-error",
71688
+ toolCallId: active.toolCallId,
71689
+ toolName: active.toolName,
71690
+ input: partialToolInput(active),
71691
+ errorText: truncatedToolInputErrorText(active)
71692
+ })
71693
+ );
71694
+ this.activeToolInputs.clear();
71695
+ return outputs;
71696
+ }
71621
71697
  // A never-streamed assistant snapshot (an SDK-internal stream retry can
71622
71698
  // reissue the in-flight message under a fresh id, so suppression misses it;
71623
71699
  // pure snapshot delivery has no stream at all). Its content must respect the
@@ -71631,7 +71707,8 @@ var ClaudeCodeProjector = class {
71631
71707
  const messageChunks = this.assistantSnapshotMessageChunks(projections);
71632
71708
  const rest = projections.filter((projection) => !isAssistantMessageOrToolCall(projection)).flatMap(
71633
71709
  (projection) => conversationProjectionToUiChunks(projection, {
71634
- interrupted: this.interruptMarkerSeen
71710
+ interruptContext: { marker: this.interruptMarkerSeen, bridge: false },
71711
+ emitDiagnostic: this.input.writeOutput
71635
71712
  })
71636
71713
  );
71637
71714
  const last = messageChunks.at(-1);
@@ -71793,6 +71870,21 @@ function completeToolInput(input) {
71793
71870
  }
71794
71871
  return input.initialInput ?? {};
71795
71872
  }
71873
+ function partialToolInput(input) {
71874
+ const inputText = input.inputText.trim();
71875
+ if (inputText.length === 0) {
71876
+ return input.initialInput ?? {};
71877
+ }
71878
+ try {
71879
+ return JSON.parse(inputText);
71880
+ } catch {
71881
+ return input.inputText;
71882
+ }
71883
+ }
71884
+ function truncatedToolInputErrorText(input) {
71885
+ const bytes = Buffer.byteLength(input.inputText, "utf8");
71886
+ return `Tool input streaming was interrupted before the call completed; the tool was never dispatched (${bytes.toString()} bytes of partial input preserved).`;
71887
+ }
71796
71888
  function sourceChunkFromDelta(delta, index) {
71797
71889
  if (delta.type !== "citations_delta") {
71798
71890
  return null;
@@ -71855,7 +71947,7 @@ function textContentFromDelta(delta) {
71855
71947
  return null;
71856
71948
  }
71857
71949
  }
71858
- function conversationProjectionToUiChunks(projection, options = { interrupted: false }) {
71950
+ function conversationProjectionToUiChunks(projection, options) {
71859
71951
  if (projection.role === "assistant" && projection.kind === "message") {
71860
71952
  return messageContentChunks(projection);
71861
71953
  }
@@ -71876,15 +71968,12 @@ function conversationProjectionToUiChunks(projection, options = { interrupted: f
71876
71968
  })
71877
71969
  ];
71878
71970
  }
71879
- const remapped = typeof part.output === "string" ? remapClaudeCodeInterruptToolResultErrorText(part.output, {
71880
- interrupted: options.interrupted
71881
- }) : null;
71882
71971
  return [
71883
- uiChunk({
71884
- type: "tool-output-error",
71885
- toolCallId: part.toolUseId ?? UNKNOWN_MESSAGE_ID,
71886
- errorText: remapped ?? JSON.stringify(part.output)
71887
- })
71972
+ toolResultErrorChunk(
71973
+ part.toolUseId ?? UNKNOWN_MESSAGE_ID,
71974
+ part.output,
71975
+ options
71976
+ )
71888
71977
  ];
71889
71978
  });
71890
71979
  }
@@ -71928,6 +72017,32 @@ function conversationProjectionToUiChunks(projection, options = { interrupted: f
71928
72017
  }
71929
72018
  ];
71930
72019
  }
72020
+ function toolResultErrorChunk(toolCallId, output, options) {
72021
+ const context = options.interruptContext;
72022
+ const remapped = typeof output === "string" ? remapClaudeCodeInterruptToolResultErrorText(output, {
72023
+ interrupted: context.marker || context.bridge
72024
+ }) : null;
72025
+ if (remapped !== null) {
72026
+ options.emitDiagnostic?.(
72027
+ `agent_bridge_claude_tool_cancellation_remapped tool_use_id=${toolCallId} gate=${interruptGate(context)}`
72028
+ );
72029
+ } else if (typeof output === "string" && isClaudeCodePermissionDenialText(output)) {
72030
+ options.emitDiagnostic?.(
72031
+ `agent_bridge_claude_permission_denial_passthrough tool_use_id=${toolCallId}`
72032
+ );
72033
+ }
72034
+ return uiChunk({
72035
+ type: "tool-output-error",
72036
+ toolCallId,
72037
+ errorText: remapped ?? JSON.stringify(output)
72038
+ });
72039
+ }
72040
+ function interruptGate(context) {
72041
+ if (context.marker && context.bridge) {
72042
+ return "marker+bridge";
72043
+ }
72044
+ return context.marker ? "marker" : "bridge";
72045
+ }
71931
72046
  function isAssistantMessageOrToolCall(projection) {
71932
72047
  return projection.role === "assistant" && (projection.kind === "message" || projection.kind === "tool_call");
71933
72048
  }
@@ -91972,7 +92087,7 @@ function withClaudeStderrDiagnosticsPointer(error51, capturedStderr) {
91972
92087
 
91973
92088
  // src/commands/agent-bridge/harness/claude-code/session.ts
91974
92089
  var CLAUDE_AGENT_STARTUP_TIMEOUT_MS = 3e4;
91975
- var CLAUDE_INTERRUPT_SETTLE_TIMEOUT_MS = 1e4;
92090
+ var CLAUDE_TOOL_BOUNDARY_WAIT_TIMEOUT_MS = 12e4;
91976
92091
  var CLAUDE_INTERRUPT_ACK_TIMEOUT_MS = 1e4;
91977
92092
  var CLAUDE_MCP_REGISTRATION_TIMEOUT_MS = 3e4;
91978
92093
  var CLAUDE_MCP_REGISTRATION_POLL_INTERVAL_MS = 100;
@@ -92048,16 +92163,33 @@ var ClaudeAgentBridgeSessionImpl = class {
92048
92163
  // flushed command id.
92049
92164
  openTurnCommandIds = [];
92050
92165
  // tool_use ids the assistant has emitted whose tool_result has not yet been
92051
- // observed. A non-empty set means a tool call is in flight, so an immediate
92052
- // interrupt would append the new user message after an unresolved
92166
+ // observed, with the name/detail needed to describe them in a cancellation
92167
+ // notice. A non-empty map means a tool call is in flight: interrupting now
92168
+ // would destroy its work and record a denial-worded cancellation, and
92169
+ // injecting now would append the new user message after an unresolved
92053
92170
  // stop_reason=tool_use — the invalid transcript that trips Claude Code's
92054
92171
  // ede_diagnostic and auto-restarts the session (FRA-3049).
92055
- pendingToolUseIds = /* @__PURE__ */ new Set();
92056
- // Incremented on every turn-terminal `result`. An interrupt that aborts an
92057
- // in-flight tool call waits for this to advance before injecting, so the
92058
- // message lands only once the interrupted turn has settled its tool_use.
92172
+ pendingToolUses = /* @__PURE__ */ new Map();
92173
+ // Incremented on every turn-terminal `result`; the interrupt-timeout latch
92174
+ // compares against it to tell whether the same wedged turn is still running.
92059
92175
  turnResultCount = 0;
92060
- turnSettlementWaiters = /* @__PURE__ */ new Set();
92176
+ // A steering message is waiting for the in-flight tool call to settle so the
92177
+ // turn can be interrupted at the tool boundary instead of mid-tool. Armed by
92178
+ // sendMessage, fired by the output pump when the pending tool_use set
92179
+ // drains, bounded by the budget timer below.
92180
+ toolBoundaryWait = null;
92181
+ // True from the moment a bridge-initiated interrupt control request fires
92182
+ // until the targeted turn emits its terminal result (or the stream ends).
92183
+ // Reported to the projector on every message so the SDK's denial-worded
92184
+ // cancellation records are classified as platform cancellations even though
92185
+ // the SDK's own interrupt marker arrives after them (probe-verified wire
92186
+ // order, 2026-07-13: tool_result → marker → error_during_execution result).
92187
+ interruptSettling = false;
92188
+ // Tool calls the SDK cancelled while a bridge-initiated interrupt was
92189
+ // settling. Consumed by the deferred flush: the injected message gets a
92190
+ // platform-cancellation notice naming this work so the model re-runs it
92191
+ // instead of treating the denial-worded tool_result as a human rejection.
92192
+ cancelledToolRuns = [];
92061
92193
  // Memoized MCP-registration gate, keyed by the attached query. A runtime
92062
92194
  // restart warms a brand-new session + query, so keying off the query object
92063
92195
  // (rather than a session-lifetime flag) re-arms the gate on every restart:
@@ -92116,18 +92248,34 @@ var ClaudeAgentBridgeSessionImpl = class {
92116
92248
  delivery_mode: mode,
92117
92249
  state: this.state.kind,
92118
92250
  active_turn_count: this.activeTurnCount,
92119
- pending_tool_use_count: this.pendingToolUseIds.size,
92251
+ pending_tool_use_count: this.pendingToolUses.size,
92120
92252
  deferred_count: this.deferredMessages.length
92121
92253
  });
92122
92254
  if (mode === "deferred" && this.hasInterruptibleTurn()) {
92123
92255
  this.deferredMessages.push({ text: message, commandIds });
92124
92256
  this.input.runtimeLogger?.info("agent_bridge_claude_message_deferred", {
92125
92257
  active_turn_count: this.activeTurnCount,
92126
- pending_tool_use_count: this.pendingToolUseIds.size,
92258
+ pending_tool_use_count: this.pendingToolUses.size,
92127
92259
  deferred_count: this.deferredMessages.length
92128
92260
  });
92129
92261
  return;
92130
92262
  }
92263
+ if (this.hasInterruptibleTurn() && this.hasInFlightToolUse()) {
92264
+ this.deferredMessages.push({ text: message, commandIds });
92265
+ this.armToolBoundaryWait();
92266
+ this.input.runtimeLogger?.info(
92267
+ "agent_bridge_claude_message_deferred_tool_boundary",
92268
+ {
92269
+ active_turn_count: this.activeTurnCount,
92270
+ pending_tool_use_count: this.pendingToolUses.size,
92271
+ pending_tools: [...this.pendingToolUses.values()].map(
92272
+ (tool) => tool.name
92273
+ ),
92274
+ deferred_count: this.deferredMessages.length
92275
+ }
92276
+ );
92277
+ return;
92278
+ }
92131
92279
  const interruption = await this.interruptActiveTurnBeforeMessage();
92132
92280
  if (interruption === "defer" && this.hasInterruptibleTurn()) {
92133
92281
  this.deferredMessages.push({ text: message, commandIds });
@@ -92135,7 +92283,7 @@ var ClaudeAgentBridgeSessionImpl = class {
92135
92283
  "agent_bridge_claude_message_deferred_after_interrupt_timeout",
92136
92284
  {
92137
92285
  active_turn_count: this.activeTurnCount,
92138
- pending_tool_use_count: this.pendingToolUseIds.size,
92286
+ pending_tool_use_count: this.pendingToolUses.size,
92139
92287
  deferred_count: this.deferredMessages.length
92140
92288
  }
92141
92289
  );
@@ -92148,7 +92296,7 @@ var ClaudeAgentBridgeSessionImpl = class {
92148
92296
  duration_ms: Date.now() - startedAt,
92149
92297
  state: this.state.kind,
92150
92298
  active_turn_count: this.activeTurnCount,
92151
- pending_tool_use_count: this.pendingToolUseIds.size,
92299
+ pending_tool_use_count: this.pendingToolUses.size,
92152
92300
  deferred_count: this.deferredMessages.length
92153
92301
  });
92154
92302
  }
@@ -92232,8 +92380,10 @@ var ClaudeAgentBridgeSessionImpl = class {
92232
92380
  }
92233
92381
  this.state = { kind: "closed" };
92234
92382
  this.activeTurnCount = 0;
92235
- this.pendingToolUseIds.clear();
92236
- this.resolveTurnSettlement(false);
92383
+ this.pendingToolUses.clear();
92384
+ this.cancelledToolRuns = [];
92385
+ this.interruptSettling = false;
92386
+ this.disarmToolBoundaryWait();
92237
92387
  if (this.deferredMessages.length > 0) {
92238
92388
  this.input.writeOutput?.(
92239
92389
  `agent_bridge_claude_deferred_dropped count=${this.deferredMessages.length} reason=session_closed`
@@ -92324,19 +92474,21 @@ var ClaudeAgentBridgeSessionImpl = class {
92324
92474
  void (async () => {
92325
92475
  try {
92326
92476
  for await (const message of query) {
92477
+ const interruptSettling = this.interruptSettling;
92327
92478
  this.trackToolUseLifecycle(message);
92328
- let meta3;
92479
+ let meta3 = { interruptSettling };
92329
92480
  if (isClaudeAgentTurnResult(message)) {
92330
92481
  this.activeTurnCount = Math.max(0, this.activeTurnCount - 1);
92331
92482
  meta3 = {
92483
+ interruptSettling,
92332
92484
  consumedCommandIds: this.openTurnCommandIds.shift() ?? []
92333
92485
  };
92334
- this.pendingToolUseIds.clear();
92486
+ this.pendingToolUses.clear();
92487
+ this.interruptSettling = false;
92335
92488
  this.turnResultCount += 1;
92336
- this.resolveTurnSettlement(true);
92337
92489
  this.input.runtimeLogger?.info("agent_bridge_claude_turn_result", {
92338
92490
  active_turn_count: this.activeTurnCount,
92339
- pending_tool_use_count: this.pendingToolUseIds.size,
92491
+ pending_tool_use_count: this.pendingToolUses.size,
92340
92492
  turn_result_count: this.turnResultCount,
92341
92493
  deferred_count: this.deferredMessages.length,
92342
92494
  consumed_command_ids: meta3.consumedCommandIds
@@ -92344,6 +92496,9 @@ var ClaudeAgentBridgeSessionImpl = class {
92344
92496
  if (this.activeTurnCount === 0) {
92345
92497
  this.flushDeferredMessages();
92346
92498
  }
92499
+ if (this.deferredMessages.length === 0) {
92500
+ this.cancelledToolRuns = [];
92501
+ }
92347
92502
  }
92348
92503
  const failure = probeClaudeAgentMessage(
92349
92504
  message,
@@ -92368,8 +92523,9 @@ var ClaudeAgentBridgeSessionImpl = class {
92368
92523
  } finally {
92369
92524
  this.activeTurnCount = 0;
92370
92525
  this.openTurnCommandIds.length = 0;
92371
- this.pendingToolUseIds.clear();
92372
- this.resolveTurnSettlement(false);
92526
+ this.pendingToolUses.clear();
92527
+ this.interruptSettling = false;
92528
+ this.disarmToolBoundaryWait();
92373
92529
  this.state = { kind: "closed" };
92374
92530
  this.reportExit();
92375
92531
  }
@@ -92386,7 +92542,7 @@ var ClaudeAgentBridgeSessionImpl = class {
92386
92542
  this.input.runtimeLogger?.info("agent_bridge_claude_message_enqueued", {
92387
92543
  state: this.state.kind,
92388
92544
  active_turn_count: this.activeTurnCount,
92389
- pending_tool_use_count: this.pendingToolUseIds.size,
92545
+ pending_tool_use_count: this.pendingToolUses.size,
92390
92546
  deferred_count: this.deferredMessages.length
92391
92547
  });
92392
92548
  void this.ensureRunningQuery().catch((error51) => {
@@ -92399,75 +92555,105 @@ var ClaudeAgentBridgeSessionImpl = class {
92399
92555
  return this.activeTurnCount > 0 && this.state.kind === "running";
92400
92556
  }
92401
92557
  // True while the assistant has an emitted tool_use with no matching
92402
- // tool_result yet — the window where an immediate interrupt would strand an
92558
+ // tool_result yet — the window where an immediate interrupt would cancel
92559
+ // real tool work, and where an immediate injection would strand an
92403
92560
  // unresolved stop_reason=tool_use ahead of the injected user message.
92404
92561
  hasInFlightToolUse() {
92405
- return this.pendingToolUseIds.size > 0;
92562
+ return this.pendingToolUses.size > 0;
92406
92563
  }
92407
92564
  // Track tool_use/tool_result pairs as they stream so hasInFlightToolUse()
92408
92565
  // reflects the live transcript. A tool_use may appear first as a streaming
92409
92566
  // content block before the full assistant message is available; the matching
92410
92567
  // tool_result closes it; the turn's terminal result clears any remainder.
92568
+ // Two side jobs ride on the same observation point:
92569
+ // - a denial-worded error tool_result arriving while a bridge interrupt is
92570
+ // settling is that interrupt's cancellation of the tool — record it so the
92571
+ // flushed message can tell the model to re-run the cancelled work;
92572
+ // - a tool_result that drains the pending set is the tool boundary an armed
92573
+ // steering message has been waiting for — interrupt the turn now, while no
92574
+ // tool work is at risk.
92411
92575
  trackToolUseLifecycle(message) {
92412
- for (const id of toolUseIds(message)) {
92413
- this.pendingToolUseIds.add(id);
92576
+ for (const entry of toolUseEntries(message)) {
92577
+ this.pendingToolUses.set(entry.id, {
92578
+ name: entry.name,
92579
+ detail: entry.detail
92580
+ });
92581
+ }
92582
+ const hadInFlightToolUse = this.hasInFlightToolUse();
92583
+ for (const result of toolResultEntries(message)) {
92584
+ const pending = this.pendingToolUses.get(result.id);
92585
+ this.pendingToolUses.delete(result.id);
92586
+ if (pending && this.interruptSettling && result.isError && result.text !== null && isClaudeCodePermissionDenialText(result.text)) {
92587
+ this.cancelledToolRuns.push(pending);
92588
+ this.input.writeOutput?.(
92589
+ `agent_bridge_claude_tool_cancellation_observed tool=${pending.name}`
92590
+ );
92591
+ }
92414
92592
  }
92415
- for (const id of toolResultIds(message)) {
92416
- this.pendingToolUseIds.delete(id);
92593
+ if (hadInFlightToolUse && !this.hasInFlightToolUse()) {
92594
+ this.fireToolBoundaryInterrupt();
92417
92595
  }
92418
92596
  }
92419
- // Resolve once the interrupted turn emits its terminal `result` (turnResultCount
92420
- // advances past the pre-interrupt baseline), bounded so a tool that never
92421
- // returns after interrupt cannot wedge delivery. The caller reads the
92422
- // outcome: "timeout" means the turn is still running and the message must
92423
- // defer to the turn boundary rather than inject mid-turn.
92424
- awaitTurnSettlement(baseline, startedAt) {
92425
- if (this.turnResultCount > baseline) {
92426
- return Promise.resolve("ready");
92597
+ // The pending tool_use set just drained mid-turn. If a steering message is
92598
+ // waiting on that boundary, interrupt the turn now: nothing is executing, so
92599
+ // the cancellation costs only the model's remaining reasoning, and the
92600
+ // turn's terminal result flushes the message. Fire-and-forget the flush at
92601
+ // the turn result owns delivery whether or not this interrupt lands.
92602
+ fireToolBoundaryInterrupt() {
92603
+ if (!this.toolBoundaryWait || this.deferredMessages.length === 0 || !this.hasInterruptibleTurn()) {
92604
+ return;
92427
92605
  }
92428
- return new Promise((resolve2) => {
92429
- let done = false;
92430
- const finish = (outcome) => {
92431
- if (done) {
92432
- return;
92433
- }
92434
- done = true;
92435
- clearTimeout(timer);
92436
- this.turnSettlementWaiters.delete(waiter);
92437
- this.input.writeOutput?.(
92438
- `agent_bridge_claude_interrupt_settle_${outcome} duration_ms=${Date.now() - startedAt}`
92439
- );
92440
- resolve2(outcome);
92441
- };
92442
- const waiter = (settled) => finish(settled ? "ready" : "aborted");
92443
- const timer = setTimeout(
92444
- () => finish("timeout"),
92445
- CLAUDE_INTERRUPT_SETTLE_TIMEOUT_MS
92606
+ this.disarmToolBoundaryWait();
92607
+ this.input.writeOutput?.(
92608
+ `agent_bridge_claude_tool_boundary_interrupt deferred_count=${this.deferredMessages.length}`
92609
+ );
92610
+ void this.interruptActiveTurn();
92611
+ }
92612
+ // Bound the tool-boundary wait: past the budget the steering message is
92613
+ // judged more urgent than the in-flight tool, which is cancelled mid-run.
92614
+ // The cancellation is observed by trackToolUseLifecycle and reported to the
92615
+ // model in the flushed message's platform notice.
92616
+ armToolBoundaryWait() {
92617
+ if (this.toolBoundaryWait) {
92618
+ return;
92619
+ }
92620
+ const timer = setTimeout(() => {
92621
+ this.toolBoundaryWait = null;
92622
+ if (!this.hasInterruptibleTurn() || this.deferredMessages.length === 0) {
92623
+ return;
92624
+ }
92625
+ this.input.writeOutput?.(
92626
+ `agent_bridge_claude_tool_boundary_wait_timeout pending_tools=${[
92627
+ ...this.pendingToolUses.values()
92628
+ ].map((tool) => tool.name).join(",")} deferred_count=${this.deferredMessages.length}`
92446
92629
  );
92447
- timer.unref?.();
92448
- this.turnSettlementWaiters.add(waiter);
92449
- });
92630
+ void this.interruptActiveTurn();
92631
+ }, CLAUDE_TOOL_BOUNDARY_WAIT_TIMEOUT_MS);
92632
+ timer.unref?.();
92633
+ this.toolBoundaryWait = { timer };
92450
92634
  }
92451
- resolveTurnSettlement(settled) {
92452
- if (this.turnSettlementWaiters.size === 0) {
92635
+ disarmToolBoundaryWait() {
92636
+ if (!this.toolBoundaryWait) {
92453
92637
  return;
92454
92638
  }
92455
- const waiters = [...this.turnSettlementWaiters];
92456
- this.turnSettlementWaiters.clear();
92457
- for (const waiter of waiters) {
92458
- waiter(settled);
92459
- }
92639
+ clearTimeout(this.toolBoundaryWait.timer);
92640
+ this.toolBoundaryWait = null;
92460
92641
  }
92461
92642
  flushDeferredMessages() {
92462
92643
  if (this.deferredMessages.length === 0) {
92463
92644
  return;
92464
92645
  }
92465
92646
  const pending = this.deferredMessages.splice(0);
92647
+ const cancelledToolRuns = this.cancelledToolRuns.splice(0);
92648
+ this.disarmToolBoundaryWait();
92466
92649
  this.input.writeOutput?.(
92467
- `agent_bridge_claude_deferred_flush count=${pending.length}`
92650
+ `agent_bridge_claude_deferred_flush count=${pending.length} cancelled_tool_count=${cancelledToolRuns.length}`
92468
92651
  );
92652
+ const text2 = pending.map((entry) => entry.text).join("\n\n");
92469
92653
  this.enqueueUserMessage(
92470
- pending.map((entry) => entry.text).join("\n\n"),
92654
+ cancelledToolRuns.length > 0 ? `${text2}
92655
+
92656
+ ${cancelledToolPlatformNotice(cancelledToolRuns)}` : text2,
92471
92657
  pending.flatMap((entry) => entry.commandIds)
92472
92658
  );
92473
92659
  }
@@ -92488,11 +92674,10 @@ var ClaudeAgentBridgeSessionImpl = class {
92488
92674
  return "proceed";
92489
92675
  }
92490
92676
  const query = this.state.query;
92491
- const hadInFlightToolUse = this.hasInFlightToolUse();
92492
92677
  const settlementBaseline = this.turnResultCount;
92493
92678
  const startedAt = Date.now();
92494
92679
  this.input.writeOutput?.(
92495
- `agent_bridge_claude_mid_turn_interrupt_started at=${new Date(startedAt).toISOString()} in_flight_tool_use=${hadInFlightToolUse}`
92680
+ `agent_bridge_claude_mid_turn_interrupt_started at=${new Date(startedAt).toISOString()} in_flight_tool_use=${this.hasInFlightToolUse()}`
92496
92681
  );
92497
92682
  const interruptPromise = (async () => {
92498
92683
  const ack = await this.requestInterruptAck(query, startedAt);
@@ -92500,16 +92685,6 @@ var ClaudeAgentBridgeSessionImpl = class {
92500
92685
  this.interruptTimeoutTurn = settlementBaseline;
92501
92686
  return "defer";
92502
92687
  }
92503
- if (hadInFlightToolUse) {
92504
- const settle = await this.awaitTurnSettlement(
92505
- settlementBaseline,
92506
- startedAt
92507
- );
92508
- if (settle === "timeout") {
92509
- this.interruptTimeoutTurn = settlementBaseline;
92510
- return "defer";
92511
- }
92512
- }
92513
92688
  return "proceed";
92514
92689
  })();
92515
92690
  this.interruptInFlight = interruptPromise;
@@ -92546,6 +92721,7 @@ var ClaudeAgentBridgeSessionImpl = class {
92546
92721
  );
92547
92722
  }, CLAUDE_INTERRUPT_ACK_TIMEOUT_MS);
92548
92723
  timer.unref?.();
92724
+ this.interruptSettling = true;
92549
92725
  query.interrupt().then(
92550
92726
  () => {
92551
92727
  if (done) {
@@ -92902,10 +93078,10 @@ function claudeAgentUserMessage(message) {
92902
93078
  function isClaudeAgentTurnResult(message) {
92903
93079
  return message.type === "result";
92904
93080
  }
92905
- function toolUseIds(message) {
93081
+ function toolUseEntries(message) {
92906
93082
  if (message.type === "assistant") {
92907
93083
  return contentBlocks2(message.message.content).flatMap(
92908
- (block2) => block2.type === "tool_use" && typeof block2.id === "string" ? [block2.id] : []
93084
+ (block2) => block2.type === "tool_use" && typeof block2.id === "string" ? [toolUseEntryFromBlock(block2)] : []
92909
93085
  );
92910
93086
  }
92911
93087
  if (message.type !== "stream_event") {
@@ -92916,16 +93092,54 @@ function toolUseIds(message) {
92916
93092
  return [];
92917
93093
  }
92918
93094
  const block = event.content_block;
92919
- return block.type === "tool_use" && typeof block.id === "string" ? [block.id] : [];
93095
+ return block.type === "tool_use" && typeof block.id === "string" ? [toolUseEntryFromBlock(block)] : [];
93096
+ }
93097
+ function toolUseEntryFromBlock(block) {
93098
+ return {
93099
+ id: String(block.id),
93100
+ name: typeof block.name === "string" ? block.name : "unknown_tool",
93101
+ detail: toolUseDetail(block.input)
93102
+ };
93103
+ }
93104
+ function toolUseDetail(input) {
93105
+ if (input === void 0 || input === null) {
93106
+ return "";
93107
+ }
93108
+ let text2;
93109
+ try {
93110
+ text2 = JSON.stringify(input);
93111
+ } catch {
93112
+ return "";
93113
+ }
93114
+ if (!text2 || text2 === "{}") {
93115
+ return "";
93116
+ }
93117
+ return text2.length > 160 ? `${text2.slice(0, 160)}\u2026` : text2;
92920
93118
  }
92921
- function toolResultIds(message) {
93119
+ function toolResultEntries(message) {
92922
93120
  if (message.type !== "user") {
92923
93121
  return [];
92924
93122
  }
92925
93123
  return contentBlocks2(message.message.content).flatMap(
92926
- (block) => block.type === "tool_result" && typeof block.tool_use_id === "string" ? [block.tool_use_id] : []
93124
+ (block) => block.type === "tool_result" && typeof block.tool_use_id === "string" ? [
93125
+ {
93126
+ id: block.tool_use_id,
93127
+ isError: block.is_error === true,
93128
+ text: typeof block.content === "string" ? block.content : null
93129
+ }
93130
+ ] : []
92927
93131
  );
92928
93132
  }
93133
+ function cancelledToolPlatformNotice(cancelled) {
93134
+ const lines = cancelled.map(
93135
+ (tool) => `- ${tool.name}${tool.detail ? ` ${tool.detail}` : ""}`
93136
+ );
93137
+ return [
93138
+ "[Auto platform notice] To deliver the message above, the platform cancelled the following in-flight tool call(s) mid-execution:",
93139
+ ...lines,
93140
+ "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."
93141
+ ].join("\n");
93142
+ }
92929
93143
  function contentBlocks2(content) {
92930
93144
  if (!Array.isArray(content)) {
92931
93145
  return [];
@@ -92944,6 +93158,9 @@ var ClaudeCodeCommandHandler = class {
92944
93158
  this.input = input;
92945
93159
  this.claudeConfig = input.claude;
92946
93160
  this.outputBuffer = new AgentBridgeOutputBuffer(input);
93161
+ this.projector = new ClaudeCodeProjector({
93162
+ writeOutput: input.writeOutput
93163
+ });
92947
93164
  this.readState = input.readState ? new ClaudeReadStateTracker(input.readState) : null;
92948
93165
  }
92949
93166
  input;
@@ -92965,7 +93182,7 @@ var ClaudeCodeCommandHandler = class {
92965
93182
  inFlightMessageCommands = /* @__PURE__ */ new Map();
92966
93183
  pendingQuestions = /* @__PURE__ */ new Map();
92967
93184
  outputBuffer;
92968
- projector = new ClaudeCodeProjector();
93185
+ projector;
92969
93186
  readState;
92970
93187
  livenessTicker = null;
92971
93188
  // -----------------------------------------------------------------------------
@@ -93346,7 +93563,9 @@ var ClaudeCodeCommandHandler = class {
93346
93563
  this.trackReadState(activeContext, message);
93347
93564
  let projections;
93348
93565
  try {
93349
- projections = this.projector.project(message);
93566
+ projections = this.projector.project(message, {
93567
+ bridgeInterruptSettling: meta3?.interruptSettling === true
93568
+ });
93350
93569
  } catch (error51) {
93351
93570
  this.input.writeOutput?.(
93352
93571
  `agent_bridge_claude_parse_failed error=${error51 instanceof Error ? error51.message : String(error51)}`