@autohq/cli 0.1.287 → 0.1.289

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.
@@ -23384,7 +23384,7 @@ Object.assign(lookup, {
23384
23384
  // package.json
23385
23385
  var package_default = {
23386
23386
  name: "@autohq/cli",
23387
- version: "0.1.287",
23387
+ version: "0.1.289",
23388
23388
  license: "SEE LICENSE IN README.md",
23389
23389
  publishConfig: {
23390
23390
  access: "public"
@@ -28362,6 +28362,9 @@ var ProjectTemplateSubscriptionSchema = external_exports.object({
28362
28362
  });
28363
28363
 
28364
28364
  // ../../packages/schemas/src/temporal.ts
28365
+ var SessionCheckTimeoutWorkflowInputSchema = external_exports.object({
28366
+ checkId: external_exports.string().trim().min(1)
28367
+ });
28365
28368
  var ResourceReconciliationScopeSchema = external_exports.object({
28366
28369
  organizationId: external_exports.string().trim().min(1),
28367
28370
  projectId: external_exports.string().trim().min(1).nullable()
@@ -36725,25 +36728,11 @@ var AgentBridgeOutputBuffer = class {
36725
36728
  );
36726
36729
  if (!completedMessage || !completedMessage.id.trim()) {
36727
36730
  if (isTerminalUiMessageChunk(projection.chunk)) {
36728
- const statusText = terminalStatusText(projection);
36729
- if (statusText) {
36730
- await this.enqueueProjectionAndDrain(context, {
36731
- type: "entry",
36732
- entry: {
36733
- role: "system",
36734
- kind: "status",
36735
- status: terminalEntryStatus(projection),
36736
- ...projection.turnStatus ? { turnStatus: projection.turnStatus } : {},
36737
- ...projection.usage ? { usage: projection.usage } : {},
36738
- content: {
36739
- parts: [{ type: "text", text: statusText }]
36740
- }
36741
- }
36742
- });
36743
- }
36731
+ await this.emitTerminalStatusEntry(context, projection);
36744
36732
  }
36745
36733
  return;
36746
36734
  }
36735
+ const turnFailed = projection.turnStatus === "failed";
36747
36736
  this.outputSeq += 1;
36748
36737
  const completedAt = now2();
36749
36738
  const completedOutput = buildUiMessageCompletedOutputEnvelope({
@@ -36751,13 +36740,35 @@ var AgentBridgeOutputBuffer = class {
36751
36740
  outputSeq: this.outputSeq,
36752
36741
  message: completedMessage,
36753
36742
  status: terminalEntryStatus(projection),
36754
- turnStatus: projection.turnStatus,
36755
- usage: projection.usage,
36743
+ turnStatus: turnFailed ? void 0 : projection.turnStatus,
36744
+ usage: turnFailed ? void 0 : projection.usage,
36756
36745
  createdAt: completedAt,
36757
36746
  completedAt
36758
36747
  });
36759
36748
  this.enqueueOutput(completedOutput);
36760
36749
  await this.drainPendingOutputs();
36750
+ if (turnFailed) {
36751
+ await this.emitTerminalStatusEntry(context, projection);
36752
+ }
36753
+ }
36754
+ async emitTerminalStatusEntry(context, projection) {
36755
+ const statusText = terminalStatusText(projection);
36756
+ if (!statusText) {
36757
+ return;
36758
+ }
36759
+ await this.enqueueProjectionAndDrain(context, {
36760
+ type: "entry",
36761
+ entry: {
36762
+ role: "system",
36763
+ kind: "status",
36764
+ status: terminalEntryStatus(projection),
36765
+ ...projection.turnStatus ? { turnStatus: projection.turnStatus } : {},
36766
+ ...projection.usage ? { usage: projection.usage } : {},
36767
+ content: {
36768
+ parts: [{ type: "text", text: statusText }]
36769
+ }
36770
+ }
36771
+ });
36761
36772
  }
36762
36773
  async emitLiveUiMessageChunk(context, chunk) {
36763
36774
  this.outputSeq += 1;
@@ -57340,6 +57351,12 @@ var claudeAgentBridgeRuntime = {
57340
57351
  var claudeAgentSdkRuntime = {
57341
57352
  startup: (input) => aj$(input)
57342
57353
  };
57354
+ var ClaudeMcpRegistryEmptyError = class extends Error {
57355
+ constructor(detail) {
57356
+ super(`MCP tool registry came up empty at session start: ${detail}`);
57357
+ this.name = "ClaudeMcpRegistryEmptyError";
57358
+ }
57359
+ };
57343
57360
  var ClaudeAgentBridgeSessionImpl = class {
57344
57361
  input;
57345
57362
  inputQueue;
@@ -57729,17 +57746,20 @@ var ClaudeAgentBridgeSessionImpl = class {
57729
57746
  }
57730
57747
  // Why this gate is correct without a provider-backed test (it relies on real
57731
57748
  // SDK mcpServerStatus() timing that fakes cannot prove):
57732
- // - Safe-degrade: pollMcpRegistration never rejects — on timeout, a
57733
- // status-read failure, or a closed session it resolves, so the worst case
57734
- // is today's immediate injection. The gate can never regress below the
57735
- // status-quo race; it can only close it.
57749
+ // - Safe-degrade on uncertainty: pollMcpRegistration never rejects — on
57750
+ // timeout, a status-read failure, or a closed session it resolves, so an
57751
+ // *unconfirmed* registry still degrades to today's immediate injection.
57752
+ // The one fail-closed exit is the confirmed-empty registry: every
57753
+ // configured server settled and none is serving tools, which is exactly
57754
+ // the state that produced silent toolless runs in production (FRA-3458).
57736
57755
  // - Provider-path validation is deferred deliberately: the failure is the
57737
57756
  // intermittent live-prod race on the chief-of-staff singleton (a real
57738
57757
  // runtime restart dispatching turn-1 while real mcp__* servers reconnect),
57739
57758
  // which cannot be reproduced deterministically without live credentials
57740
57759
  // and a forced restart racing reconnect.
57741
57760
  // - Live confirmation comes from the agent_bridge_claude_mcp_gate_started/
57742
- // _ready/_timeout diagnostics on the next real restart, not a CI fake.
57761
+ // _ready/_timeout/_empty diagnostics on the next real restart, not a CI
57762
+ // fake.
57743
57763
  //
57744
57764
  // Block until the freshly-warmed query's MCP servers have left `pending`, so
57745
57765
  // the first turn's prompt is built with the mcp__* tools registered. Memoized
@@ -57763,15 +57783,22 @@ var ClaudeAgentBridgeSessionImpl = class {
57763
57783
  promise: this.pollMcpRegistration(query)
57764
57784
  };
57765
57785
  }
57766
- await this.mcpRegistrationGate.promise;
57786
+ const result = await this.mcpRegistrationGate.promise;
57787
+ if (result.outcome !== "empty") {
57788
+ return;
57789
+ }
57790
+ this.close();
57791
+ throw new ClaudeMcpRegistryEmptyError(result.detail);
57767
57792
  }
57768
57793
  // Poll the live query's MCP server status until no configured server is still
57769
57794
  // `pending`, bounded by CLAUDE_MCP_REGISTRATION_TIMEOUT_MS. Terminal states
57770
57795
  // (connected/failed/needs-auth/disabled) all count as settled, so a server
57771
57796
  // that cannot connect in this runtime (e.g. an interactively-authed server in
57772
- // a headless sandbox) never holds the gate to the timeout. Never rejects:
57797
+ // a headless sandbox) never holds the gate to the timeout. A settled state
57798
+ // where no configured server is serving tools resolves "empty" — the caller
57799
+ // fails the send rather than injecting a toolless turn. Never rejects:
57773
57800
  // status-read failure, timeout, or a closed session all resolve so the caller
57774
- // degrades to immediate injection.
57801
+ // degrades to immediate injection when the registry state is uncertain.
57775
57802
  async pollMcpRegistration(query) {
57776
57803
  const configuredNames = Object.keys(this.options.mcpServers ?? {});
57777
57804
  const startedAt = Date.now();
@@ -57781,7 +57808,7 @@ var ClaudeAgentBridgeSessionImpl = class {
57781
57808
  );
57782
57809
  while (true) {
57783
57810
  if (this.state.kind === "closed") {
57784
- return;
57811
+ return { outcome: "closed" };
57785
57812
  }
57786
57813
  let statuses;
57787
57814
  try {
@@ -57790,19 +57817,26 @@ var ClaudeAgentBridgeSessionImpl = class {
57790
57817
  this.input.writeOutput?.(
57791
57818
  `agent_bridge_claude_mcp_gate_status_failed duration_ms=${Date.now() - startedAt} error=${error51 instanceof Error ? error51.message : String(error51)}`
57792
57819
  );
57793
- return;
57820
+ return { outcome: "status_failed" };
57794
57821
  }
57795
57822
  if (mcpServersSettled(statuses, configuredNames)) {
57823
+ if (!mcpRegistryHasTools(statuses, configuredNames)) {
57824
+ const detail = mcpStatusDetailList(statuses);
57825
+ this.input.writeOutput?.(
57826
+ `agent_bridge_claude_mcp_gate_empty duration_ms=${Date.now() - startedAt} servers=${detail}`
57827
+ );
57828
+ return { outcome: "empty", detail };
57829
+ }
57796
57830
  this.input.writeOutput?.(
57797
57831
  `agent_bridge_claude_mcp_gate_ready duration_ms=${Date.now() - startedAt} servers=${mcpStatusList(statuses)}`
57798
57832
  );
57799
- return;
57833
+ return { outcome: "ready" };
57800
57834
  }
57801
57835
  if (Date.now() >= deadline) {
57802
57836
  this.input.writeOutput?.(
57803
57837
  `agent_bridge_claude_mcp_gate_timeout duration_ms=${Date.now() - startedAt} servers=${mcpStatusList(statuses)}`
57804
57838
  );
57805
- return;
57839
+ return { outcome: "timeout" };
57806
57840
  }
57807
57841
  await delay(CLAUDE_MCP_REGISTRATION_POLL_INTERVAL_MS);
57808
57842
  }
@@ -57945,9 +57979,20 @@ function mcpServersSettled(statuses, configuredNames) {
57945
57979
  return status !== void 0 && status !== "pending";
57946
57980
  });
57947
57981
  }
57982
+ function mcpRegistryHasTools(statuses, configuredNames) {
57983
+ const configured = new Set(configuredNames);
57984
+ return statuses.some(
57985
+ (server) => configured.has(server.name) && server.status === "connected" && (server.tools === void 0 || server.tools.length > 0)
57986
+ );
57987
+ }
57948
57988
  function mcpStatusList(statuses) {
57949
57989
  return statuses.map((server) => `${server.name}:${server.status}`).join(",");
57950
57990
  }
57991
+ function mcpStatusDetailList(statuses) {
57992
+ return statuses.map(
57993
+ (server) => server.error ? `${server.name}:${server.status}(${server.error})` : `${server.name}:${server.status}`
57994
+ ).join(",");
57995
+ }
57951
57996
  function delay(ms) {
57952
57997
  return new Promise((resolve2) => {
57953
57998
  const timer = setTimeout(resolve2, ms);
@@ -58055,6 +58100,13 @@ var ClaudeCodeCommandHandler = class {
58055
58100
  agentSession = null;
58056
58101
  persistedAgentId = null;
58057
58102
  injectedCommands = /* @__PURE__ */ new Set();
58103
+ // Message commands whose injection has started but not settled. A fresh SDK
58104
+ // warm inside sendMessage can outlast the bridge's command-ack window, so a
58105
+ // redelivery may arrive while the first attempt is still in flight; acking it
58106
+ // "duplicate" then would let the worker accept a command whose injection
58107
+ // later fails, silently stalling the turn (FRA-3458). The duplicate branch
58108
+ // awaits the in-flight outcome here and relays it instead.
58109
+ inFlightMessageCommands = /* @__PURE__ */ new Map();
58058
58110
  pendingQuestions = /* @__PURE__ */ new Map();
58059
58111
  outputBuffer;
58060
58112
  projector = new ClaudeCodeProjector();
@@ -58102,13 +58154,25 @@ var ClaudeCodeCommandHandler = class {
58102
58154
  });
58103
58155
  }
58104
58156
  if (this.injectedCommands.has(delivery.commandId)) {
58157
+ const inFlight = this.inFlightMessageCommands.get(delivery.commandId);
58158
+ const outcome2 = inFlight ? await inFlight : { status: "injected" };
58105
58159
  this.input.runtimeLogger?.info(
58106
58160
  "agent_bridge_claude_command_duplicate",
58107
58161
  commandLogContext(delivery, {
58108
58162
  socket_id: socketId,
58109
- duration_ms: Date.now() - startedAt
58163
+ duration_ms: Date.now() - startedAt,
58164
+ awaited_in_flight: Boolean(inFlight),
58165
+ in_flight_status: outcome2.status
58110
58166
  })
58111
58167
  );
58168
+ if (outcome2.status === "failed") {
58169
+ return commandAck({
58170
+ delivery,
58171
+ socketId,
58172
+ status: "failed",
58173
+ error: outcome2.error
58174
+ });
58175
+ }
58112
58176
  return commandAck({
58113
58177
  delivery,
58114
58178
  socketId,
@@ -58159,6 +58223,53 @@ var ClaudeCodeCommandHandler = class {
58159
58223
  });
58160
58224
  }
58161
58225
  this.injectedCommands.add(delivery.commandId);
58226
+ const injection = this.injectMessageCommand(
58227
+ activeContext,
58228
+ delivery,
58229
+ message,
58230
+ socketId
58231
+ );
58232
+ this.inFlightMessageCommands.set(delivery.commandId, injection);
58233
+ let outcome;
58234
+ try {
58235
+ outcome = await injection;
58236
+ } finally {
58237
+ this.inFlightMessageCommands.delete(delivery.commandId);
58238
+ }
58239
+ if (outcome.status === "failed") {
58240
+ this.injectedCommands.delete(delivery.commandId);
58241
+ this.input.runtimeLogger?.warn(
58242
+ "agent_bridge_claude_command_failed",
58243
+ commandLogContext(delivery, {
58244
+ socket_id: socketId,
58245
+ duration_ms: Date.now() - startedAt,
58246
+ error: outcome.error
58247
+ })
58248
+ );
58249
+ return commandAck({
58250
+ delivery,
58251
+ socketId,
58252
+ status: "failed",
58253
+ error: outcome.error
58254
+ });
58255
+ }
58256
+ this.input.runtimeLogger?.info(
58257
+ "agent_bridge_claude_command_injected",
58258
+ commandLogContext(delivery, {
58259
+ socket_id: socketId,
58260
+ duration_ms: Date.now() - startedAt
58261
+ })
58262
+ );
58263
+ return commandAck({
58264
+ delivery,
58265
+ socketId,
58266
+ status: "injected"
58267
+ });
58268
+ }
58269
+ // Runs one message command's injection to a terminal outcome. Never rejects:
58270
+ // the returned promise is stored in the in-flight map and awaited by both the
58271
+ // original delivery and any redeliveries, so failures travel as values.
58272
+ async injectMessageCommand(activeContext, delivery, message, socketId) {
58162
58273
  try {
58163
58274
  const emitStartedAt = Date.now();
58164
58275
  this.input.runtimeLogger?.info(
@@ -58204,35 +58315,13 @@ var ClaudeCodeCommandHandler = class {
58204
58315
  duration_ms: Date.now() - sendStartedAt
58205
58316
  })
58206
58317
  );
58318
+ return { status: "injected" };
58207
58319
  } catch (error51) {
58208
- this.injectedCommands.delete(delivery.commandId);
58209
- this.input.runtimeLogger?.warn(
58210
- "agent_bridge_claude_command_failed",
58211
- commandLogContext(delivery, {
58212
- socket_id: socketId,
58213
- duration_ms: Date.now() - startedAt,
58214
- error: error51 instanceof Error ? error51.message : String(error51)
58215
- })
58216
- );
58217
- return commandAck({
58218
- delivery,
58219
- socketId,
58320
+ return {
58220
58321
  status: "failed",
58221
58322
  error: error51 instanceof Error ? error51.message : String(error51)
58222
- });
58323
+ };
58223
58324
  }
58224
- this.input.runtimeLogger?.info(
58225
- "agent_bridge_claude_command_injected",
58226
- commandLogContext(delivery, {
58227
- socket_id: socketId,
58228
- duration_ms: Date.now() - startedAt
58229
- })
58230
- );
58231
- return commandAck({
58232
- delivery,
58233
- socketId,
58234
- status: "injected"
58235
- });
58236
58325
  }
58237
58326
  // -----------------------------------------------------------------------------
58238
58327
  // Question answering
package/dist/index.js CHANGED
@@ -20265,11 +20265,14 @@ var init_templates = __esm({
20265
20265
  });
20266
20266
 
20267
20267
  // ../../packages/schemas/src/temporal.ts
20268
- var ResourceReconciliationScopeSchema;
20268
+ var SessionCheckTimeoutWorkflowInputSchema, ResourceReconciliationScopeSchema;
20269
20269
  var init_temporal = __esm({
20270
20270
  "../../packages/schemas/src/temporal.ts"() {
20271
20271
  "use strict";
20272
20272
  init_zod();
20273
+ SessionCheckTimeoutWorkflowInputSchema = external_exports.object({
20274
+ checkId: external_exports.string().trim().min(1)
20275
+ });
20273
20276
  ResourceReconciliationScopeSchema = external_exports.object({
20274
20277
  organizationId: external_exports.string().trim().min(1),
20275
20278
  projectId: external_exports.string().trim().min(1).nullable()
@@ -23170,7 +23173,7 @@ var init_package = __esm({
23170
23173
  "package.json"() {
23171
23174
  package_default = {
23172
23175
  name: "@autohq/cli",
23173
- version: "0.1.287",
23176
+ version: "0.1.289",
23174
23177
  license: "SEE LICENSE IN README.md",
23175
23178
  publishConfig: {
23176
23179
  access: "public"
@@ -33742,25 +33745,11 @@ var AgentBridgeOutputBuffer = class {
33742
33745
  );
33743
33746
  if (!completedMessage || !completedMessage.id.trim()) {
33744
33747
  if (isTerminalUiMessageChunk(projection.chunk)) {
33745
- const statusText = terminalStatusText(projection);
33746
- if (statusText) {
33747
- await this.enqueueProjectionAndDrain(context, {
33748
- type: "entry",
33749
- entry: {
33750
- role: "system",
33751
- kind: "status",
33752
- status: terminalEntryStatus(projection),
33753
- ...projection.turnStatus ? { turnStatus: projection.turnStatus } : {},
33754
- ...projection.usage ? { usage: projection.usage } : {},
33755
- content: {
33756
- parts: [{ type: "text", text: statusText }]
33757
- }
33758
- }
33759
- });
33760
- }
33748
+ await this.emitTerminalStatusEntry(context, projection);
33761
33749
  }
33762
33750
  return;
33763
33751
  }
33752
+ const turnFailed = projection.turnStatus === "failed";
33764
33753
  this.outputSeq += 1;
33765
33754
  const completedAt = now2();
33766
33755
  const completedOutput = buildUiMessageCompletedOutputEnvelope({
@@ -33768,13 +33757,35 @@ var AgentBridgeOutputBuffer = class {
33768
33757
  outputSeq: this.outputSeq,
33769
33758
  message: completedMessage,
33770
33759
  status: terminalEntryStatus(projection),
33771
- turnStatus: projection.turnStatus,
33772
- usage: projection.usage,
33760
+ turnStatus: turnFailed ? void 0 : projection.turnStatus,
33761
+ usage: turnFailed ? void 0 : projection.usage,
33773
33762
  createdAt: completedAt,
33774
33763
  completedAt
33775
33764
  });
33776
33765
  this.enqueueOutput(completedOutput);
33777
33766
  await this.drainPendingOutputs();
33767
+ if (turnFailed) {
33768
+ await this.emitTerminalStatusEntry(context, projection);
33769
+ }
33770
+ }
33771
+ async emitTerminalStatusEntry(context, projection) {
33772
+ const statusText = terminalStatusText(projection);
33773
+ if (!statusText) {
33774
+ return;
33775
+ }
33776
+ await this.enqueueProjectionAndDrain(context, {
33777
+ type: "entry",
33778
+ entry: {
33779
+ role: "system",
33780
+ kind: "status",
33781
+ status: terminalEntryStatus(projection),
33782
+ ...projection.turnStatus ? { turnStatus: projection.turnStatus } : {},
33783
+ ...projection.usage ? { usage: projection.usage } : {},
33784
+ content: {
33785
+ parts: [{ type: "text", text: statusText }]
33786
+ }
33787
+ }
33788
+ });
33778
33789
  }
33779
33790
  async emitLiveUiMessageChunk(context, chunk) {
33780
33791
  this.outputSeq += 1;
@@ -34786,6 +34797,12 @@ var claudeAgentBridgeRuntime = {
34786
34797
  var claudeAgentSdkRuntime = {
34787
34798
  startup: (input) => startupClaudeAgent(input)
34788
34799
  };
34800
+ var ClaudeMcpRegistryEmptyError = class extends Error {
34801
+ constructor(detail) {
34802
+ super(`MCP tool registry came up empty at session start: ${detail}`);
34803
+ this.name = "ClaudeMcpRegistryEmptyError";
34804
+ }
34805
+ };
34789
34806
  var ClaudeAgentBridgeSessionImpl = class {
34790
34807
  input;
34791
34808
  inputQueue;
@@ -35175,17 +35192,20 @@ var ClaudeAgentBridgeSessionImpl = class {
35175
35192
  }
35176
35193
  // Why this gate is correct without a provider-backed test (it relies on real
35177
35194
  // SDK mcpServerStatus() timing that fakes cannot prove):
35178
- // - Safe-degrade: pollMcpRegistration never rejects — on timeout, a
35179
- // status-read failure, or a closed session it resolves, so the worst case
35180
- // is today's immediate injection. The gate can never regress below the
35181
- // status-quo race; it can only close it.
35195
+ // - Safe-degrade on uncertainty: pollMcpRegistration never rejects — on
35196
+ // timeout, a status-read failure, or a closed session it resolves, so an
35197
+ // *unconfirmed* registry still degrades to today's immediate injection.
35198
+ // The one fail-closed exit is the confirmed-empty registry: every
35199
+ // configured server settled and none is serving tools, which is exactly
35200
+ // the state that produced silent toolless runs in production (FRA-3458).
35182
35201
  // - Provider-path validation is deferred deliberately: the failure is the
35183
35202
  // intermittent live-prod race on the chief-of-staff singleton (a real
35184
35203
  // runtime restart dispatching turn-1 while real mcp__* servers reconnect),
35185
35204
  // which cannot be reproduced deterministically without live credentials
35186
35205
  // and a forced restart racing reconnect.
35187
35206
  // - Live confirmation comes from the agent_bridge_claude_mcp_gate_started/
35188
- // _ready/_timeout diagnostics on the next real restart, not a CI fake.
35207
+ // _ready/_timeout/_empty diagnostics on the next real restart, not a CI
35208
+ // fake.
35189
35209
  //
35190
35210
  // Block until the freshly-warmed query's MCP servers have left `pending`, so
35191
35211
  // the first turn's prompt is built with the mcp__* tools registered. Memoized
@@ -35209,15 +35229,22 @@ var ClaudeAgentBridgeSessionImpl = class {
35209
35229
  promise: this.pollMcpRegistration(query)
35210
35230
  };
35211
35231
  }
35212
- await this.mcpRegistrationGate.promise;
35232
+ const result = await this.mcpRegistrationGate.promise;
35233
+ if (result.outcome !== "empty") {
35234
+ return;
35235
+ }
35236
+ this.close();
35237
+ throw new ClaudeMcpRegistryEmptyError(result.detail);
35213
35238
  }
35214
35239
  // Poll the live query's MCP server status until no configured server is still
35215
35240
  // `pending`, bounded by CLAUDE_MCP_REGISTRATION_TIMEOUT_MS. Terminal states
35216
35241
  // (connected/failed/needs-auth/disabled) all count as settled, so a server
35217
35242
  // that cannot connect in this runtime (e.g. an interactively-authed server in
35218
- // a headless sandbox) never holds the gate to the timeout. Never rejects:
35243
+ // a headless sandbox) never holds the gate to the timeout. A settled state
35244
+ // where no configured server is serving tools resolves "empty" — the caller
35245
+ // fails the send rather than injecting a toolless turn. Never rejects:
35219
35246
  // status-read failure, timeout, or a closed session all resolve so the caller
35220
- // degrades to immediate injection.
35247
+ // degrades to immediate injection when the registry state is uncertain.
35221
35248
  async pollMcpRegistration(query) {
35222
35249
  const configuredNames = Object.keys(this.options.mcpServers ?? {});
35223
35250
  const startedAt = Date.now();
@@ -35227,7 +35254,7 @@ var ClaudeAgentBridgeSessionImpl = class {
35227
35254
  );
35228
35255
  while (true) {
35229
35256
  if (this.state.kind === "closed") {
35230
- return;
35257
+ return { outcome: "closed" };
35231
35258
  }
35232
35259
  let statuses;
35233
35260
  try {
@@ -35236,19 +35263,26 @@ var ClaudeAgentBridgeSessionImpl = class {
35236
35263
  this.input.writeOutput?.(
35237
35264
  `agent_bridge_claude_mcp_gate_status_failed duration_ms=${Date.now() - startedAt} error=${error51 instanceof Error ? error51.message : String(error51)}`
35238
35265
  );
35239
- return;
35266
+ return { outcome: "status_failed" };
35240
35267
  }
35241
35268
  if (mcpServersSettled(statuses, configuredNames)) {
35269
+ if (!mcpRegistryHasTools(statuses, configuredNames)) {
35270
+ const detail = mcpStatusDetailList(statuses);
35271
+ this.input.writeOutput?.(
35272
+ `agent_bridge_claude_mcp_gate_empty duration_ms=${Date.now() - startedAt} servers=${detail}`
35273
+ );
35274
+ return { outcome: "empty", detail };
35275
+ }
35242
35276
  this.input.writeOutput?.(
35243
35277
  `agent_bridge_claude_mcp_gate_ready duration_ms=${Date.now() - startedAt} servers=${mcpStatusList(statuses)}`
35244
35278
  );
35245
- return;
35279
+ return { outcome: "ready" };
35246
35280
  }
35247
35281
  if (Date.now() >= deadline) {
35248
35282
  this.input.writeOutput?.(
35249
35283
  `agent_bridge_claude_mcp_gate_timeout duration_ms=${Date.now() - startedAt} servers=${mcpStatusList(statuses)}`
35250
35284
  );
35251
- return;
35285
+ return { outcome: "timeout" };
35252
35286
  }
35253
35287
  await delay(CLAUDE_MCP_REGISTRATION_POLL_INTERVAL_MS);
35254
35288
  }
@@ -35391,9 +35425,20 @@ function mcpServersSettled(statuses, configuredNames) {
35391
35425
  return status !== void 0 && status !== "pending";
35392
35426
  });
35393
35427
  }
35428
+ function mcpRegistryHasTools(statuses, configuredNames) {
35429
+ const configured = new Set(configuredNames);
35430
+ return statuses.some(
35431
+ (server) => configured.has(server.name) && server.status === "connected" && (server.tools === void 0 || server.tools.length > 0)
35432
+ );
35433
+ }
35394
35434
  function mcpStatusList(statuses) {
35395
35435
  return statuses.map((server) => `${server.name}:${server.status}`).join(",");
35396
35436
  }
35437
+ function mcpStatusDetailList(statuses) {
35438
+ return statuses.map(
35439
+ (server) => server.error ? `${server.name}:${server.status}(${server.error})` : `${server.name}:${server.status}`
35440
+ ).join(",");
35441
+ }
35397
35442
  function delay(ms) {
35398
35443
  return new Promise((resolve4) => {
35399
35444
  const timer = setTimeout(resolve4, ms);
@@ -35501,6 +35546,13 @@ var ClaudeCodeCommandHandler = class {
35501
35546
  agentSession = null;
35502
35547
  persistedAgentId = null;
35503
35548
  injectedCommands = /* @__PURE__ */ new Set();
35549
+ // Message commands whose injection has started but not settled. A fresh SDK
35550
+ // warm inside sendMessage can outlast the bridge's command-ack window, so a
35551
+ // redelivery may arrive while the first attempt is still in flight; acking it
35552
+ // "duplicate" then would let the worker accept a command whose injection
35553
+ // later fails, silently stalling the turn (FRA-3458). The duplicate branch
35554
+ // awaits the in-flight outcome here and relays it instead.
35555
+ inFlightMessageCommands = /* @__PURE__ */ new Map();
35504
35556
  pendingQuestions = /* @__PURE__ */ new Map();
35505
35557
  outputBuffer;
35506
35558
  projector = new ClaudeCodeProjector();
@@ -35548,13 +35600,25 @@ var ClaudeCodeCommandHandler = class {
35548
35600
  });
35549
35601
  }
35550
35602
  if (this.injectedCommands.has(delivery.commandId)) {
35603
+ const inFlight = this.inFlightMessageCommands.get(delivery.commandId);
35604
+ const outcome2 = inFlight ? await inFlight : { status: "injected" };
35551
35605
  this.input.runtimeLogger?.info(
35552
35606
  "agent_bridge_claude_command_duplicate",
35553
35607
  commandLogContext(delivery, {
35554
35608
  socket_id: socketId,
35555
- duration_ms: Date.now() - startedAt
35609
+ duration_ms: Date.now() - startedAt,
35610
+ awaited_in_flight: Boolean(inFlight),
35611
+ in_flight_status: outcome2.status
35556
35612
  })
35557
35613
  );
35614
+ if (outcome2.status === "failed") {
35615
+ return commandAck({
35616
+ delivery,
35617
+ socketId,
35618
+ status: "failed",
35619
+ error: outcome2.error
35620
+ });
35621
+ }
35558
35622
  return commandAck({
35559
35623
  delivery,
35560
35624
  socketId,
@@ -35605,6 +35669,53 @@ var ClaudeCodeCommandHandler = class {
35605
35669
  });
35606
35670
  }
35607
35671
  this.injectedCommands.add(delivery.commandId);
35672
+ const injection = this.injectMessageCommand(
35673
+ activeContext,
35674
+ delivery,
35675
+ message,
35676
+ socketId
35677
+ );
35678
+ this.inFlightMessageCommands.set(delivery.commandId, injection);
35679
+ let outcome;
35680
+ try {
35681
+ outcome = await injection;
35682
+ } finally {
35683
+ this.inFlightMessageCommands.delete(delivery.commandId);
35684
+ }
35685
+ if (outcome.status === "failed") {
35686
+ this.injectedCommands.delete(delivery.commandId);
35687
+ this.input.runtimeLogger?.warn(
35688
+ "agent_bridge_claude_command_failed",
35689
+ commandLogContext(delivery, {
35690
+ socket_id: socketId,
35691
+ duration_ms: Date.now() - startedAt,
35692
+ error: outcome.error
35693
+ })
35694
+ );
35695
+ return commandAck({
35696
+ delivery,
35697
+ socketId,
35698
+ status: "failed",
35699
+ error: outcome.error
35700
+ });
35701
+ }
35702
+ this.input.runtimeLogger?.info(
35703
+ "agent_bridge_claude_command_injected",
35704
+ commandLogContext(delivery, {
35705
+ socket_id: socketId,
35706
+ duration_ms: Date.now() - startedAt
35707
+ })
35708
+ );
35709
+ return commandAck({
35710
+ delivery,
35711
+ socketId,
35712
+ status: "injected"
35713
+ });
35714
+ }
35715
+ // Runs one message command's injection to a terminal outcome. Never rejects:
35716
+ // the returned promise is stored in the in-flight map and awaited by both the
35717
+ // original delivery and any redeliveries, so failures travel as values.
35718
+ async injectMessageCommand(activeContext, delivery, message, socketId) {
35608
35719
  try {
35609
35720
  const emitStartedAt = Date.now();
35610
35721
  this.input.runtimeLogger?.info(
@@ -35650,35 +35761,13 @@ var ClaudeCodeCommandHandler = class {
35650
35761
  duration_ms: Date.now() - sendStartedAt
35651
35762
  })
35652
35763
  );
35764
+ return { status: "injected" };
35653
35765
  } catch (error51) {
35654
- this.injectedCommands.delete(delivery.commandId);
35655
- this.input.runtimeLogger?.warn(
35656
- "agent_bridge_claude_command_failed",
35657
- commandLogContext(delivery, {
35658
- socket_id: socketId,
35659
- duration_ms: Date.now() - startedAt,
35660
- error: error51 instanceof Error ? error51.message : String(error51)
35661
- })
35662
- );
35663
- return commandAck({
35664
- delivery,
35665
- socketId,
35766
+ return {
35666
35767
  status: "failed",
35667
35768
  error: error51 instanceof Error ? error51.message : String(error51)
35668
- });
35769
+ };
35669
35770
  }
35670
- this.input.runtimeLogger?.info(
35671
- "agent_bridge_claude_command_injected",
35672
- commandLogContext(delivery, {
35673
- socket_id: socketId,
35674
- duration_ms: Date.now() - startedAt
35675
- })
35676
- );
35677
- return commandAck({
35678
- delivery,
35679
- socketId,
35680
- status: "injected"
35681
- });
35682
35771
  }
35683
35772
  // -----------------------------------------------------------------------------
35684
35773
  // Question answering
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autohq/cli",
3
- "version": "0.1.287",
3
+ "version": "0.1.289",
4
4
  "license": "SEE LICENSE IN README.md",
5
5
  "publishConfig": {
6
6
  "access": "public"