@autohq/cli 0.1.448 → 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.
package/dist/index.js CHANGED
@@ -20694,34 +20694,26 @@ var init_pricing = __esm({
20694
20694
  });
20695
20695
 
20696
20696
  // ../../packages/schemas/src/platform-usage-feed.ts
20697
- var SafeTelemetryTextSchema, PlatformUsageUserSchema, PlatformUsageSessionActorSchema, PlatformUsageScopeSchema, ScopedUserEventSchema, PlatformUsageEventSchema;
20697
+ var SafeTelemetryTextSchema, PlatformUsageActorSchema, PlatformUsageAgentSchema, PlatformUsageScopeSchema, ScopedActorEventSchema, PlatformUsageEventSchema;
20698
20698
  var init_platform_usage_feed = __esm({
20699
20699
  "../../packages/schemas/src/platform-usage-feed.ts"() {
20700
20700
  "use strict";
20701
20701
  init_zod();
20702
+ init_agents();
20702
20703
  SafeTelemetryTextSchema = external_exports.string().transform(
20703
20704
  (value) => [...value].map((character) => {
20704
20705
  const codePoint = character.codePointAt(0) ?? 0;
20705
20706
  return codePoint < 32 || codePoint === 127 ? " " : character;
20706
20707
  }).join("").replace(/\s+/g, " ").trim().slice(0, 300).trim()
20707
20708
  ).pipe(external_exports.string().min(1).max(300));
20708
- PlatformUsageUserSchema = external_exports.object({
20709
- id: external_exports.string().trim().min(1).max(200),
20710
- displayName: SafeTelemetryTextSchema.nullable(),
20711
- email: SafeTelemetryTextSchema.nullable()
20712
- });
20713
- PlatformUsageSessionActorSchema = external_exports.discriminatedUnion("kind", [
20714
- external_exports.object({
20715
- kind: external_exports.literal("user"),
20716
- user: PlatformUsageUserSchema
20717
- }),
20718
- external_exports.object({
20719
- kind: external_exports.literal("provider"),
20720
- provider: SafeTelemetryTextSchema,
20721
- externalId: external_exports.string().trim().min(1).max(200),
20722
- displayName: SafeTelemetryTextSchema.nullable()
20723
- })
20724
- ]);
20709
+ PlatformUsageActorSchema = external_exports.object({
20710
+ displayName: SafeTelemetryTextSchema,
20711
+ source: SafeTelemetryTextSchema.nullable()
20712
+ }).strict();
20713
+ PlatformUsageAgentSchema = external_exports.object({
20714
+ displayName: SafeTelemetryTextSchema,
20715
+ model: SafeTelemetryTextSchema.nullable()
20716
+ }).strict();
20725
20717
  PlatformUsageScopeSchema = external_exports.object({
20726
20718
  environment: SafeTelemetryTextSchema,
20727
20719
  organizationId: external_exports.string().trim().min(1).max(200),
@@ -20729,13 +20721,13 @@ var init_platform_usage_feed = __esm({
20729
20721
  projectId: external_exports.string().trim().min(1).max(200).nullable(),
20730
20722
  projectName: SafeTelemetryTextSchema.nullable()
20731
20723
  });
20732
- ScopedUserEventSchema = external_exports.object({
20733
- user: PlatformUsageUserSchema,
20724
+ ScopedActorEventSchema = external_exports.object({
20725
+ actor: PlatformUsageActorSchema,
20734
20726
  scope: PlatformUsageScopeSchema
20735
20727
  });
20736
20728
  PlatformUsageEventSchema = external_exports.discriminatedUnion("kind", [
20737
- ScopedUserEventSchema.extend({ kind: external_exports.literal("user.signup") }),
20738
- ScopedUserEventSchema.extend({
20729
+ ScopedActorEventSchema.extend({ kind: external_exports.literal("user.signup") }),
20730
+ ScopedActorEventSchema.extend({
20739
20731
  kind: external_exports.literal("onboarding.lifecycle"),
20740
20732
  stage: external_exports.enum(["team_selected", "github_connected", "team_applied"]),
20741
20733
  team: SafeTelemetryTextSchema,
@@ -20745,23 +20737,21 @@ var init_platform_usage_feed = __esm({
20745
20737
  failureSummary: SafeTelemetryTextSchema.nullable()
20746
20738
  }).nullable()
20747
20739
  }),
20748
- ScopedUserEventSchema.extend({
20740
+ ScopedActorEventSchema.extend({
20749
20741
  kind: external_exports.literal("agent.created"),
20750
- agentName: SafeTelemetryTextSchema
20742
+ agent: PlatformUsageAgentSchema
20751
20743
  }),
20752
- external_exports.object({
20744
+ ScopedActorEventSchema.extend({
20753
20745
  kind: external_exports.literal("session.started"),
20754
- actor: PlatformUsageSessionActorSchema,
20755
- scope: PlatformUsageScopeSchema,
20756
- agentName: SafeTelemetryTextSchema,
20746
+ agent: PlatformUsageAgentSchema,
20757
20747
  sessionId: external_exports.string().trim().min(1).max(200)
20758
20748
  }),
20759
- ScopedUserEventSchema.extend({
20749
+ ScopedActorEventSchema.extend({
20760
20750
  kind: external_exports.literal("credits.added"),
20761
20751
  amountUsd: external_exports.number().positive().finite(),
20762
20752
  source: external_exports.enum(["purchase", "auto_topup", "adjustment"])
20763
20753
  }),
20764
- ScopedUserEventSchema.extend({
20754
+ ScopedActorEventSchema.extend({
20765
20755
  kind: external_exports.literal("credits.exhausted"),
20766
20756
  autoReloadOutcome: external_exports.enum([
20767
20757
  "not_enabled",
@@ -27217,6 +27207,15 @@ triggers:
27217
27207
  content: "harness: claude-code\nenvironment:\n name: auto-default-runtime\n image:\n kind: preset\n name: node24\nidentity:\n displayName: Default\n username: default\n description: Auto's built-in general-purpose agent.\ntools:\n auto:\n kind: local\n implementation: auto\n"
27218
27208
  }
27219
27209
  ]
27210
+ },
27211
+ {
27212
+ version: "1.1.0",
27213
+ files: [
27214
+ {
27215
+ path: "fragments/default.yaml",
27216
+ content: "# 1.1.0 bakes the built-in catalog avatar (default.png) into Default's identity so\n# every provisioned agent/default shows the house face instead of an initials\n# fallback. Otherwise byte-identical to 1.0.0.\nharness: claude-code\nenvironment:\n name: auto-default-runtime\n image:\n kind: preset\n name: node24\nidentity:\n displayName: Default\n username: default\n avatar:\n asset: .auto/assets/default.png\n sha256: a5dd97676173a83dfc6fb9bdf30e7f50c7392f9e382fca40a23d6ab9285e9bf2\n description: Auto's built-in general-purpose agent.\ntools:\n auto:\n kind: local\n implementation: auto\n"
27217
+ }
27218
+ ]
27220
27219
  }
27221
27220
  ],
27222
27221
  "@auto/engineering-tier": [
@@ -49212,7 +49211,7 @@ var init_package = __esm({
49212
49211
  "package.json"() {
49213
49212
  package_default = {
49214
49213
  name: "@autohq/cli",
49215
- version: "0.1.448",
49214
+ version: "0.1.450",
49216
49215
  license: "SEE LICENSE IN README.md",
49217
49216
  publishConfig: {
49218
49217
  access: "public"
@@ -62053,6 +62052,10 @@ function jsonRecordString2(value, key) {
62053
62052
  // src/commands/agent-bridge/harness/claude-code/projector.ts
62054
62053
  init_src();
62055
62054
  var ClaudeCodeProjector = class {
62055
+ constructor(input = {}) {
62056
+ this.input = input;
62057
+ }
62058
+ input;
62056
62059
  currentMessageId = null;
62057
62060
  // The turn's UI message id. Claude emits a `message_start` per assistant
62058
62061
  // message, but a tool-using turn spans several (message A with the tool call,
@@ -62071,13 +62074,17 @@ var ClaudeCodeProjector = class {
62071
62074
  // real turn failure (FRA-3548). Cleared on every result and on the next
62072
62075
  // message_start so a stale marker can never reclassify a later failure.
62073
62076
  interruptMarkerSeen = false;
62074
- project(message) {
62077
+ project(message, context = {}) {
62075
62078
  if (isClaudeCodeInterruptMarker(message)) {
62076
62079
  this.interruptMarkerSeen = true;
62077
62080
  }
62078
62081
  if (message.type === "stream_event") {
62079
62082
  return this.projectStreamEvent(message);
62080
62083
  }
62084
+ const interruptContext = {
62085
+ marker: this.interruptMarkerSeen,
62086
+ bridge: context.bridgeInterruptSettling === true
62087
+ };
62081
62088
  const parsed = parseClaudeCodeStreamRecord(message);
62082
62089
  const snapshotMessageId = assistantSnapshotMessageId(message);
62083
62090
  const suppressStreamed = snapshotMessageId !== null && this.streamedMessageIds.has(snapshotMessageId);
@@ -62089,18 +62096,24 @@ var ClaudeCodeProjector = class {
62089
62096
  return [];
62090
62097
  }
62091
62098
  return conversationProjectionToUiChunks(entry, {
62092
- interrupted: this.interruptMarkerSeen
62099
+ interruptContext,
62100
+ emitDiagnostic: this.input.writeOutput
62093
62101
  });
62094
62102
  });
62095
62103
  if (parsed.result) {
62096
62104
  outputs.push(...this.endActiveParts(), ...this.finishActiveToolInputs());
62097
62105
  this.currentMessageId = null;
62098
62106
  this.activeResponseMessageId = null;
62099
- outputs.push(
62100
- projectClaudeCodeResult(parsed.result, {
62101
- interrupted: this.isInterruptCancellation(parsed.result)
62102
- })
62107
+ const interrupted = this.isInterruptCancellation(
62108
+ parsed.result,
62109
+ interruptContext
62103
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 }));
62104
62117
  this.interruptMarkerSeen = false;
62105
62118
  }
62106
62119
  return outputs;
@@ -62110,9 +62123,11 @@ var ClaudeCodeProjector = class {
62110
62123
  // surfaces as a diagnostic-only `error_during_execution`. Treating that as a
62111
62124
  // turn failure fed the FRA-3548 redelivery loop (the bridge redelivered a
62112
62125
  // message the SDK was actively answering), so a failed result bracketed by
62113
- // the SDK's own interrupt marker is classified as a cancellation instead.
62114
- isInterruptCancellation(result) {
62115
- 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);
62116
62131
  }
62117
62132
  flushPendingAssistantMessages() {
62118
62133
  const outputs = [
@@ -62233,7 +62248,8 @@ var ClaudeCodeProjector = class {
62233
62248
  const messageChunks = this.assistantSnapshotMessageChunks(projections);
62234
62249
  const rest = projections.filter((projection) => !isAssistantMessageOrToolCall(projection)).flatMap(
62235
62250
  (projection) => conversationProjectionToUiChunks(projection, {
62236
- interrupted: this.interruptMarkerSeen
62251
+ interruptContext: { marker: this.interruptMarkerSeen, bridge: false },
62252
+ emitDiagnostic: this.input.writeOutput
62237
62253
  })
62238
62254
  );
62239
62255
  const last = messageChunks.at(-1);
@@ -62457,7 +62473,7 @@ function textContentFromDelta(delta) {
62457
62473
  return null;
62458
62474
  }
62459
62475
  }
62460
- function conversationProjectionToUiChunks(projection, options = { interrupted: false }) {
62476
+ function conversationProjectionToUiChunks(projection, options) {
62461
62477
  if (projection.role === "assistant" && projection.kind === "message") {
62462
62478
  return messageContentChunks(projection);
62463
62479
  }
@@ -62478,15 +62494,12 @@ function conversationProjectionToUiChunks(projection, options = { interrupted: f
62478
62494
  })
62479
62495
  ];
62480
62496
  }
62481
- const remapped = typeof part.output === "string" ? remapClaudeCodeInterruptToolResultErrorText(part.output, {
62482
- interrupted: options.interrupted
62483
- }) : null;
62484
62497
  return [
62485
- uiChunk({
62486
- type: "tool-output-error",
62487
- toolCallId: part.toolUseId ?? UNKNOWN_MESSAGE_ID,
62488
- errorText: remapped ?? JSON.stringify(part.output)
62489
- })
62498
+ toolResultErrorChunk(
62499
+ part.toolUseId ?? UNKNOWN_MESSAGE_ID,
62500
+ part.output,
62501
+ options
62502
+ )
62490
62503
  ];
62491
62504
  });
62492
62505
  }
@@ -62530,6 +62543,32 @@ function conversationProjectionToUiChunks(projection, options = { interrupted: f
62530
62543
  }
62531
62544
  ];
62532
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
+ }
62533
62572
  function isAssistantMessageOrToolCall(projection) {
62534
62573
  return projection.role === "assistant" && (projection.kind === "message" || projection.kind === "tool_call");
62535
62574
  }
@@ -62808,6 +62847,7 @@ function parseReadStateRecord(raw) {
62808
62847
  }
62809
62848
 
62810
62849
  // src/commands/agent-bridge/harness/claude-code/session.ts
62850
+ init_src();
62811
62851
  import {
62812
62852
  startup as startupClaudeAgent
62813
62853
  } from "@anthropic-ai/claude-agent-sdk";
@@ -63002,7 +63042,7 @@ function withClaudeStderrDiagnosticsPointer(error51, capturedStderr) {
63002
63042
 
63003
63043
  // src/commands/agent-bridge/harness/claude-code/session.ts
63004
63044
  var CLAUDE_AGENT_STARTUP_TIMEOUT_MS = 3e4;
63005
- var CLAUDE_INTERRUPT_SETTLE_TIMEOUT_MS = 1e4;
63045
+ var CLAUDE_TOOL_BOUNDARY_WAIT_TIMEOUT_MS = 12e4;
63006
63046
  var CLAUDE_INTERRUPT_ACK_TIMEOUT_MS = 1e4;
63007
63047
  var CLAUDE_MCP_REGISTRATION_TIMEOUT_MS = 3e4;
63008
63048
  var CLAUDE_MCP_REGISTRATION_POLL_INTERVAL_MS = 100;
@@ -63078,16 +63118,33 @@ var ClaudeAgentBridgeSessionImpl = class {
63078
63118
  // flushed command id.
63079
63119
  openTurnCommandIds = [];
63080
63120
  // tool_use ids the assistant has emitted whose tool_result has not yet been
63081
- // observed. A non-empty set means a tool call is in flight, so an immediate
63082
- // 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
63083
63125
  // stop_reason=tool_use — the invalid transcript that trips Claude Code's
63084
63126
  // ede_diagnostic and auto-restarts the session (FRA-3049).
63085
- pendingToolUseIds = /* @__PURE__ */ new Set();
63086
- // Incremented on every turn-terminal `result`. An interrupt that aborts an
63087
- // in-flight tool call waits for this to advance before injecting, so the
63088
- // 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.
63089
63130
  turnResultCount = 0;
63090
- 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 = [];
63091
63148
  // Memoized MCP-registration gate, keyed by the attached query. A runtime
63092
63149
  // restart warms a brand-new session + query, so keying off the query object
63093
63150
  // (rather than a session-lifetime flag) re-arms the gate on every restart:
@@ -63146,18 +63203,34 @@ var ClaudeAgentBridgeSessionImpl = class {
63146
63203
  delivery_mode: mode,
63147
63204
  state: this.state.kind,
63148
63205
  active_turn_count: this.activeTurnCount,
63149
- pending_tool_use_count: this.pendingToolUseIds.size,
63206
+ pending_tool_use_count: this.pendingToolUses.size,
63150
63207
  deferred_count: this.deferredMessages.length
63151
63208
  });
63152
63209
  if (mode === "deferred" && this.hasInterruptibleTurn()) {
63153
63210
  this.deferredMessages.push({ text: message, commandIds });
63154
63211
  this.input.runtimeLogger?.info("agent_bridge_claude_message_deferred", {
63155
63212
  active_turn_count: this.activeTurnCount,
63156
- pending_tool_use_count: this.pendingToolUseIds.size,
63213
+ pending_tool_use_count: this.pendingToolUses.size,
63157
63214
  deferred_count: this.deferredMessages.length
63158
63215
  });
63159
63216
  return;
63160
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
+ }
63161
63234
  const interruption = await this.interruptActiveTurnBeforeMessage();
63162
63235
  if (interruption === "defer" && this.hasInterruptibleTurn()) {
63163
63236
  this.deferredMessages.push({ text: message, commandIds });
@@ -63165,7 +63238,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63165
63238
  "agent_bridge_claude_message_deferred_after_interrupt_timeout",
63166
63239
  {
63167
63240
  active_turn_count: this.activeTurnCount,
63168
- pending_tool_use_count: this.pendingToolUseIds.size,
63241
+ pending_tool_use_count: this.pendingToolUses.size,
63169
63242
  deferred_count: this.deferredMessages.length
63170
63243
  }
63171
63244
  );
@@ -63178,7 +63251,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63178
63251
  duration_ms: Date.now() - startedAt,
63179
63252
  state: this.state.kind,
63180
63253
  active_turn_count: this.activeTurnCount,
63181
- pending_tool_use_count: this.pendingToolUseIds.size,
63254
+ pending_tool_use_count: this.pendingToolUses.size,
63182
63255
  deferred_count: this.deferredMessages.length
63183
63256
  });
63184
63257
  }
@@ -63262,8 +63335,10 @@ var ClaudeAgentBridgeSessionImpl = class {
63262
63335
  }
63263
63336
  this.state = { kind: "closed" };
63264
63337
  this.activeTurnCount = 0;
63265
- this.pendingToolUseIds.clear();
63266
- this.resolveTurnSettlement(false);
63338
+ this.pendingToolUses.clear();
63339
+ this.cancelledToolRuns = [];
63340
+ this.interruptSettling = false;
63341
+ this.disarmToolBoundaryWait();
63267
63342
  if (this.deferredMessages.length > 0) {
63268
63343
  this.input.writeOutput?.(
63269
63344
  `agent_bridge_claude_deferred_dropped count=${this.deferredMessages.length} reason=session_closed`
@@ -63354,19 +63429,21 @@ var ClaudeAgentBridgeSessionImpl = class {
63354
63429
  void (async () => {
63355
63430
  try {
63356
63431
  for await (const message of query) {
63432
+ const interruptSettling = this.interruptSettling;
63357
63433
  this.trackToolUseLifecycle(message);
63358
- let meta3;
63434
+ let meta3 = { interruptSettling };
63359
63435
  if (isClaudeAgentTurnResult(message)) {
63360
63436
  this.activeTurnCount = Math.max(0, this.activeTurnCount - 1);
63361
63437
  meta3 = {
63438
+ interruptSettling,
63362
63439
  consumedCommandIds: this.openTurnCommandIds.shift() ?? []
63363
63440
  };
63364
- this.pendingToolUseIds.clear();
63441
+ this.pendingToolUses.clear();
63442
+ this.interruptSettling = false;
63365
63443
  this.turnResultCount += 1;
63366
- this.resolveTurnSettlement(true);
63367
63444
  this.input.runtimeLogger?.info("agent_bridge_claude_turn_result", {
63368
63445
  active_turn_count: this.activeTurnCount,
63369
- pending_tool_use_count: this.pendingToolUseIds.size,
63446
+ pending_tool_use_count: this.pendingToolUses.size,
63370
63447
  turn_result_count: this.turnResultCount,
63371
63448
  deferred_count: this.deferredMessages.length,
63372
63449
  consumed_command_ids: meta3.consumedCommandIds
@@ -63374,6 +63451,9 @@ var ClaudeAgentBridgeSessionImpl = class {
63374
63451
  if (this.activeTurnCount === 0) {
63375
63452
  this.flushDeferredMessages();
63376
63453
  }
63454
+ if (this.deferredMessages.length === 0) {
63455
+ this.cancelledToolRuns = [];
63456
+ }
63377
63457
  }
63378
63458
  const failure = probeClaudeAgentMessage(
63379
63459
  message,
@@ -63398,8 +63478,9 @@ var ClaudeAgentBridgeSessionImpl = class {
63398
63478
  } finally {
63399
63479
  this.activeTurnCount = 0;
63400
63480
  this.openTurnCommandIds.length = 0;
63401
- this.pendingToolUseIds.clear();
63402
- this.resolveTurnSettlement(false);
63481
+ this.pendingToolUses.clear();
63482
+ this.interruptSettling = false;
63483
+ this.disarmToolBoundaryWait();
63403
63484
  this.state = { kind: "closed" };
63404
63485
  this.reportExit();
63405
63486
  }
@@ -63416,7 +63497,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63416
63497
  this.input.runtimeLogger?.info("agent_bridge_claude_message_enqueued", {
63417
63498
  state: this.state.kind,
63418
63499
  active_turn_count: this.activeTurnCount,
63419
- pending_tool_use_count: this.pendingToolUseIds.size,
63500
+ pending_tool_use_count: this.pendingToolUses.size,
63420
63501
  deferred_count: this.deferredMessages.length
63421
63502
  });
63422
63503
  void this.ensureRunningQuery().catch((error51) => {
@@ -63429,75 +63510,105 @@ var ClaudeAgentBridgeSessionImpl = class {
63429
63510
  return this.activeTurnCount > 0 && this.state.kind === "running";
63430
63511
  }
63431
63512
  // True while the assistant has an emitted tool_use with no matching
63432
- // 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
63433
63515
  // unresolved stop_reason=tool_use ahead of the injected user message.
63434
63516
  hasInFlightToolUse() {
63435
- return this.pendingToolUseIds.size > 0;
63517
+ return this.pendingToolUses.size > 0;
63436
63518
  }
63437
63519
  // Track tool_use/tool_result pairs as they stream so hasInFlightToolUse()
63438
63520
  // reflects the live transcript. A tool_use may appear first as a streaming
63439
63521
  // content block before the full assistant message is available; the matching
63440
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.
63441
63530
  trackToolUseLifecycle(message) {
63442
- for (const id of toolUseIds(message)) {
63443
- 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
+ }
63444
63547
  }
63445
- for (const id of toolResultIds(message)) {
63446
- this.pendingToolUseIds.delete(id);
63548
+ if (hadInFlightToolUse && !this.hasInFlightToolUse()) {
63549
+ this.fireToolBoundaryInterrupt();
63447
63550
  }
63448
63551
  }
63449
- // Resolve once the interrupted turn emits its terminal `result` (turnResultCount
63450
- // advances past the pre-interrupt baseline), bounded so a tool that never
63451
- // returns after interrupt cannot wedge delivery. The caller reads the
63452
- // outcome: "timeout" means the turn is still running and the message must
63453
- // defer to the turn boundary rather than inject mid-turn.
63454
- awaitTurnSettlement(baseline, startedAt) {
63455
- if (this.turnResultCount > baseline) {
63456
- 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;
63457
63560
  }
63458
- return new Promise((resolve4) => {
63459
- let done = false;
63460
- const finish = (outcome) => {
63461
- if (done) {
63462
- return;
63463
- }
63464
- done = true;
63465
- clearTimeout(timer);
63466
- this.turnSettlementWaiters.delete(waiter);
63467
- this.input.writeOutput?.(
63468
- `agent_bridge_claude_interrupt_settle_${outcome} duration_ms=${Date.now() - startedAt}`
63469
- );
63470
- resolve4(outcome);
63471
- };
63472
- const waiter = (settled) => finish(settled ? "ready" : "aborted");
63473
- const timer = setTimeout(
63474
- () => finish("timeout"),
63475
- 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}`
63476
63584
  );
63477
- timer.unref?.();
63478
- this.turnSettlementWaiters.add(waiter);
63479
- });
63585
+ void this.interruptActiveTurn();
63586
+ }, CLAUDE_TOOL_BOUNDARY_WAIT_TIMEOUT_MS);
63587
+ timer.unref?.();
63588
+ this.toolBoundaryWait = { timer };
63480
63589
  }
63481
- resolveTurnSettlement(settled) {
63482
- if (this.turnSettlementWaiters.size === 0) {
63590
+ disarmToolBoundaryWait() {
63591
+ if (!this.toolBoundaryWait) {
63483
63592
  return;
63484
63593
  }
63485
- const waiters = [...this.turnSettlementWaiters];
63486
- this.turnSettlementWaiters.clear();
63487
- for (const waiter of waiters) {
63488
- waiter(settled);
63489
- }
63594
+ clearTimeout(this.toolBoundaryWait.timer);
63595
+ this.toolBoundaryWait = null;
63490
63596
  }
63491
63597
  flushDeferredMessages() {
63492
63598
  if (this.deferredMessages.length === 0) {
63493
63599
  return;
63494
63600
  }
63495
63601
  const pending = this.deferredMessages.splice(0);
63602
+ const cancelledToolRuns = this.cancelledToolRuns.splice(0);
63603
+ this.disarmToolBoundaryWait();
63496
63604
  this.input.writeOutput?.(
63497
- `agent_bridge_claude_deferred_flush count=${pending.length}`
63605
+ `agent_bridge_claude_deferred_flush count=${pending.length} cancelled_tool_count=${cancelledToolRuns.length}`
63498
63606
  );
63607
+ const text = pending.map((entry) => entry.text).join("\n\n");
63499
63608
  this.enqueueUserMessage(
63500
- pending.map((entry) => entry.text).join("\n\n"),
63609
+ cancelledToolRuns.length > 0 ? `${text}
63610
+
63611
+ ${cancelledToolPlatformNotice(cancelledToolRuns)}` : text,
63501
63612
  pending.flatMap((entry) => entry.commandIds)
63502
63613
  );
63503
63614
  }
@@ -63518,11 +63629,10 @@ var ClaudeAgentBridgeSessionImpl = class {
63518
63629
  return "proceed";
63519
63630
  }
63520
63631
  const query = this.state.query;
63521
- const hadInFlightToolUse = this.hasInFlightToolUse();
63522
63632
  const settlementBaseline = this.turnResultCount;
63523
63633
  const startedAt = Date.now();
63524
63634
  this.input.writeOutput?.(
63525
- `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()}`
63526
63636
  );
63527
63637
  const interruptPromise = (async () => {
63528
63638
  const ack = await this.requestInterruptAck(query, startedAt);
@@ -63530,16 +63640,6 @@ var ClaudeAgentBridgeSessionImpl = class {
63530
63640
  this.interruptTimeoutTurn = settlementBaseline;
63531
63641
  return "defer";
63532
63642
  }
63533
- if (hadInFlightToolUse) {
63534
- const settle = await this.awaitTurnSettlement(
63535
- settlementBaseline,
63536
- startedAt
63537
- );
63538
- if (settle === "timeout") {
63539
- this.interruptTimeoutTurn = settlementBaseline;
63540
- return "defer";
63541
- }
63542
- }
63543
63643
  return "proceed";
63544
63644
  })();
63545
63645
  this.interruptInFlight = interruptPromise;
@@ -63576,6 +63676,7 @@ var ClaudeAgentBridgeSessionImpl = class {
63576
63676
  );
63577
63677
  }, CLAUDE_INTERRUPT_ACK_TIMEOUT_MS);
63578
63678
  timer.unref?.();
63679
+ this.interruptSettling = true;
63579
63680
  query.interrupt().then(
63580
63681
  () => {
63581
63682
  if (done) {
@@ -63932,10 +64033,10 @@ function claudeAgentUserMessage(message) {
63932
64033
  function isClaudeAgentTurnResult(message) {
63933
64034
  return message.type === "result";
63934
64035
  }
63935
- function toolUseIds(message) {
64036
+ function toolUseEntries(message) {
63936
64037
  if (message.type === "assistant") {
63937
64038
  return contentBlocks2(message.message.content).flatMap(
63938
- (block2) => block2.type === "tool_use" && typeof block2.id === "string" ? [block2.id] : []
64039
+ (block2) => block2.type === "tool_use" && typeof block2.id === "string" ? [toolUseEntryFromBlock(block2)] : []
63939
64040
  );
63940
64041
  }
63941
64042
  if (message.type !== "stream_event") {
@@ -63946,16 +64047,54 @@ function toolUseIds(message) {
63946
64047
  return [];
63947
64048
  }
63948
64049
  const block = event.content_block;
63949
- return block.type === "tool_use" && typeof block.id === "string" ? [block.id] : [];
64050
+ return block.type === "tool_use" && typeof block.id === "string" ? [toolUseEntryFromBlock(block)] : [];
64051
+ }
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
+ };
63950
64058
  }
63951
- function toolResultIds(message) {
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) {
63952
64075
  if (message.type !== "user") {
63953
64076
  return [];
63954
64077
  }
63955
64078
  return contentBlocks2(message.message.content).flatMap(
63956
- (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
+ ] : []
63957
64086
  );
63958
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
+ }
63959
64098
  function contentBlocks2(content) {
63960
64099
  if (!Array.isArray(content)) {
63961
64100
  return [];
@@ -63974,6 +64113,9 @@ var ClaudeCodeCommandHandler = class {
63974
64113
  this.input = input;
63975
64114
  this.claudeConfig = input.claude;
63976
64115
  this.outputBuffer = new AgentBridgeOutputBuffer(input);
64116
+ this.projector = new ClaudeCodeProjector({
64117
+ writeOutput: input.writeOutput
64118
+ });
63977
64119
  this.readState = input.readState ? new ClaudeReadStateTracker(input.readState) : null;
63978
64120
  }
63979
64121
  input;
@@ -63995,7 +64137,7 @@ var ClaudeCodeCommandHandler = class {
63995
64137
  inFlightMessageCommands = /* @__PURE__ */ new Map();
63996
64138
  pendingQuestions = /* @__PURE__ */ new Map();
63997
64139
  outputBuffer;
63998
- projector = new ClaudeCodeProjector();
64140
+ projector;
63999
64141
  readState;
64000
64142
  livenessTicker = null;
64001
64143
  // -----------------------------------------------------------------------------
@@ -64376,7 +64518,9 @@ var ClaudeCodeCommandHandler = class {
64376
64518
  this.trackReadState(activeContext, message);
64377
64519
  let projections;
64378
64520
  try {
64379
- projections = this.projector.project(message);
64521
+ projections = this.projector.project(message, {
64522
+ bridgeInterruptSettling: meta3?.interruptSettling === true
64523
+ });
64380
64524
  } catch (error51) {
64381
64525
  this.input.writeOutput?.(
64382
64526
  `agent_bridge_claude_parse_failed error=${error51 instanceof Error ? error51.message : String(error51)}`