@autohq/cli 0.1.449 → 0.1.450

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.450",
30829
30829
  license: "SEE LICENSE IN README.md",
30830
30830
  publishConfig: {
30831
30831
  access: "public"
@@ -71451,6 +71451,10 @@ function jsonRecordString2(value2, key) {
71451
71451
 
71452
71452
  // src/commands/agent-bridge/harness/claude-code/projector.ts
71453
71453
  var ClaudeCodeProjector = class {
71454
+ constructor(input = {}) {
71455
+ this.input = input;
71456
+ }
71457
+ input;
71454
71458
  currentMessageId = null;
71455
71459
  // The turn's UI message id. Claude emits a `message_start` per assistant
71456
71460
  // message, but a tool-using turn spans several (message A with the tool call,
@@ -71469,13 +71473,17 @@ var ClaudeCodeProjector = class {
71469
71473
  // real turn failure (FRA-3548). Cleared on every result and on the next
71470
71474
  // message_start so a stale marker can never reclassify a later failure.
71471
71475
  interruptMarkerSeen = false;
71472
- project(message) {
71476
+ project(message, context = {}) {
71473
71477
  if (isClaudeCodeInterruptMarker(message)) {
71474
71478
  this.interruptMarkerSeen = true;
71475
71479
  }
71476
71480
  if (message.type === "stream_event") {
71477
71481
  return this.projectStreamEvent(message);
71478
71482
  }
71483
+ const interruptContext = {
71484
+ marker: this.interruptMarkerSeen,
71485
+ bridge: context.bridgeInterruptSettling === true
71486
+ };
71479
71487
  const parsed = parseClaudeCodeStreamRecord(message);
71480
71488
  const snapshotMessageId = assistantSnapshotMessageId(message);
71481
71489
  const suppressStreamed = snapshotMessageId !== null && this.streamedMessageIds.has(snapshotMessageId);
@@ -71487,18 +71495,24 @@ var ClaudeCodeProjector = class {
71487
71495
  return [];
71488
71496
  }
71489
71497
  return conversationProjectionToUiChunks(entry, {
71490
- interrupted: this.interruptMarkerSeen
71498
+ interruptContext,
71499
+ emitDiagnostic: this.input.writeOutput
71491
71500
  });
71492
71501
  });
71493
71502
  if (parsed.result) {
71494
71503
  outputs.push(...this.endActiveParts(), ...this.finishActiveToolInputs());
71495
71504
  this.currentMessageId = null;
71496
71505
  this.activeResponseMessageId = null;
71497
- outputs.push(
71498
- projectClaudeCodeResult(parsed.result, {
71499
- interrupted: this.isInterruptCancellation(parsed.result)
71500
- })
71506
+ const interrupted = this.isInterruptCancellation(
71507
+ parsed.result,
71508
+ interruptContext
71501
71509
  );
71510
+ if (interrupted) {
71511
+ this.input.writeOutput?.(
71512
+ `agent_bridge_claude_result_interrupt_cancellation gate=${interruptGate(interruptContext)}`
71513
+ );
71514
+ }
71515
+ outputs.push(projectClaudeCodeResult(parsed.result, { interrupted }));
71502
71516
  this.interruptMarkerSeen = false;
71503
71517
  }
71504
71518
  return outputs;
@@ -71508,9 +71522,11 @@ var ClaudeCodeProjector = class {
71508
71522
  // surfaces as a diagnostic-only `error_during_execution`. Treating that as a
71509
71523
  // turn failure fed the FRA-3548 redelivery loop (the bridge redelivered a
71510
71524
  // 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);
71525
+ // interrupt context — the SDK's own marker, or the session's knowledge that
71526
+ // a bridge interrupt is settling (which covers a missing or late marker)
71527
+ // is classified as a cancellation instead.
71528
+ isInterruptCancellation(result, context) {
71529
+ return result.isError && (context.marker || context.bridge) && isClaudeCodeEdeDiagnosticOnlyError(result.errorMessage);
71514
71530
  }
71515
71531
  flushPendingAssistantMessages() {
71516
71532
  const outputs = [
@@ -71631,7 +71647,8 @@ var ClaudeCodeProjector = class {
71631
71647
  const messageChunks = this.assistantSnapshotMessageChunks(projections);
71632
71648
  const rest = projections.filter((projection) => !isAssistantMessageOrToolCall(projection)).flatMap(
71633
71649
  (projection) => conversationProjectionToUiChunks(projection, {
71634
- interrupted: this.interruptMarkerSeen
71650
+ interruptContext: { marker: this.interruptMarkerSeen, bridge: false },
71651
+ emitDiagnostic: this.input.writeOutput
71635
71652
  })
71636
71653
  );
71637
71654
  const last = messageChunks.at(-1);
@@ -71855,7 +71872,7 @@ function textContentFromDelta(delta) {
71855
71872
  return null;
71856
71873
  }
71857
71874
  }
71858
- function conversationProjectionToUiChunks(projection, options = { interrupted: false }) {
71875
+ function conversationProjectionToUiChunks(projection, options) {
71859
71876
  if (projection.role === "assistant" && projection.kind === "message") {
71860
71877
  return messageContentChunks(projection);
71861
71878
  }
@@ -71876,15 +71893,12 @@ function conversationProjectionToUiChunks(projection, options = { interrupted: f
71876
71893
  })
71877
71894
  ];
71878
71895
  }
71879
- const remapped = typeof part.output === "string" ? remapClaudeCodeInterruptToolResultErrorText(part.output, {
71880
- interrupted: options.interrupted
71881
- }) : null;
71882
71896
  return [
71883
- uiChunk({
71884
- type: "tool-output-error",
71885
- toolCallId: part.toolUseId ?? UNKNOWN_MESSAGE_ID,
71886
- errorText: remapped ?? JSON.stringify(part.output)
71887
- })
71897
+ toolResultErrorChunk(
71898
+ part.toolUseId ?? UNKNOWN_MESSAGE_ID,
71899
+ part.output,
71900
+ options
71901
+ )
71888
71902
  ];
71889
71903
  });
71890
71904
  }
@@ -71928,6 +71942,32 @@ function conversationProjectionToUiChunks(projection, options = { interrupted: f
71928
71942
  }
71929
71943
  ];
71930
71944
  }
71945
+ function toolResultErrorChunk(toolCallId, output, options) {
71946
+ const context = options.interruptContext;
71947
+ const remapped = typeof output === "string" ? remapClaudeCodeInterruptToolResultErrorText(output, {
71948
+ interrupted: context.marker || context.bridge
71949
+ }) : null;
71950
+ if (remapped !== null) {
71951
+ options.emitDiagnostic?.(
71952
+ `agent_bridge_claude_tool_cancellation_remapped tool_use_id=${toolCallId} gate=${interruptGate(context)}`
71953
+ );
71954
+ } else if (typeof output === "string" && isClaudeCodePermissionDenialText(output)) {
71955
+ options.emitDiagnostic?.(
71956
+ `agent_bridge_claude_permission_denial_passthrough tool_use_id=${toolCallId}`
71957
+ );
71958
+ }
71959
+ return uiChunk({
71960
+ type: "tool-output-error",
71961
+ toolCallId,
71962
+ errorText: remapped ?? JSON.stringify(output)
71963
+ });
71964
+ }
71965
+ function interruptGate(context) {
71966
+ if (context.marker && context.bridge) {
71967
+ return "marker+bridge";
71968
+ }
71969
+ return context.marker ? "marker" : "bridge";
71970
+ }
71931
71971
  function isAssistantMessageOrToolCall(projection) {
71932
71972
  return projection.role === "assistant" && (projection.kind === "message" || projection.kind === "tool_call");
71933
71973
  }
@@ -91972,7 +92012,7 @@ function withClaudeStderrDiagnosticsPointer(error51, capturedStderr) {
91972
92012
 
91973
92013
  // src/commands/agent-bridge/harness/claude-code/session.ts
91974
92014
  var CLAUDE_AGENT_STARTUP_TIMEOUT_MS = 3e4;
91975
- var CLAUDE_INTERRUPT_SETTLE_TIMEOUT_MS = 1e4;
92015
+ var CLAUDE_TOOL_BOUNDARY_WAIT_TIMEOUT_MS = 12e4;
91976
92016
  var CLAUDE_INTERRUPT_ACK_TIMEOUT_MS = 1e4;
91977
92017
  var CLAUDE_MCP_REGISTRATION_TIMEOUT_MS = 3e4;
91978
92018
  var CLAUDE_MCP_REGISTRATION_POLL_INTERVAL_MS = 100;
@@ -92048,16 +92088,33 @@ var ClaudeAgentBridgeSessionImpl = class {
92048
92088
  // flushed command id.
92049
92089
  openTurnCommandIds = [];
92050
92090
  // 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
92091
+ // observed, with the name/detail needed to describe them in a cancellation
92092
+ // notice. A non-empty map means a tool call is in flight: interrupting now
92093
+ // would destroy its work and record a denial-worded cancellation, and
92094
+ // injecting now would append the new user message after an unresolved
92053
92095
  // stop_reason=tool_use — the invalid transcript that trips Claude Code's
92054
92096
  // 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.
92097
+ pendingToolUses = /* @__PURE__ */ new Map();
92098
+ // Incremented on every turn-terminal `result`; the interrupt-timeout latch
92099
+ // compares against it to tell whether the same wedged turn is still running.
92059
92100
  turnResultCount = 0;
92060
- turnSettlementWaiters = /* @__PURE__ */ new Set();
92101
+ // A steering message is waiting for the in-flight tool call to settle so the
92102
+ // turn can be interrupted at the tool boundary instead of mid-tool. Armed by
92103
+ // sendMessage, fired by the output pump when the pending tool_use set
92104
+ // drains, bounded by the budget timer below.
92105
+ toolBoundaryWait = null;
92106
+ // True from the moment a bridge-initiated interrupt control request fires
92107
+ // until the targeted turn emits its terminal result (or the stream ends).
92108
+ // Reported to the projector on every message so the SDK's denial-worded
92109
+ // cancellation records are classified as platform cancellations even though
92110
+ // the SDK's own interrupt marker arrives after them (probe-verified wire
92111
+ // order, 2026-07-13: tool_result → marker → error_during_execution result).
92112
+ interruptSettling = false;
92113
+ // Tool calls the SDK cancelled while a bridge-initiated interrupt was
92114
+ // settling. Consumed by the deferred flush: the injected message gets a
92115
+ // platform-cancellation notice naming this work so the model re-runs it
92116
+ // instead of treating the denial-worded tool_result as a human rejection.
92117
+ cancelledToolRuns = [];
92061
92118
  // Memoized MCP-registration gate, keyed by the attached query. A runtime
92062
92119
  // restart warms a brand-new session + query, so keying off the query object
92063
92120
  // (rather than a session-lifetime flag) re-arms the gate on every restart:
@@ -92116,18 +92173,34 @@ var ClaudeAgentBridgeSessionImpl = class {
92116
92173
  delivery_mode: mode,
92117
92174
  state: this.state.kind,
92118
92175
  active_turn_count: this.activeTurnCount,
92119
- pending_tool_use_count: this.pendingToolUseIds.size,
92176
+ pending_tool_use_count: this.pendingToolUses.size,
92120
92177
  deferred_count: this.deferredMessages.length
92121
92178
  });
92122
92179
  if (mode === "deferred" && this.hasInterruptibleTurn()) {
92123
92180
  this.deferredMessages.push({ text: message, commandIds });
92124
92181
  this.input.runtimeLogger?.info("agent_bridge_claude_message_deferred", {
92125
92182
  active_turn_count: this.activeTurnCount,
92126
- pending_tool_use_count: this.pendingToolUseIds.size,
92183
+ pending_tool_use_count: this.pendingToolUses.size,
92127
92184
  deferred_count: this.deferredMessages.length
92128
92185
  });
92129
92186
  return;
92130
92187
  }
92188
+ if (this.hasInterruptibleTurn() && this.hasInFlightToolUse()) {
92189
+ this.deferredMessages.push({ text: message, commandIds });
92190
+ this.armToolBoundaryWait();
92191
+ this.input.runtimeLogger?.info(
92192
+ "agent_bridge_claude_message_deferred_tool_boundary",
92193
+ {
92194
+ active_turn_count: this.activeTurnCount,
92195
+ pending_tool_use_count: this.pendingToolUses.size,
92196
+ pending_tools: [...this.pendingToolUses.values()].map(
92197
+ (tool) => tool.name
92198
+ ),
92199
+ deferred_count: this.deferredMessages.length
92200
+ }
92201
+ );
92202
+ return;
92203
+ }
92131
92204
  const interruption = await this.interruptActiveTurnBeforeMessage();
92132
92205
  if (interruption === "defer" && this.hasInterruptibleTurn()) {
92133
92206
  this.deferredMessages.push({ text: message, commandIds });
@@ -92135,7 +92208,7 @@ var ClaudeAgentBridgeSessionImpl = class {
92135
92208
  "agent_bridge_claude_message_deferred_after_interrupt_timeout",
92136
92209
  {
92137
92210
  active_turn_count: this.activeTurnCount,
92138
- pending_tool_use_count: this.pendingToolUseIds.size,
92211
+ pending_tool_use_count: this.pendingToolUses.size,
92139
92212
  deferred_count: this.deferredMessages.length
92140
92213
  }
92141
92214
  );
@@ -92148,7 +92221,7 @@ var ClaudeAgentBridgeSessionImpl = class {
92148
92221
  duration_ms: Date.now() - startedAt,
92149
92222
  state: this.state.kind,
92150
92223
  active_turn_count: this.activeTurnCount,
92151
- pending_tool_use_count: this.pendingToolUseIds.size,
92224
+ pending_tool_use_count: this.pendingToolUses.size,
92152
92225
  deferred_count: this.deferredMessages.length
92153
92226
  });
92154
92227
  }
@@ -92232,8 +92305,10 @@ var ClaudeAgentBridgeSessionImpl = class {
92232
92305
  }
92233
92306
  this.state = { kind: "closed" };
92234
92307
  this.activeTurnCount = 0;
92235
- this.pendingToolUseIds.clear();
92236
- this.resolveTurnSettlement(false);
92308
+ this.pendingToolUses.clear();
92309
+ this.cancelledToolRuns = [];
92310
+ this.interruptSettling = false;
92311
+ this.disarmToolBoundaryWait();
92237
92312
  if (this.deferredMessages.length > 0) {
92238
92313
  this.input.writeOutput?.(
92239
92314
  `agent_bridge_claude_deferred_dropped count=${this.deferredMessages.length} reason=session_closed`
@@ -92324,19 +92399,21 @@ var ClaudeAgentBridgeSessionImpl = class {
92324
92399
  void (async () => {
92325
92400
  try {
92326
92401
  for await (const message of query) {
92402
+ const interruptSettling = this.interruptSettling;
92327
92403
  this.trackToolUseLifecycle(message);
92328
- let meta3;
92404
+ let meta3 = { interruptSettling };
92329
92405
  if (isClaudeAgentTurnResult(message)) {
92330
92406
  this.activeTurnCount = Math.max(0, this.activeTurnCount - 1);
92331
92407
  meta3 = {
92408
+ interruptSettling,
92332
92409
  consumedCommandIds: this.openTurnCommandIds.shift() ?? []
92333
92410
  };
92334
- this.pendingToolUseIds.clear();
92411
+ this.pendingToolUses.clear();
92412
+ this.interruptSettling = false;
92335
92413
  this.turnResultCount += 1;
92336
- this.resolveTurnSettlement(true);
92337
92414
  this.input.runtimeLogger?.info("agent_bridge_claude_turn_result", {
92338
92415
  active_turn_count: this.activeTurnCount,
92339
- pending_tool_use_count: this.pendingToolUseIds.size,
92416
+ pending_tool_use_count: this.pendingToolUses.size,
92340
92417
  turn_result_count: this.turnResultCount,
92341
92418
  deferred_count: this.deferredMessages.length,
92342
92419
  consumed_command_ids: meta3.consumedCommandIds
@@ -92344,6 +92421,9 @@ var ClaudeAgentBridgeSessionImpl = class {
92344
92421
  if (this.activeTurnCount === 0) {
92345
92422
  this.flushDeferredMessages();
92346
92423
  }
92424
+ if (this.deferredMessages.length === 0) {
92425
+ this.cancelledToolRuns = [];
92426
+ }
92347
92427
  }
92348
92428
  const failure = probeClaudeAgentMessage(
92349
92429
  message,
@@ -92368,8 +92448,9 @@ var ClaudeAgentBridgeSessionImpl = class {
92368
92448
  } finally {
92369
92449
  this.activeTurnCount = 0;
92370
92450
  this.openTurnCommandIds.length = 0;
92371
- this.pendingToolUseIds.clear();
92372
- this.resolveTurnSettlement(false);
92451
+ this.pendingToolUses.clear();
92452
+ this.interruptSettling = false;
92453
+ this.disarmToolBoundaryWait();
92373
92454
  this.state = { kind: "closed" };
92374
92455
  this.reportExit();
92375
92456
  }
@@ -92386,7 +92467,7 @@ var ClaudeAgentBridgeSessionImpl = class {
92386
92467
  this.input.runtimeLogger?.info("agent_bridge_claude_message_enqueued", {
92387
92468
  state: this.state.kind,
92388
92469
  active_turn_count: this.activeTurnCount,
92389
- pending_tool_use_count: this.pendingToolUseIds.size,
92470
+ pending_tool_use_count: this.pendingToolUses.size,
92390
92471
  deferred_count: this.deferredMessages.length
92391
92472
  });
92392
92473
  void this.ensureRunningQuery().catch((error51) => {
@@ -92399,75 +92480,105 @@ var ClaudeAgentBridgeSessionImpl = class {
92399
92480
  return this.activeTurnCount > 0 && this.state.kind === "running";
92400
92481
  }
92401
92482
  // 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
92483
+ // tool_result yet — the window where an immediate interrupt would cancel
92484
+ // real tool work, and where an immediate injection would strand an
92403
92485
  // unresolved stop_reason=tool_use ahead of the injected user message.
92404
92486
  hasInFlightToolUse() {
92405
- return this.pendingToolUseIds.size > 0;
92487
+ return this.pendingToolUses.size > 0;
92406
92488
  }
92407
92489
  // Track tool_use/tool_result pairs as they stream so hasInFlightToolUse()
92408
92490
  // reflects the live transcript. A tool_use may appear first as a streaming
92409
92491
  // content block before the full assistant message is available; the matching
92410
92492
  // tool_result closes it; the turn's terminal result clears any remainder.
92493
+ // Two side jobs ride on the same observation point:
92494
+ // - a denial-worded error tool_result arriving while a bridge interrupt is
92495
+ // settling is that interrupt's cancellation of the tool — record it so the
92496
+ // flushed message can tell the model to re-run the cancelled work;
92497
+ // - a tool_result that drains the pending set is the tool boundary an armed
92498
+ // steering message has been waiting for — interrupt the turn now, while no
92499
+ // tool work is at risk.
92411
92500
  trackToolUseLifecycle(message) {
92412
- for (const id of toolUseIds(message)) {
92413
- this.pendingToolUseIds.add(id);
92501
+ for (const entry of toolUseEntries(message)) {
92502
+ this.pendingToolUses.set(entry.id, {
92503
+ name: entry.name,
92504
+ detail: entry.detail
92505
+ });
92414
92506
  }
92415
- for (const id of toolResultIds(message)) {
92416
- this.pendingToolUseIds.delete(id);
92507
+ const hadInFlightToolUse = this.hasInFlightToolUse();
92508
+ for (const result of toolResultEntries(message)) {
92509
+ const pending = this.pendingToolUses.get(result.id);
92510
+ this.pendingToolUses.delete(result.id);
92511
+ if (pending && this.interruptSettling && result.isError && result.text !== null && isClaudeCodePermissionDenialText(result.text)) {
92512
+ this.cancelledToolRuns.push(pending);
92513
+ this.input.writeOutput?.(
92514
+ `agent_bridge_claude_tool_cancellation_observed tool=${pending.name}`
92515
+ );
92516
+ }
92517
+ }
92518
+ if (hadInFlightToolUse && !this.hasInFlightToolUse()) {
92519
+ this.fireToolBoundaryInterrupt();
92417
92520
  }
92418
92521
  }
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");
92522
+ // The pending tool_use set just drained mid-turn. If a steering message is
92523
+ // waiting on that boundary, interrupt the turn now: nothing is executing, so
92524
+ // the cancellation costs only the model's remaining reasoning, and the
92525
+ // turn's terminal result flushes the message. Fire-and-forget the flush at
92526
+ // the turn result owns delivery whether or not this interrupt lands.
92527
+ fireToolBoundaryInterrupt() {
92528
+ if (!this.toolBoundaryWait || this.deferredMessages.length === 0 || !this.hasInterruptibleTurn()) {
92529
+ return;
92427
92530
  }
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
92531
+ this.disarmToolBoundaryWait();
92532
+ this.input.writeOutput?.(
92533
+ `agent_bridge_claude_tool_boundary_interrupt deferred_count=${this.deferredMessages.length}`
92534
+ );
92535
+ void this.interruptActiveTurn();
92536
+ }
92537
+ // Bound the tool-boundary wait: past the budget the steering message is
92538
+ // judged more urgent than the in-flight tool, which is cancelled mid-run.
92539
+ // The cancellation is observed by trackToolUseLifecycle and reported to the
92540
+ // model in the flushed message's platform notice.
92541
+ armToolBoundaryWait() {
92542
+ if (this.toolBoundaryWait) {
92543
+ return;
92544
+ }
92545
+ const timer = setTimeout(() => {
92546
+ this.toolBoundaryWait = null;
92547
+ if (!this.hasInterruptibleTurn() || this.deferredMessages.length === 0) {
92548
+ return;
92549
+ }
92550
+ this.input.writeOutput?.(
92551
+ `agent_bridge_claude_tool_boundary_wait_timeout pending_tools=${[
92552
+ ...this.pendingToolUses.values()
92553
+ ].map((tool) => tool.name).join(",")} deferred_count=${this.deferredMessages.length}`
92446
92554
  );
92447
- timer.unref?.();
92448
- this.turnSettlementWaiters.add(waiter);
92449
- });
92555
+ void this.interruptActiveTurn();
92556
+ }, CLAUDE_TOOL_BOUNDARY_WAIT_TIMEOUT_MS);
92557
+ timer.unref?.();
92558
+ this.toolBoundaryWait = { timer };
92450
92559
  }
92451
- resolveTurnSettlement(settled) {
92452
- if (this.turnSettlementWaiters.size === 0) {
92560
+ disarmToolBoundaryWait() {
92561
+ if (!this.toolBoundaryWait) {
92453
92562
  return;
92454
92563
  }
92455
- const waiters = [...this.turnSettlementWaiters];
92456
- this.turnSettlementWaiters.clear();
92457
- for (const waiter of waiters) {
92458
- waiter(settled);
92459
- }
92564
+ clearTimeout(this.toolBoundaryWait.timer);
92565
+ this.toolBoundaryWait = null;
92460
92566
  }
92461
92567
  flushDeferredMessages() {
92462
92568
  if (this.deferredMessages.length === 0) {
92463
92569
  return;
92464
92570
  }
92465
92571
  const pending = this.deferredMessages.splice(0);
92572
+ const cancelledToolRuns = this.cancelledToolRuns.splice(0);
92573
+ this.disarmToolBoundaryWait();
92466
92574
  this.input.writeOutput?.(
92467
- `agent_bridge_claude_deferred_flush count=${pending.length}`
92575
+ `agent_bridge_claude_deferred_flush count=${pending.length} cancelled_tool_count=${cancelledToolRuns.length}`
92468
92576
  );
92577
+ const text2 = pending.map((entry) => entry.text).join("\n\n");
92469
92578
  this.enqueueUserMessage(
92470
- pending.map((entry) => entry.text).join("\n\n"),
92579
+ cancelledToolRuns.length > 0 ? `${text2}
92580
+
92581
+ ${cancelledToolPlatformNotice(cancelledToolRuns)}` : text2,
92471
92582
  pending.flatMap((entry) => entry.commandIds)
92472
92583
  );
92473
92584
  }
@@ -92488,11 +92599,10 @@ var ClaudeAgentBridgeSessionImpl = class {
92488
92599
  return "proceed";
92489
92600
  }
92490
92601
  const query = this.state.query;
92491
- const hadInFlightToolUse = this.hasInFlightToolUse();
92492
92602
  const settlementBaseline = this.turnResultCount;
92493
92603
  const startedAt = Date.now();
92494
92604
  this.input.writeOutput?.(
92495
- `agent_bridge_claude_mid_turn_interrupt_started at=${new Date(startedAt).toISOString()} in_flight_tool_use=${hadInFlightToolUse}`
92605
+ `agent_bridge_claude_mid_turn_interrupt_started at=${new Date(startedAt).toISOString()} in_flight_tool_use=${this.hasInFlightToolUse()}`
92496
92606
  );
92497
92607
  const interruptPromise = (async () => {
92498
92608
  const ack = await this.requestInterruptAck(query, startedAt);
@@ -92500,16 +92610,6 @@ var ClaudeAgentBridgeSessionImpl = class {
92500
92610
  this.interruptTimeoutTurn = settlementBaseline;
92501
92611
  return "defer";
92502
92612
  }
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
92613
  return "proceed";
92514
92614
  })();
92515
92615
  this.interruptInFlight = interruptPromise;
@@ -92546,6 +92646,7 @@ var ClaudeAgentBridgeSessionImpl = class {
92546
92646
  );
92547
92647
  }, CLAUDE_INTERRUPT_ACK_TIMEOUT_MS);
92548
92648
  timer.unref?.();
92649
+ this.interruptSettling = true;
92549
92650
  query.interrupt().then(
92550
92651
  () => {
92551
92652
  if (done) {
@@ -92902,10 +93003,10 @@ function claudeAgentUserMessage(message) {
92902
93003
  function isClaudeAgentTurnResult(message) {
92903
93004
  return message.type === "result";
92904
93005
  }
92905
- function toolUseIds(message) {
93006
+ function toolUseEntries(message) {
92906
93007
  if (message.type === "assistant") {
92907
93008
  return contentBlocks2(message.message.content).flatMap(
92908
- (block2) => block2.type === "tool_use" && typeof block2.id === "string" ? [block2.id] : []
93009
+ (block2) => block2.type === "tool_use" && typeof block2.id === "string" ? [toolUseEntryFromBlock(block2)] : []
92909
93010
  );
92910
93011
  }
92911
93012
  if (message.type !== "stream_event") {
@@ -92916,15 +93017,53 @@ function toolUseIds(message) {
92916
93017
  return [];
92917
93018
  }
92918
93019
  const block = event.content_block;
92919
- return block.type === "tool_use" && typeof block.id === "string" ? [block.id] : [];
93020
+ return block.type === "tool_use" && typeof block.id === "string" ? [toolUseEntryFromBlock(block)] : [];
92920
93021
  }
92921
- function toolResultIds(message) {
93022
+ function toolUseEntryFromBlock(block) {
93023
+ return {
93024
+ id: String(block.id),
93025
+ name: typeof block.name === "string" ? block.name : "unknown_tool",
93026
+ detail: toolUseDetail(block.input)
93027
+ };
93028
+ }
93029
+ function toolUseDetail(input) {
93030
+ if (input === void 0 || input === null) {
93031
+ return "";
93032
+ }
93033
+ let text2;
93034
+ try {
93035
+ text2 = JSON.stringify(input);
93036
+ } catch {
93037
+ return "";
93038
+ }
93039
+ if (!text2 || text2 === "{}") {
93040
+ return "";
93041
+ }
93042
+ return text2.length > 160 ? `${text2.slice(0, 160)}\u2026` : text2;
93043
+ }
93044
+ function toolResultEntries(message) {
92922
93045
  if (message.type !== "user") {
92923
93046
  return [];
92924
93047
  }
92925
93048
  return contentBlocks2(message.message.content).flatMap(
92926
- (block) => block.type === "tool_result" && typeof block.tool_use_id === "string" ? [block.tool_use_id] : []
93049
+ (block) => block.type === "tool_result" && typeof block.tool_use_id === "string" ? [
93050
+ {
93051
+ id: block.tool_use_id,
93052
+ isError: block.is_error === true,
93053
+ text: typeof block.content === "string" ? block.content : null
93054
+ }
93055
+ ] : []
93056
+ );
93057
+ }
93058
+ function cancelledToolPlatformNotice(cancelled) {
93059
+ const lines = cancelled.map(
93060
+ (tool) => `- ${tool.name}${tool.detail ? ` ${tool.detail}` : ""}`
92927
93061
  );
93062
+ return [
93063
+ "[Auto platform notice] To deliver the message above, the platform cancelled the following in-flight tool call(s) mid-execution:",
93064
+ ...lines,
93065
+ "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."
93066
+ ].join("\n");
92928
93067
  }
92929
93068
  function contentBlocks2(content) {
92930
93069
  if (!Array.isArray(content)) {
@@ -92944,6 +93083,9 @@ var ClaudeCodeCommandHandler = class {
92944
93083
  this.input = input;
92945
93084
  this.claudeConfig = input.claude;
92946
93085
  this.outputBuffer = new AgentBridgeOutputBuffer(input);
93086
+ this.projector = new ClaudeCodeProjector({
93087
+ writeOutput: input.writeOutput
93088
+ });
92947
93089
  this.readState = input.readState ? new ClaudeReadStateTracker(input.readState) : null;
92948
93090
  }
92949
93091
  input;
@@ -92965,7 +93107,7 @@ var ClaudeCodeCommandHandler = class {
92965
93107
  inFlightMessageCommands = /* @__PURE__ */ new Map();
92966
93108
  pendingQuestions = /* @__PURE__ */ new Map();
92967
93109
  outputBuffer;
92968
- projector = new ClaudeCodeProjector();
93110
+ projector;
92969
93111
  readState;
92970
93112
  livenessTicker = null;
92971
93113
  // -----------------------------------------------------------------------------
@@ -93346,7 +93488,9 @@ var ClaudeCodeCommandHandler = class {
93346
93488
  this.trackReadState(activeContext, message);
93347
93489
  let projections;
93348
93490
  try {
93349
- projections = this.projector.project(message);
93491
+ projections = this.projector.project(message, {
93492
+ bridgeInterruptSettling: meta3?.interruptSettling === true
93493
+ });
93350
93494
  } catch (error51) {
93351
93495
  this.input.writeOutput?.(
93352
93496
  `agent_bridge_claude_parse_failed error=${error51 instanceof Error ? error51.message : String(error51)}`
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.450",
49215
49215
  license: "SEE LICENSE IN README.md",
49216
49216
  publishConfig: {
49217
49217
  access: "public"
@@ -62052,6 +62052,10 @@ function jsonRecordString2(value, key) {
62052
62052
  // src/commands/agent-bridge/harness/claude-code/projector.ts
62053
62053
  init_src();
62054
62054
  var ClaudeCodeProjector = class {
62055
+ constructor(input = {}) {
62056
+ this.input = input;
62057
+ }
62058
+ input;
62055
62059
  currentMessageId = null;
62056
62060
  // The turn's UI message id. Claude emits a `message_start` per assistant
62057
62061
  // message, but a tool-using turn spans several (message A with the tool call,
@@ -62070,13 +62074,17 @@ var ClaudeCodeProjector = class {
62070
62074
  // real turn failure (FRA-3548). Cleared on every result and on the next
62071
62075
  // message_start so a stale marker can never reclassify a later failure.
62072
62076
  interruptMarkerSeen = false;
62073
- project(message) {
62077
+ project(message, context = {}) {
62074
62078
  if (isClaudeCodeInterruptMarker(message)) {
62075
62079
  this.interruptMarkerSeen = true;
62076
62080
  }
62077
62081
  if (message.type === "stream_event") {
62078
62082
  return this.projectStreamEvent(message);
62079
62083
  }
62084
+ const interruptContext = {
62085
+ marker: this.interruptMarkerSeen,
62086
+ bridge: context.bridgeInterruptSettling === true
62087
+ };
62080
62088
  const parsed = parseClaudeCodeStreamRecord(message);
62081
62089
  const snapshotMessageId = assistantSnapshotMessageId(message);
62082
62090
  const suppressStreamed = snapshotMessageId !== null && this.streamedMessageIds.has(snapshotMessageId);
@@ -62088,18 +62096,24 @@ var ClaudeCodeProjector = class {
62088
62096
  return [];
62089
62097
  }
62090
62098
  return conversationProjectionToUiChunks(entry, {
62091
- interrupted: this.interruptMarkerSeen
62099
+ interruptContext,
62100
+ emitDiagnostic: this.input.writeOutput
62092
62101
  });
62093
62102
  });
62094
62103
  if (parsed.result) {
62095
62104
  outputs.push(...this.endActiveParts(), ...this.finishActiveToolInputs());
62096
62105
  this.currentMessageId = null;
62097
62106
  this.activeResponseMessageId = null;
62098
- outputs.push(
62099
- projectClaudeCodeResult(parsed.result, {
62100
- interrupted: this.isInterruptCancellation(parsed.result)
62101
- })
62107
+ const interrupted = this.isInterruptCancellation(
62108
+ parsed.result,
62109
+ interruptContext
62102
62110
  );
62111
+ if (interrupted) {
62112
+ this.input.writeOutput?.(
62113
+ `agent_bridge_claude_result_interrupt_cancellation gate=${interruptGate(interruptContext)}`
62114
+ );
62115
+ }
62116
+ outputs.push(projectClaudeCodeResult(parsed.result, { interrupted }));
62103
62117
  this.interruptMarkerSeen = false;
62104
62118
  }
62105
62119
  return outputs;
@@ -62109,9 +62123,11 @@ var ClaudeCodeProjector = class {
62109
62123
  // surfaces as a diagnostic-only `error_during_execution`. Treating that as a
62110
62124
  // turn failure fed the FRA-3548 redelivery loop (the bridge redelivered a
62111
62125
  // 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);
62126
+ // interrupt context — the SDK's own marker, or the session's knowledge that
62127
+ // a bridge interrupt is settling (which covers a missing or late marker)
62128
+ // is classified as a cancellation instead.
62129
+ isInterruptCancellation(result, context) {
62130
+ return result.isError && (context.marker || context.bridge) && isClaudeCodeEdeDiagnosticOnlyError(result.errorMessage);
62115
62131
  }
62116
62132
  flushPendingAssistantMessages() {
62117
62133
  const outputs = [
@@ -62232,7 +62248,8 @@ var ClaudeCodeProjector = class {
62232
62248
  const messageChunks = this.assistantSnapshotMessageChunks(projections);
62233
62249
  const rest = projections.filter((projection) => !isAssistantMessageOrToolCall(projection)).flatMap(
62234
62250
  (projection) => conversationProjectionToUiChunks(projection, {
62235
- interrupted: this.interruptMarkerSeen
62251
+ interruptContext: { marker: this.interruptMarkerSeen, bridge: false },
62252
+ emitDiagnostic: this.input.writeOutput
62236
62253
  })
62237
62254
  );
62238
62255
  const last = messageChunks.at(-1);
@@ -62456,7 +62473,7 @@ function textContentFromDelta(delta) {
62456
62473
  return null;
62457
62474
  }
62458
62475
  }
62459
- function conversationProjectionToUiChunks(projection, options = { interrupted: false }) {
62476
+ function conversationProjectionToUiChunks(projection, options) {
62460
62477
  if (projection.role === "assistant" && projection.kind === "message") {
62461
62478
  return messageContentChunks(projection);
62462
62479
  }
@@ -62477,15 +62494,12 @@ function conversationProjectionToUiChunks(projection, options = { interrupted: f
62477
62494
  })
62478
62495
  ];
62479
62496
  }
62480
- const remapped = typeof part.output === "string" ? remapClaudeCodeInterruptToolResultErrorText(part.output, {
62481
- interrupted: options.interrupted
62482
- }) : null;
62483
62497
  return [
62484
- uiChunk({
62485
- type: "tool-output-error",
62486
- toolCallId: part.toolUseId ?? UNKNOWN_MESSAGE_ID,
62487
- errorText: remapped ?? JSON.stringify(part.output)
62488
- })
62498
+ toolResultErrorChunk(
62499
+ part.toolUseId ?? UNKNOWN_MESSAGE_ID,
62500
+ part.output,
62501
+ options
62502
+ )
62489
62503
  ];
62490
62504
  });
62491
62505
  }
@@ -62529,6 +62543,32 @@ function conversationProjectionToUiChunks(projection, options = { interrupted: f
62529
62543
  }
62530
62544
  ];
62531
62545
  }
62546
+ function toolResultErrorChunk(toolCallId, output, options) {
62547
+ const context = options.interruptContext;
62548
+ const remapped = typeof output === "string" ? remapClaudeCodeInterruptToolResultErrorText(output, {
62549
+ interrupted: context.marker || context.bridge
62550
+ }) : null;
62551
+ if (remapped !== null) {
62552
+ options.emitDiagnostic?.(
62553
+ `agent_bridge_claude_tool_cancellation_remapped tool_use_id=${toolCallId} gate=${interruptGate(context)}`
62554
+ );
62555
+ } else if (typeof output === "string" && isClaudeCodePermissionDenialText(output)) {
62556
+ options.emitDiagnostic?.(
62557
+ `agent_bridge_claude_permission_denial_passthrough tool_use_id=${toolCallId}`
62558
+ );
62559
+ }
62560
+ return uiChunk({
62561
+ type: "tool-output-error",
62562
+ toolCallId,
62563
+ errorText: remapped ?? JSON.stringify(output)
62564
+ });
62565
+ }
62566
+ function interruptGate(context) {
62567
+ if (context.marker && context.bridge) {
62568
+ return "marker+bridge";
62569
+ }
62570
+ return context.marker ? "marker" : "bridge";
62571
+ }
62532
62572
  function isAssistantMessageOrToolCall(projection) {
62533
62573
  return projection.role === "assistant" && (projection.kind === "message" || projection.kind === "tool_call");
62534
62574
  }
@@ -62807,6 +62847,7 @@ function parseReadStateRecord(raw) {
62807
62847
  }
62808
62848
 
62809
62849
  // src/commands/agent-bridge/harness/claude-code/session.ts
62850
+ init_src();
62810
62851
  import {
62811
62852
  startup as startupClaudeAgent
62812
62853
  } from "@anthropic-ai/claude-agent-sdk";
@@ -63001,7 +63042,7 @@ function withClaudeStderrDiagnosticsPointer(error51, capturedStderr) {
63001
63042
 
63002
63043
  // src/commands/agent-bridge/harness/claude-code/session.ts
63003
63044
  var CLAUDE_AGENT_STARTUP_TIMEOUT_MS = 3e4;
63004
- var CLAUDE_INTERRUPT_SETTLE_TIMEOUT_MS = 1e4;
63045
+ var CLAUDE_TOOL_BOUNDARY_WAIT_TIMEOUT_MS = 12e4;
63005
63046
  var CLAUDE_INTERRUPT_ACK_TIMEOUT_MS = 1e4;
63006
63047
  var CLAUDE_MCP_REGISTRATION_TIMEOUT_MS = 3e4;
63007
63048
  var CLAUDE_MCP_REGISTRATION_POLL_INTERVAL_MS = 100;
@@ -63077,16 +63118,33 @@ var ClaudeAgentBridgeSessionImpl = class {
63077
63118
  // flushed command id.
63078
63119
  openTurnCommandIds = [];
63079
63120
  // 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
63121
+ // observed, with the name/detail needed to describe them in a cancellation
63122
+ // notice. A non-empty map means a tool call is in flight: interrupting now
63123
+ // would destroy its work and record a denial-worded cancellation, and
63124
+ // injecting now would append the new user message after an unresolved
63082
63125
  // stop_reason=tool_use — the invalid transcript that trips Claude Code's
63083
63126
  // 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.
63127
+ pendingToolUses = /* @__PURE__ */ new Map();
63128
+ // Incremented on every turn-terminal `result`; the interrupt-timeout latch
63129
+ // compares against it to tell whether the same wedged turn is still running.
63088
63130
  turnResultCount = 0;
63089
- turnSettlementWaiters = /* @__PURE__ */ new Set();
63131
+ // A steering message is waiting for the in-flight tool call to settle so the
63132
+ // turn can be interrupted at the tool boundary instead of mid-tool. Armed by
63133
+ // sendMessage, fired by the output pump when the pending tool_use set
63134
+ // drains, bounded by the budget timer below.
63135
+ toolBoundaryWait = null;
63136
+ // True from the moment a bridge-initiated interrupt control request fires
63137
+ // until the targeted turn emits its terminal result (or the stream ends).
63138
+ // Reported to the projector on every message so the SDK's denial-worded
63139
+ // cancellation records are classified as platform cancellations even though
63140
+ // the SDK's own interrupt marker arrives after them (probe-verified wire
63141
+ // order, 2026-07-13: tool_result → marker → error_during_execution result).
63142
+ interruptSettling = false;
63143
+ // Tool calls the SDK cancelled while a bridge-initiated interrupt was
63144
+ // settling. Consumed by the deferred flush: the injected message gets a
63145
+ // platform-cancellation notice naming this work so the model re-runs it
63146
+ // instead of treating the denial-worded tool_result as a human rejection.
63147
+ cancelledToolRuns = [];
63090
63148
  // Memoized MCP-registration gate, keyed by the attached query. A runtime
63091
63149
  // restart warms a brand-new session + query, so keying off the query object
63092
63150
  // (rather than a session-lifetime flag) re-arms the gate on every restart:
@@ -63145,18 +63203,34 @@ var ClaudeAgentBridgeSessionImpl = class {
63145
63203
  delivery_mode: mode,
63146
63204
  state: this.state.kind,
63147
63205
  active_turn_count: this.activeTurnCount,
63148
- pending_tool_use_count: this.pendingToolUseIds.size,
63206
+ pending_tool_use_count: this.pendingToolUses.size,
63149
63207
  deferred_count: this.deferredMessages.length
63150
63208
  });
63151
63209
  if (mode === "deferred" && this.hasInterruptibleTurn()) {
63152
63210
  this.deferredMessages.push({ text: message, commandIds });
63153
63211
  this.input.runtimeLogger?.info("agent_bridge_claude_message_deferred", {
63154
63212
  active_turn_count: this.activeTurnCount,
63155
- pending_tool_use_count: this.pendingToolUseIds.size,
63213
+ pending_tool_use_count: this.pendingToolUses.size,
63156
63214
  deferred_count: this.deferredMessages.length
63157
63215
  });
63158
63216
  return;
63159
63217
  }
63218
+ if (this.hasInterruptibleTurn() && this.hasInFlightToolUse()) {
63219
+ this.deferredMessages.push({ text: message, commandIds });
63220
+ this.armToolBoundaryWait();
63221
+ this.input.runtimeLogger?.info(
63222
+ "agent_bridge_claude_message_deferred_tool_boundary",
63223
+ {
63224
+ active_turn_count: this.activeTurnCount,
63225
+ pending_tool_use_count: this.pendingToolUses.size,
63226
+ pending_tools: [...this.pendingToolUses.values()].map(
63227
+ (tool) => tool.name
63228
+ ),
63229
+ deferred_count: this.deferredMessages.length
63230
+ }
63231
+ );
63232
+ return;
63233
+ }
63160
63234
  const interruption = await this.interruptActiveTurnBeforeMessage();
63161
63235
  if (interruption === "defer" && this.hasInterruptibleTurn()) {
63162
63236
  this.deferredMessages.push({ text: message, commandIds });
@@ -63164,7 +63238,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63164
63238
  "agent_bridge_claude_message_deferred_after_interrupt_timeout",
63165
63239
  {
63166
63240
  active_turn_count: this.activeTurnCount,
63167
- pending_tool_use_count: this.pendingToolUseIds.size,
63241
+ pending_tool_use_count: this.pendingToolUses.size,
63168
63242
  deferred_count: this.deferredMessages.length
63169
63243
  }
63170
63244
  );
@@ -63177,7 +63251,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63177
63251
  duration_ms: Date.now() - startedAt,
63178
63252
  state: this.state.kind,
63179
63253
  active_turn_count: this.activeTurnCount,
63180
- pending_tool_use_count: this.pendingToolUseIds.size,
63254
+ pending_tool_use_count: this.pendingToolUses.size,
63181
63255
  deferred_count: this.deferredMessages.length
63182
63256
  });
63183
63257
  }
@@ -63261,8 +63335,10 @@ var ClaudeAgentBridgeSessionImpl = class {
63261
63335
  }
63262
63336
  this.state = { kind: "closed" };
63263
63337
  this.activeTurnCount = 0;
63264
- this.pendingToolUseIds.clear();
63265
- this.resolveTurnSettlement(false);
63338
+ this.pendingToolUses.clear();
63339
+ this.cancelledToolRuns = [];
63340
+ this.interruptSettling = false;
63341
+ this.disarmToolBoundaryWait();
63266
63342
  if (this.deferredMessages.length > 0) {
63267
63343
  this.input.writeOutput?.(
63268
63344
  `agent_bridge_claude_deferred_dropped count=${this.deferredMessages.length} reason=session_closed`
@@ -63353,19 +63429,21 @@ var ClaudeAgentBridgeSessionImpl = class {
63353
63429
  void (async () => {
63354
63430
  try {
63355
63431
  for await (const message of query) {
63432
+ const interruptSettling = this.interruptSettling;
63356
63433
  this.trackToolUseLifecycle(message);
63357
- let meta3;
63434
+ let meta3 = { interruptSettling };
63358
63435
  if (isClaudeAgentTurnResult(message)) {
63359
63436
  this.activeTurnCount = Math.max(0, this.activeTurnCount - 1);
63360
63437
  meta3 = {
63438
+ interruptSettling,
63361
63439
  consumedCommandIds: this.openTurnCommandIds.shift() ?? []
63362
63440
  };
63363
- this.pendingToolUseIds.clear();
63441
+ this.pendingToolUses.clear();
63442
+ this.interruptSettling = false;
63364
63443
  this.turnResultCount += 1;
63365
- this.resolveTurnSettlement(true);
63366
63444
  this.input.runtimeLogger?.info("agent_bridge_claude_turn_result", {
63367
63445
  active_turn_count: this.activeTurnCount,
63368
- pending_tool_use_count: this.pendingToolUseIds.size,
63446
+ pending_tool_use_count: this.pendingToolUses.size,
63369
63447
  turn_result_count: this.turnResultCount,
63370
63448
  deferred_count: this.deferredMessages.length,
63371
63449
  consumed_command_ids: meta3.consumedCommandIds
@@ -63373,6 +63451,9 @@ var ClaudeAgentBridgeSessionImpl = class {
63373
63451
  if (this.activeTurnCount === 0) {
63374
63452
  this.flushDeferredMessages();
63375
63453
  }
63454
+ if (this.deferredMessages.length === 0) {
63455
+ this.cancelledToolRuns = [];
63456
+ }
63376
63457
  }
63377
63458
  const failure = probeClaudeAgentMessage(
63378
63459
  message,
@@ -63397,8 +63478,9 @@ var ClaudeAgentBridgeSessionImpl = class {
63397
63478
  } finally {
63398
63479
  this.activeTurnCount = 0;
63399
63480
  this.openTurnCommandIds.length = 0;
63400
- this.pendingToolUseIds.clear();
63401
- this.resolveTurnSettlement(false);
63481
+ this.pendingToolUses.clear();
63482
+ this.interruptSettling = false;
63483
+ this.disarmToolBoundaryWait();
63402
63484
  this.state = { kind: "closed" };
63403
63485
  this.reportExit();
63404
63486
  }
@@ -63415,7 +63497,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63415
63497
  this.input.runtimeLogger?.info("agent_bridge_claude_message_enqueued", {
63416
63498
  state: this.state.kind,
63417
63499
  active_turn_count: this.activeTurnCount,
63418
- pending_tool_use_count: this.pendingToolUseIds.size,
63500
+ pending_tool_use_count: this.pendingToolUses.size,
63419
63501
  deferred_count: this.deferredMessages.length
63420
63502
  });
63421
63503
  void this.ensureRunningQuery().catch((error51) => {
@@ -63428,75 +63510,105 @@ var ClaudeAgentBridgeSessionImpl = class {
63428
63510
  return this.activeTurnCount > 0 && this.state.kind === "running";
63429
63511
  }
63430
63512
  // 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
63513
+ // tool_result yet — the window where an immediate interrupt would cancel
63514
+ // real tool work, and where an immediate injection would strand an
63432
63515
  // unresolved stop_reason=tool_use ahead of the injected user message.
63433
63516
  hasInFlightToolUse() {
63434
- return this.pendingToolUseIds.size > 0;
63517
+ return this.pendingToolUses.size > 0;
63435
63518
  }
63436
63519
  // Track tool_use/tool_result pairs as they stream so hasInFlightToolUse()
63437
63520
  // reflects the live transcript. A tool_use may appear first as a streaming
63438
63521
  // content block before the full assistant message is available; the matching
63439
63522
  // tool_result closes it; the turn's terminal result clears any remainder.
63523
+ // Two side jobs ride on the same observation point:
63524
+ // - a denial-worded error tool_result arriving while a bridge interrupt is
63525
+ // settling is that interrupt's cancellation of the tool — record it so the
63526
+ // flushed message can tell the model to re-run the cancelled work;
63527
+ // - a tool_result that drains the pending set is the tool boundary an armed
63528
+ // steering message has been waiting for — interrupt the turn now, while no
63529
+ // tool work is at risk.
63440
63530
  trackToolUseLifecycle(message) {
63441
- for (const id of toolUseIds(message)) {
63442
- this.pendingToolUseIds.add(id);
63531
+ for (const entry of toolUseEntries(message)) {
63532
+ this.pendingToolUses.set(entry.id, {
63533
+ name: entry.name,
63534
+ detail: entry.detail
63535
+ });
63536
+ }
63537
+ const hadInFlightToolUse = this.hasInFlightToolUse();
63538
+ for (const result of toolResultEntries(message)) {
63539
+ const pending = this.pendingToolUses.get(result.id);
63540
+ this.pendingToolUses.delete(result.id);
63541
+ if (pending && this.interruptSettling && result.isError && result.text !== null && isClaudeCodePermissionDenialText(result.text)) {
63542
+ this.cancelledToolRuns.push(pending);
63543
+ this.input.writeOutput?.(
63544
+ `agent_bridge_claude_tool_cancellation_observed tool=${pending.name}`
63545
+ );
63546
+ }
63443
63547
  }
63444
- for (const id of toolResultIds(message)) {
63445
- this.pendingToolUseIds.delete(id);
63548
+ if (hadInFlightToolUse && !this.hasInFlightToolUse()) {
63549
+ this.fireToolBoundaryInterrupt();
63446
63550
  }
63447
63551
  }
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");
63552
+ // The pending tool_use set just drained mid-turn. If a steering message is
63553
+ // waiting on that boundary, interrupt the turn now: nothing is executing, so
63554
+ // the cancellation costs only the model's remaining reasoning, and the
63555
+ // turn's terminal result flushes the message. Fire-and-forget the flush at
63556
+ // the turn result owns delivery whether or not this interrupt lands.
63557
+ fireToolBoundaryInterrupt() {
63558
+ if (!this.toolBoundaryWait || this.deferredMessages.length === 0 || !this.hasInterruptibleTurn()) {
63559
+ return;
63456
63560
  }
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
63561
+ this.disarmToolBoundaryWait();
63562
+ this.input.writeOutput?.(
63563
+ `agent_bridge_claude_tool_boundary_interrupt deferred_count=${this.deferredMessages.length}`
63564
+ );
63565
+ void this.interruptActiveTurn();
63566
+ }
63567
+ // Bound the tool-boundary wait: past the budget the steering message is
63568
+ // judged more urgent than the in-flight tool, which is cancelled mid-run.
63569
+ // The cancellation is observed by trackToolUseLifecycle and reported to the
63570
+ // model in the flushed message's platform notice.
63571
+ armToolBoundaryWait() {
63572
+ if (this.toolBoundaryWait) {
63573
+ return;
63574
+ }
63575
+ const timer = setTimeout(() => {
63576
+ this.toolBoundaryWait = null;
63577
+ if (!this.hasInterruptibleTurn() || this.deferredMessages.length === 0) {
63578
+ return;
63579
+ }
63580
+ this.input.writeOutput?.(
63581
+ `agent_bridge_claude_tool_boundary_wait_timeout pending_tools=${[
63582
+ ...this.pendingToolUses.values()
63583
+ ].map((tool) => tool.name).join(",")} deferred_count=${this.deferredMessages.length}`
63475
63584
  );
63476
- timer.unref?.();
63477
- this.turnSettlementWaiters.add(waiter);
63478
- });
63585
+ void this.interruptActiveTurn();
63586
+ }, CLAUDE_TOOL_BOUNDARY_WAIT_TIMEOUT_MS);
63587
+ timer.unref?.();
63588
+ this.toolBoundaryWait = { timer };
63479
63589
  }
63480
- resolveTurnSettlement(settled) {
63481
- if (this.turnSettlementWaiters.size === 0) {
63590
+ disarmToolBoundaryWait() {
63591
+ if (!this.toolBoundaryWait) {
63482
63592
  return;
63483
63593
  }
63484
- const waiters = [...this.turnSettlementWaiters];
63485
- this.turnSettlementWaiters.clear();
63486
- for (const waiter of waiters) {
63487
- waiter(settled);
63488
- }
63594
+ clearTimeout(this.toolBoundaryWait.timer);
63595
+ this.toolBoundaryWait = null;
63489
63596
  }
63490
63597
  flushDeferredMessages() {
63491
63598
  if (this.deferredMessages.length === 0) {
63492
63599
  return;
63493
63600
  }
63494
63601
  const pending = this.deferredMessages.splice(0);
63602
+ const cancelledToolRuns = this.cancelledToolRuns.splice(0);
63603
+ this.disarmToolBoundaryWait();
63495
63604
  this.input.writeOutput?.(
63496
- `agent_bridge_claude_deferred_flush count=${pending.length}`
63605
+ `agent_bridge_claude_deferred_flush count=${pending.length} cancelled_tool_count=${cancelledToolRuns.length}`
63497
63606
  );
63607
+ const text = pending.map((entry) => entry.text).join("\n\n");
63498
63608
  this.enqueueUserMessage(
63499
- pending.map((entry) => entry.text).join("\n\n"),
63609
+ cancelledToolRuns.length > 0 ? `${text}
63610
+
63611
+ ${cancelledToolPlatformNotice(cancelledToolRuns)}` : text,
63500
63612
  pending.flatMap((entry) => entry.commandIds)
63501
63613
  );
63502
63614
  }
@@ -63517,11 +63629,10 @@ var ClaudeAgentBridgeSessionImpl = class {
63517
63629
  return "proceed";
63518
63630
  }
63519
63631
  const query = this.state.query;
63520
- const hadInFlightToolUse = this.hasInFlightToolUse();
63521
63632
  const settlementBaseline = this.turnResultCount;
63522
63633
  const startedAt = Date.now();
63523
63634
  this.input.writeOutput?.(
63524
- `agent_bridge_claude_mid_turn_interrupt_started at=${new Date(startedAt).toISOString()} in_flight_tool_use=${hadInFlightToolUse}`
63635
+ `agent_bridge_claude_mid_turn_interrupt_started at=${new Date(startedAt).toISOString()} in_flight_tool_use=${this.hasInFlightToolUse()}`
63525
63636
  );
63526
63637
  const interruptPromise = (async () => {
63527
63638
  const ack = await this.requestInterruptAck(query, startedAt);
@@ -63529,16 +63640,6 @@ var ClaudeAgentBridgeSessionImpl = class {
63529
63640
  this.interruptTimeoutTurn = settlementBaseline;
63530
63641
  return "defer";
63531
63642
  }
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
63643
  return "proceed";
63543
63644
  })();
63544
63645
  this.interruptInFlight = interruptPromise;
@@ -63575,6 +63676,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63575
63676
  );
63576
63677
  }, CLAUDE_INTERRUPT_ACK_TIMEOUT_MS);
63577
63678
  timer.unref?.();
63679
+ this.interruptSettling = true;
63578
63680
  query.interrupt().then(
63579
63681
  () => {
63580
63682
  if (done) {
@@ -63931,10 +64033,10 @@ function claudeAgentUserMessage(message) {
63931
64033
  function isClaudeAgentTurnResult(message) {
63932
64034
  return message.type === "result";
63933
64035
  }
63934
- function toolUseIds(message) {
64036
+ function toolUseEntries(message) {
63935
64037
  if (message.type === "assistant") {
63936
64038
  return contentBlocks2(message.message.content).flatMap(
63937
- (block2) => block2.type === "tool_use" && typeof block2.id === "string" ? [block2.id] : []
64039
+ (block2) => block2.type === "tool_use" && typeof block2.id === "string" ? [toolUseEntryFromBlock(block2)] : []
63938
64040
  );
63939
64041
  }
63940
64042
  if (message.type !== "stream_event") {
@@ -63945,16 +64047,54 @@ function toolUseIds(message) {
63945
64047
  return [];
63946
64048
  }
63947
64049
  const block = event.content_block;
63948
- return block.type === "tool_use" && typeof block.id === "string" ? [block.id] : [];
64050
+ return block.type === "tool_use" && typeof block.id === "string" ? [toolUseEntryFromBlock(block)] : [];
63949
64051
  }
63950
- function toolResultIds(message) {
64052
+ function toolUseEntryFromBlock(block) {
64053
+ return {
64054
+ id: String(block.id),
64055
+ name: typeof block.name === "string" ? block.name : "unknown_tool",
64056
+ detail: toolUseDetail(block.input)
64057
+ };
64058
+ }
64059
+ function toolUseDetail(input) {
64060
+ if (input === void 0 || input === null) {
64061
+ return "";
64062
+ }
64063
+ let text;
64064
+ try {
64065
+ text = JSON.stringify(input);
64066
+ } catch {
64067
+ return "";
64068
+ }
64069
+ if (!text || text === "{}") {
64070
+ return "";
64071
+ }
64072
+ return text.length > 160 ? `${text.slice(0, 160)}\u2026` : text;
64073
+ }
64074
+ function toolResultEntries(message) {
63951
64075
  if (message.type !== "user") {
63952
64076
  return [];
63953
64077
  }
63954
64078
  return contentBlocks2(message.message.content).flatMap(
63955
- (block) => block.type === "tool_result" && typeof block.tool_use_id === "string" ? [block.tool_use_id] : []
64079
+ (block) => block.type === "tool_result" && typeof block.tool_use_id === "string" ? [
64080
+ {
64081
+ id: block.tool_use_id,
64082
+ isError: block.is_error === true,
64083
+ text: typeof block.content === "string" ? block.content : null
64084
+ }
64085
+ ] : []
63956
64086
  );
63957
64087
  }
64088
+ function cancelledToolPlatformNotice(cancelled) {
64089
+ const lines = cancelled.map(
64090
+ (tool) => `- ${tool.name}${tool.detail ? ` ${tool.detail}` : ""}`
64091
+ );
64092
+ return [
64093
+ "[Auto platform notice] To deliver the message above, the platform cancelled the following in-flight tool call(s) mid-execution:",
64094
+ ...lines,
64095
+ "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."
64096
+ ].join("\n");
64097
+ }
63958
64098
  function contentBlocks2(content) {
63959
64099
  if (!Array.isArray(content)) {
63960
64100
  return [];
@@ -63973,6 +64113,9 @@ var ClaudeCodeCommandHandler = class {
63973
64113
  this.input = input;
63974
64114
  this.claudeConfig = input.claude;
63975
64115
  this.outputBuffer = new AgentBridgeOutputBuffer(input);
64116
+ this.projector = new ClaudeCodeProjector({
64117
+ writeOutput: input.writeOutput
64118
+ });
63976
64119
  this.readState = input.readState ? new ClaudeReadStateTracker(input.readState) : null;
63977
64120
  }
63978
64121
  input;
@@ -63994,7 +64137,7 @@ var ClaudeCodeCommandHandler = class {
63994
64137
  inFlightMessageCommands = /* @__PURE__ */ new Map();
63995
64138
  pendingQuestions = /* @__PURE__ */ new Map();
63996
64139
  outputBuffer;
63997
- projector = new ClaudeCodeProjector();
64140
+ projector;
63998
64141
  readState;
63999
64142
  livenessTicker = null;
64000
64143
  // -----------------------------------------------------------------------------
@@ -64375,7 +64518,9 @@ var ClaudeCodeCommandHandler = class {
64375
64518
  this.trackReadState(activeContext, message);
64376
64519
  let projections;
64377
64520
  try {
64378
- projections = this.projector.project(message);
64521
+ projections = this.projector.project(message, {
64522
+ bridgeInterruptSettling: meta3?.interruptSettling === true
64523
+ });
64379
64524
  } catch (error51) {
64380
64525
  this.input.writeOutput?.(
64381
64526
  `agent_bridge_claude_parse_failed error=${error51 instanceof Error ? error51.message : String(error51)}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autohq/cli",
3
- "version": "0.1.449",
3
+ "version": "0.1.450",
4
4
  "license": "SEE LICENSE IN README.md",
5
5
  "publishConfig": {
6
6
  "access": "public"