claude-code-rust 0.14.3 → 0.14.5

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.
@@ -6,17 +6,19 @@ import { bridgeLogger, LOG_TARGETS, logSdkStderrLine } from "./logger.js";
6
6
  import { AsyncQueue } from "./shared.js";
7
7
  import { permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./permissions.js";
8
8
  import { failConnection, emitSessionUpdate, emitConnectEvent, emitPermissionRequestEvent, emitElicitationRequestEvent, emitUserDialogRequestEvent, } from "./events.js";
9
- import { ensureToolCallVisible, setToolCallStatus, } from "./tool_calls.js";
9
+ import { ensureToolCallVisible, setToolCallStatus } from "./tool_calls.js";
10
10
  import { isToolSearchToolName } from "./tooling.js";
11
11
  import { requestExitPlanModeApproval, requestAskUserQuestionAnswers, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "./user_interaction.js";
12
- import { mapAvailableAgents, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
12
+ import { mapAvailableAgents, emitAvailableAgentsIfChanged, refreshAvailableAgents, } from "./agents.js";
13
13
  import { mapSdkSlashCommands, updateAvailableCommands, } from "./available_commands.js";
14
14
  import { emitAuthRequired, emitFastModeUpdate, setFastModeSnapshotIfChanged, } from "./error_classification.js";
15
15
  import { mapAvailableModels, resolveCurrentModel, currentModelsEqual, } from "./model_metadata.js";
16
16
  import { shouldEmitStartupAuthRequiredForAccount } from "./account_metadata.js";
17
17
  export { mapAvailableModels, resolveCurrentModel } from "./model_metadata.js";
18
18
  export { shouldEmitStartupAuthRequiredForAccount } from "./account_metadata.js";
19
- const BRIDGE_RUNTIME_PROCESS_NAME = process.platform === "win32" ? "claude-rs-bridge-bun.exe" : "claude-rs-bridge-bun";
19
+ const BRIDGE_RUNTIME_PROCESS_NAME = process.platform === "win32"
20
+ ? "claude-rs-bridge-bun.exe"
21
+ : "claude-rs-bridge-bun";
20
22
  const BRIDGE_RUNTIME_GUARD_PROMPT = `Do not terminate the Claude Rust bridge runtime process \`${BRIDGE_RUNTIME_PROCESS_NAME}\`; ` +
21
23
  "when cleaning up development servers, only stop processes by explicit PIDs you started in this session.";
22
24
  const STARTUP_FALLBACK_MODEL_ALIAS = "fable";
@@ -82,7 +84,9 @@ function normalizeRefusalFallbackPayload(payload) {
82
84
  ? { guidance_text: optionalString(payload.guidanceText) }
83
85
  : {}),
84
86
  ...(optionalStringArray(payload.retractedMessageUuids) !== undefined
85
- ? { retracted_message_uuids: optionalStringArray(payload.retractedMessageUuids) }
87
+ ? {
88
+ retracted_message_uuids: optionalStringArray(payload.retractedMessageUuids),
89
+ }
86
90
  : {}),
87
91
  };
88
92
  }
@@ -93,8 +97,14 @@ function normalizeRefusalFallbackPayload(payload) {
93
97
  */
94
98
  function buildRefusalFallbackOptions(payload) {
95
99
  return [
96
- { option_id: "retry_fallback", label: `Switch to ${payload.fallback_model}` },
97
- { option_id: "edit_prompt", label: `Edit prompt and retry with ${payload.original_model}` },
100
+ {
101
+ option_id: "retry_fallback",
102
+ label: `Switch to ${payload.fallback_model}`,
103
+ },
104
+ {
105
+ option_id: "edit_prompt",
106
+ label: `Edit prompt and retry with ${payload.original_model}`,
107
+ },
98
108
  ];
99
109
  }
100
110
  function settingsObjectFromLaunchSettings(launchSettings) {
@@ -110,7 +120,9 @@ function normalizedSettingsFromLaunchSettings(launchSettings) {
110
120
  ...settings,
111
121
  feedbackDrafts: "off",
112
122
  };
113
- const sandbox = settings.sandbox && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)
123
+ const sandbox = settings.sandbox &&
124
+ typeof settings.sandbox === "object" &&
125
+ !Array.isArray(settings.sandbox)
114
126
  ? settings.sandbox
115
127
  : undefined;
116
128
  if (sandbox?.enabled === true && sandbox.failIfUnavailable === undefined) {
@@ -247,10 +259,12 @@ export async function closeAllSessions(options = {}) {
247
259
  }
248
260
  export async function createSession(params) {
249
261
  const input = new AsyncQueue();
250
- const provisionalSessionId = params.resume ?? randomUUID();
262
+ const provisionalSessionId = params.sessionId ??
263
+ (params.resume && !params.forkSession ? params.resume : randomUUID());
251
264
  const initialModel = initialSessionModel(params.launchSettings);
252
265
  const initialMode = initialSessionMode(params.launchSettings);
253
- const supportsBypassPermissionsMode = startupPermissionModeOptions(params.launchSettings).allowDangerouslySkipPermissions === true;
266
+ const supportsBypassPermissionsMode = startupPermissionModeOptions(params.launchSettings)
267
+ .allowDangerouslySkipPermissions === true;
254
268
  const historyUpdateCount = params.resumeUpdates?.length ?? 0;
255
269
  const staleSessionCount = params.sessionsToCloseAfterConnect?.length ?? 0;
256
270
  const staleSessionBeforeRegisterCount = params.sessionsToCloseBeforeRegister?.length ?? 0;
@@ -260,7 +274,11 @@ export async function createSession(params) {
260
274
  const toolUseId = options.toolUseID;
261
275
  if (isToolSearchToolName(toolName)) {
262
276
  session?.hiddenToolUseIds.add(toolUseId);
263
- return { behavior: "allow", updatedInput: inputData, toolUseID: toolUseId };
277
+ return {
278
+ behavior: "allow",
279
+ updatedInput: inputData,
280
+ toolUseID: toolUseId,
281
+ };
264
282
  }
265
283
  if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
266
284
  const existing = ensureToolCallVisible(session, toolUseId, toolName, inputData);
@@ -321,6 +339,8 @@ export async function createSession(params) {
321
339
  connect_event: params.connectEvent,
322
340
  resume_requested: params.resume !== undefined,
323
341
  resume_session_at: params.resumeSessionAt ?? "<none>",
342
+ resume_drops_turn: params.resumeDropsTurn ?? "<none>",
343
+ fork_session: params.forkSession === true,
324
344
  history_update_count: historyUpdateCount,
325
345
  stale_session_count: staleSessionCount,
326
346
  stale_session_before_register_count: staleSessionBeforeRegisterCount,
@@ -333,6 +353,8 @@ export async function createSession(params) {
333
353
  cwd: params.cwd,
334
354
  resume: params.resume,
335
355
  resumeSessionAt: params.resumeSessionAt,
356
+ resumeDropsTurn: params.resumeDropsTurn,
357
+ forkSession: params.forkSession,
336
358
  launchSettings: params.launchSettings,
337
359
  provisionalSessionId,
338
360
  input,
@@ -379,6 +401,10 @@ export async function createSession(params) {
379
401
  query: queryHandle,
380
402
  input,
381
403
  connected: false,
404
+ ...(params.deferConnect ? { deferConnect: true } : {}),
405
+ ...(params.resumeDropsTurn
406
+ ? { resumeDropsTurn: params.resumeDropsTurn }
407
+ : {}),
382
408
  closing: false,
383
409
  connectEvent: params.connectEvent,
384
410
  connectRequestId: params.requestId,
@@ -400,7 +426,9 @@ export async function createSession(params) {
400
426
  ...(params.resumeUpdates && params.resumeUpdates.length > 0
401
427
  ? { resumeUpdates: params.resumeUpdates }
402
428
  : {}),
403
- ...(params.restoredInput !== undefined ? { restoredInput: params.restoredInput } : {}),
429
+ ...(params.restoredInput !== undefined
430
+ ? { restoredInput: params.restoredInput }
431
+ : {}),
404
432
  ...(params.pendingRewindResult !== undefined
405
433
  ? { pendingRewindResult: params.pendingRewindResult }
406
434
  : {}),
@@ -425,6 +453,8 @@ export async function createSession(params) {
425
453
  connect_event: session.connectEvent,
426
454
  resume_requested: params.resume !== undefined,
427
455
  resume_session_at: params.resumeSessionAt ?? "<none>",
456
+ resume_drops_turn: params.resumeDropsTurn ?? "<none>",
457
+ fork_session: params.forkSession === true,
428
458
  },
429
459
  });
430
460
  bridgeLogger.info({
@@ -451,10 +481,14 @@ export async function createSession(params) {
451
481
  eventName: "session_initialization_completed",
452
482
  message: "session initialization completed",
453
483
  outcome: "success",
454
- ...(session.connectRequestId ? { requestId: session.connectRequestId } : {}),
484
+ ...(session.connectRequestId
485
+ ? { requestId: session.connectRequestId }
486
+ : {}),
455
487
  sessionId: session.sessionId,
456
488
  fields: {
457
- available_model_count: Array.isArray(result.models) ? result.models.length : 0,
489
+ available_model_count: Array.isArray(result.models)
490
+ ? result.models.length
491
+ : 0,
458
492
  connect_event: session.connectEvent,
459
493
  history_update_count: session.resumeUpdates?.length ?? 0,
460
494
  },
@@ -464,7 +498,12 @@ export async function createSession(params) {
464
498
  const { buildModeState, refreshSupportedModesForSession } = await import("./commands.js");
465
499
  refreshSupportedModesForSession(session);
466
500
  const fastModeChanged = setFastModeSnapshotIfChanged(session, result.fast_mode_state, result.fast_mode_disabled_reason);
467
- if (!session.connected) {
501
+ if (!session.connected &&
502
+ session.deferConnect &&
503
+ !session.initializationError) {
504
+ session.initializationReady = true;
505
+ }
506
+ else if (!session.connected) {
468
507
  emitConnectEvent(session);
469
508
  }
470
509
  else {
@@ -495,16 +534,21 @@ export async function createSession(params) {
495
534
  return;
496
535
  }
497
536
  const message = error instanceof Error ? error.message : String(error);
537
+ session.initializationError = message;
498
538
  bridgeLogger.error({
499
539
  target: LOG_TARGETS.APP_SESSION,
500
540
  eventName: "session_initialization_failed",
501
541
  message: "session initialization failed before connect",
502
542
  outcome: "failure",
503
- ...(session.connectRequestId ? { requestId: session.connectRequestId } : {}),
543
+ ...(session.connectRequestId
544
+ ? { requestId: session.connectRequestId }
545
+ : {}),
504
546
  sessionId: session.sessionId,
505
547
  fields: { error_message: message },
506
548
  });
507
- failConnection(`agent initialization failed: ${message}`, session.connectRequestId);
549
+ if (!session.deferConnect) {
550
+ failConnection(`agent initialization failed: ${message}`, session.connectRequestId);
551
+ }
508
552
  session.connectRequestId = undefined;
509
553
  });
510
554
  session.queryConsumerTask = (async () => {
@@ -528,11 +572,16 @@ export async function createSession(params) {
528
572
  ...(params.requestId ? { requestId: params.requestId } : {}),
529
573
  sessionId: session.sessionId,
530
574
  });
531
- failConnection("agent stream ended before session initialization", params.requestId);
575
+ session.initializationError =
576
+ "agent stream ended before session initialization";
577
+ if (!session.deferConnect) {
578
+ failConnection("agent stream ended before session initialization", params.requestId);
579
+ }
532
580
  }
533
581
  }
534
582
  catch (error) {
535
583
  const message = error instanceof Error ? error.message : String(error);
584
+ session.initializationError = message;
536
585
  bridgeLogger.error({
537
586
  target: LOG_TARGETS.APP_SESSION,
538
587
  eventName: "session_stream_failed_before_connect",
@@ -542,11 +591,65 @@ export async function createSession(params) {
542
591
  sessionId: session.sessionId,
543
592
  fields: { error_message: message },
544
593
  });
545
- failConnection(`agent stream failed: ${message}`, params.requestId);
594
+ if (!session.deferConnect) {
595
+ failConnection(`agent stream failed: ${message}`, params.requestId);
596
+ }
546
597
  }
547
598
  })();
548
599
  return session;
549
600
  }
601
+ export async function awaitSessionInitialization(session) {
602
+ await session.initializationTask;
603
+ if (session.deferConnect &&
604
+ session.resumeDropsTurn &&
605
+ !session.initializationError) {
606
+ // The guard is evaluated at fork time and a refusal is delivered as a result event,
607
+ // not as a rejected initialization request. A post-initialize control response is
608
+ // the protocol barrier proving that startup messages have reached the SDK; yield
609
+ // once more so the bridge's query consumer can process the already-enqueued result.
610
+ try {
611
+ await session.query.getContextUsage();
612
+ }
613
+ catch (error) {
614
+ const message = error instanceof Error ? error.message : String(error);
615
+ bridgeLogger.warn({
616
+ target: LOG_TARGETS.APP_SESSION,
617
+ eventName: "session_resume_guard_barrier_failed",
618
+ message: "guarded resume control fence failed",
619
+ outcome: "failure",
620
+ sessionId: session.sessionId,
621
+ fields: { error_message: message },
622
+ });
623
+ throw new Error(`guarded resume validation fence failed: ${message}`);
624
+ }
625
+ await new Promise((resolve) => setImmediate(resolve));
626
+ session.resumeGuardFenceComplete = true;
627
+ }
628
+ if (session.initializationError) {
629
+ throw new Error(session.initializationError);
630
+ }
631
+ if (session.deferConnect && !session.initializationReady) {
632
+ throw new Error("candidate session did not reach initialization readiness");
633
+ }
634
+ if (!session.deferConnect && !session.connected) {
635
+ throw new Error("session did not connect after initialization");
636
+ }
637
+ }
638
+ export function commitDeferredSession(session) {
639
+ if (!session.deferConnect ||
640
+ !session.initializationReady ||
641
+ session.initializationError ||
642
+ (session.resumeDropsTurn !== undefined && !session.resumeGuardFenceComplete)) {
643
+ throw new Error("candidate session is not ready to commit");
644
+ }
645
+ const deferredAuthRequired = session.deferredAuthRequired;
646
+ session.deferredAuthRequired = undefined;
647
+ session.deferConnect = false;
648
+ emitConnectEvent(session);
649
+ if (deferredAuthRequired) {
650
+ emitAuthRequired(session, deferredAuthRequired.detail);
651
+ }
652
+ }
550
653
  function logSdkProcessSpawnStarted(options, includeArgsPreview) {
551
654
  bridgeLogger.info({
552
655
  target: LOG_TARGETS.BRIDGE_SDK,
@@ -622,14 +725,19 @@ function startupModelOption(launchSettings) {
622
725
  }
623
726
  function initialSessionMode(launchSettings) {
624
727
  const settings = settingsObjectFromLaunchSettings(launchSettings);
625
- const permissions = settings?.permissions && typeof settings.permissions === "object" && !Array.isArray(settings.permissions)
728
+ const permissions = settings?.permissions &&
729
+ typeof settings.permissions === "object" &&
730
+ !Array.isArray(settings.permissions)
626
731
  ? settings.permissions
627
732
  : undefined;
628
- return permissionModeFromSettingsValue(permissions?.defaultMode) ?? DEFAULT_PERMISSION_MODE;
733
+ return (permissionModeFromSettingsValue(permissions?.defaultMode) ??
734
+ DEFAULT_PERMISSION_MODE);
629
735
  }
630
736
  function startupPermissionModeOptions(launchSettings) {
631
737
  const settings = settingsObjectFromLaunchSettings(launchSettings);
632
- const permissions = settings?.permissions && typeof settings.permissions === "object" && !Array.isArray(settings.permissions)
738
+ const permissions = settings?.permissions &&
739
+ typeof settings.permissions === "object" &&
740
+ !Array.isArray(settings.permissions)
633
741
  ? settings.permissions
634
742
  : undefined;
635
743
  const permissionMode = permissionModeFromSettingsValue(permissions?.defaultMode);
@@ -667,10 +775,9 @@ export function buildQueryOptions(params) {
667
775
  includePartialMessages: true,
668
776
  promptSuggestions: true,
669
777
  enableFileCheckpointing: true,
670
- // ProposeSkills reports only a proposal count and expects a native review
671
- // surface. Keep it out of this host until claude-rs can display the actual
672
- // proposal input and accept/reject it without losing content.
673
- disallowedTools: ["ProposeSkills"],
778
+ // Proposal tools require native review and lifecycle surfaces that the
779
+ // public headless SDK does not currently expose to this host.
780
+ disallowedTools: ["ProposeSkills", "ProposeGoal"],
674
781
  executable: "bun",
675
782
  ...(params.resume ? {} : { sessionId: params.provisionalSessionId }),
676
783
  settings,
@@ -679,7 +786,9 @@ export function buildQueryOptions(params) {
679
786
  toolConfig: { askUserQuestion: { previewFormat: "markdown" } },
680
787
  systemPrompt,
681
788
  ...(params.launchSettings.agent_progress_summaries !== undefined
682
- ? { agentProgressSummaries: params.launchSettings.agent_progress_summaries }
789
+ ? {
790
+ agentProgressSummaries: params.launchSettings.agent_progress_summaries,
791
+ }
683
792
  : {}),
684
793
  ...(params.claudeCodeExecutable
685
794
  ? { pathToClaudeCodeExecutable: params.claudeCodeExecutable }
@@ -693,11 +802,15 @@ export function buildQueryOptions(params) {
693
802
  },
694
803
  spawnClaudeCodeProcess: (options) => {
695
804
  const command = resolveClaudeCodeSpawnCommand(options.command);
696
- const spawnOptions = { ...options, command };
805
+ const env = { ...options.env };
806
+ if (env.CLAUDE_CODE_ENABLE_TODO_TOOLS === undefined) {
807
+ env.CLAUDE_CODE_ENABLE_TODO_TOOLS = "1";
808
+ }
809
+ const spawnOptions = { ...options, command, env };
697
810
  logSdkProcessSpawnStarted(spawnOptions, params.enableSpawnDebug);
698
811
  const child = spawnChild(command, options.args, {
699
812
  cwd: options.cwd,
700
- env: options.env,
813
+ env,
701
814
  signal: options.signal,
702
815
  stdio: ["pipe", "pipe", "pipe"],
703
816
  windowsHide: true,
@@ -724,10 +837,18 @@ export function buildQueryOptions(params) {
724
837
  // --setting-sources argument.
725
838
  settingSources: DEFAULT_SETTING_SOURCES,
726
839
  resume: params.resume,
727
- ...(params.resumeSessionAt ? { resumeSessionAt: params.resumeSessionAt } : {}),
840
+ ...(params.resumeSessionAt
841
+ ? { resumeSessionAt: params.resumeSessionAt }
842
+ : {}),
843
+ ...(params.resumeDropsTurn
844
+ ? { resumeDropsTurn: params.resumeDropsTurn }
845
+ : {}),
846
+ ...(params.forkSession
847
+ ? { forkSession: true, sessionId: params.provisionalSessionId }
848
+ : {}),
728
849
  ...(shouldPassCanUseTool ? { canUseTool: params.canUseTool } : {}),
729
- onElicitation: async (request) => {
730
- const requestId = randomUUID();
850
+ onElicitation: async (request, options) => {
851
+ const requestId = options.requestId;
731
852
  const mode = request.mode === "form" || request.mode === "url"
732
853
  ? request.mode
733
854
  : typeof request.url === "string" && request.url.trim().length > 0
@@ -735,21 +856,26 @@ export function buildQueryOptions(params) {
735
856
  : "form";
736
857
  const normalized = {
737
858
  request_id: requestId,
738
- server_name: typeof request.serverName === "string" && request.serverName.trim().length > 0
859
+ server_name: typeof request.serverName === "string" &&
860
+ request.serverName.trim().length > 0
739
861
  ? request.serverName
740
862
  : "unknown",
741
- message: typeof request.message === "string" && request.message.trim().length > 0
863
+ message: typeof request.message === "string" &&
864
+ request.message.trim().length > 0
742
865
  ? request.message
743
866
  : "<no message>",
744
867
  mode,
745
868
  ...(typeof request.url === "string" && request.url.trim().length > 0
746
869
  ? { url: request.url }
747
870
  : {}),
748
- ...(typeof request.elicitationId === "string" && request.elicitationId.trim().length > 0
871
+ ...(typeof request.elicitationId === "string" &&
872
+ request.elicitationId.trim().length > 0
749
873
  ? { elicitation_id: request.elicitationId }
750
874
  : {}),
751
875
  ...(request.requestedSchema
752
- ? { requested_schema: request.requestedSchema }
876
+ ? {
877
+ requested_schema: request.requestedSchema,
878
+ }
753
879
  : {}),
754
880
  };
755
881
  bridgeLogger.info({
@@ -781,8 +907,37 @@ export function buildQueryOptions(params) {
781
907
  resolve({ action: "cancel" });
782
908
  return;
783
909
  }
910
+ if (currentSession.pendingElicitations.has(requestId)) {
911
+ bridgeLogger.warn({
912
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
913
+ eventName: "elicitation_request_dropped",
914
+ message: "duplicate elicitation request dropped",
915
+ outcome: "dropped",
916
+ sessionId: params.sessionIdForLogs(),
917
+ requestId,
918
+ fields: { reason: "duplicate_request_id" },
919
+ });
920
+ resolve({ action: "cancel" });
921
+ return;
922
+ }
923
+ let settled = false;
924
+ const settle = (result) => {
925
+ if (settled) {
926
+ return;
927
+ }
928
+ settled = true;
929
+ currentSession.pendingElicitations.delete(requestId);
930
+ options.signal.removeEventListener("abort", onAbort);
931
+ resolve(result);
932
+ };
933
+ const onAbort = () => settle({ action: "cancel" });
934
+ if (options.signal.aborted) {
935
+ settle({ action: "cancel" });
936
+ return;
937
+ }
938
+ options.signal.addEventListener("abort", onAbort);
784
939
  currentSession.pendingElicitations.set(requestId, {
785
- resolve,
940
+ resolve: settle,
786
941
  serverName: normalized.server_name,
787
942
  elicitationId: normalized.elicitation_id,
788
943
  });
@@ -794,8 +949,7 @@ export function buildQueryOptions(params) {
794
949
  // the CLI emits `request_user_dialog` and the host renders the chooser below.
795
950
  supportedDialogKinds: [REFUSAL_FALLBACK_DIALOG_KIND],
796
951
  // Host policy for `request_user_dialog` control requests. Unknown kinds are
797
- // logged and answered `{ behavior: "cancelled" }` (the spec-required answer
798
- // for unrecognized kinds). For `refusal_fallback_prompt`, we surface an
952
+ // logged and left unsettled by returning `null`. For `refusal_fallback_prompt`, we surface an
799
953
  // interactive chooser in the TUI and route the user's decision back to the
800
954
  // CLI as `{ behavior: "completed", result: <choice> }` (or cancelled on
801
955
  // decline/abort/teardown).
@@ -804,16 +958,18 @@ export function buildQueryOptions(params) {
804
958
  bridgeLogger.warn({
805
959
  target: LOG_TARGETS.APP_SESSION,
806
960
  eventName: "user_dialog_received",
807
- message: "request_user_dialog received for unknown kind; cancelled",
808
- outcome: "cancelled",
961
+ message: "request_user_dialog received for unknown kind; unsupported",
962
+ outcome: "unsupported",
809
963
  sessionId: params.sessionIdForLogs(),
810
- ...(typeof request.toolUseID === "string" ? { toolCallId: request.toolUseID } : {}),
964
+ ...(typeof request.toolUseID === "string"
965
+ ? { toolCallId: request.toolUseID }
966
+ : {}),
811
967
  fields: { dialog_kind: request.dialogKind },
812
968
  });
813
- return { behavior: "cancelled" };
969
+ return null;
814
970
  }
815
971
  const payload = normalizeRefusalFallbackPayload(request.payload);
816
- const requestId = randomUUID();
972
+ const requestId = options.requestId;
817
973
  const dialogRequest = {
818
974
  request_id: requestId,
819
975
  dialog_kind: REFUSAL_FALLBACK_DIALOG_KIND,
@@ -827,7 +983,9 @@ export function buildQueryOptions(params) {
827
983
  outcome: "start",
828
984
  sessionId: params.sessionIdForLogs(),
829
985
  requestId,
830
- ...(typeof request.toolUseID === "string" ? { toolCallId: request.toolUseID } : {}),
986
+ ...(typeof request.toolUseID === "string"
987
+ ? { toolCallId: request.toolUseID }
988
+ : {}),
831
989
  fields: {
832
990
  dialog_kind: request.dialogKind,
833
991
  original_model: payload.original_model,
@@ -849,6 +1007,19 @@ export function buildQueryOptions(params) {
849
1007
  resolve("cancelled");
850
1008
  return;
851
1009
  }
1010
+ if (currentSession.pendingUserDialogs.has(requestId)) {
1011
+ bridgeLogger.warn({
1012
+ target: LOG_TARGETS.APP_SESSION,
1013
+ eventName: "user_dialog_request_dropped",
1014
+ message: "duplicate user dialog request dropped",
1015
+ outcome: "dropped",
1016
+ sessionId: params.sessionIdForLogs(),
1017
+ requestId,
1018
+ fields: { reason: "duplicate_request_id" },
1019
+ });
1020
+ resolve("cancelled");
1021
+ return;
1022
+ }
852
1023
  let settled = false;
853
1024
  const settle = (value) => {
854
1025
  if (settled) {
@@ -884,7 +1055,9 @@ export function handlePermissionResponse(command) {
884
1055
  toolCallId: command.tool_call_id,
885
1056
  fields: {
886
1057
  response_kind: command.outcome.outcome,
887
- selected_option: command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled",
1058
+ selected_option: command.outcome.outcome === "selected"
1059
+ ? command.outcome.option_id
1060
+ : "cancelled",
888
1061
  },
889
1062
  });
890
1063
  const session = sessionById(command.session_id);
@@ -1021,8 +1194,11 @@ export function handleQuestionResponse(command) {
1021
1194
  fields: {
1022
1195
  tool_name: resolver.toolName,
1023
1196
  response_kind: command.outcome.outcome,
1024
- selected_option_count: command.outcome.outcome === "answered" ? command.outcome.selected_option_ids.length : 0,
1025
- has_annotation: command.outcome.outcome === "answered" && command.outcome.annotation !== undefined,
1197
+ selected_option_count: command.outcome.outcome === "answered"
1198
+ ? command.outcome.selected_option_ids.length
1199
+ : 0,
1200
+ has_annotation: command.outcome.outcome === "answered" &&
1201
+ command.outcome.annotation !== undefined,
1026
1202
  },
1027
1203
  });
1028
1204
  resolver.onOutcome(command.outcome);
@@ -1037,7 +1213,9 @@ export function handleUserDialogResponse(command) {
1037
1213
  requestId: command.request_id,
1038
1214
  fields: {
1039
1215
  response_kind: command.outcome.outcome,
1040
- selected_option: command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled",
1216
+ selected_option: command.outcome.outcome === "selected"
1217
+ ? command.outcome.option_id
1218
+ : "cancelled",
1041
1219
  },
1042
1220
  });
1043
1221
  const session = sessionById(command.session_id);
@@ -1069,7 +1247,9 @@ export function handleUserDialogResponse(command) {
1069
1247
  return;
1070
1248
  }
1071
1249
  session.pendingUserDialogs.delete(command.request_id);
1072
- const choice = command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled";
1250
+ const choice = command.outcome.outcome === "selected"
1251
+ ? command.outcome.option_id
1252
+ : "cancelled";
1073
1253
  bridgeLogger.info({
1074
1254
  target: LOG_TARGETS.BRIDGE_PERMISSION,
1075
1255
  eventName: "user_dialog_response_applied",
@@ -1136,9 +1316,11 @@ export function handleElicitationResponse(command) {
1136
1316
  });
1137
1317
  pending.resolve({
1138
1318
  action: command.action,
1139
- ...(normalizeSdkElicitationContent(command.content) ? {
1140
- content: normalizeSdkElicitationContent(command.content),
1141
- } : {}),
1319
+ ...(normalizeSdkElicitationContent(command.content)
1320
+ ? {
1321
+ content: normalizeSdkElicitationContent(command.content),
1322
+ }
1323
+ : {}),
1142
1324
  });
1143
1325
  }
1144
1326
  export function shouldInvalidateResolvedRuntimeModel(previousRequestedId, previousSessionModel, nextRequestedId) {
@@ -17,7 +17,9 @@ function nonNegativeNumberField(record, ...keys) {
17
17
  }
18
18
  export function nonNegativeIntegerField(record, ...keys) {
19
19
  const value = numberField(record, ...keys);
20
- return value !== undefined && value >= 0 && Number.isInteger(value) ? value : undefined;
20
+ return value !== undefined && value >= 0 && Number.isInteger(value)
21
+ ? value
22
+ : undefined;
21
23
  }
22
24
  export function buildSubagentRetryUpdate(message) {
23
25
  const retry = asRecordOrNull(message.subagent_retry);
@@ -27,7 +29,9 @@ export function buildSubagentRetryUpdate(message) {
27
29
  const attempt = nonNegativeIntegerField(retry, "attempt");
28
30
  const maxRetries = nonNegativeIntegerField(retry, "max_retries", "maxRetries");
29
31
  const retryDelayMs = nonNegativeIntegerField(retry, "retry_delay_ms", "retryDelayMs");
30
- if (attempt === undefined || maxRetries === undefined || retryDelayMs === undefined) {
32
+ if (attempt === undefined ||
33
+ maxRetries === undefined ||
34
+ retryDelayMs === undefined) {
31
35
  return null;
32
36
  }
33
37
  const agentId = typeof retry.agent_id === "string" ? retry.agent_id.trim() : "";
@@ -57,7 +61,9 @@ export function parseFastModeDisabledReason(value) {
57
61
  return reason.length > 0 ? reason : undefined;
58
62
  }
59
63
  export function parseRateLimitStatus(value) {
60
- if (value === "allowed" || value === "allowed_warning" || value === "rejected") {
64
+ if (value === "allowed" ||
65
+ value === "allowed_warning" ||
66
+ value === "rejected") {
61
67
  return value;
62
68
  }
63
69
  return null;
@@ -71,6 +77,7 @@ export function parseRuntimeSessionState(value) {
71
77
  export function parseApiRetryError(value) {
72
78
  switch (value) {
73
79
  case "authentication_failed":
80
+ case "account_on_hold":
74
81
  case "oauth_org_not_allowed":
75
82
  case "billing_error":
76
83
  case "rate_limit":
@@ -119,7 +126,8 @@ export function buildRateLimitUpdate(rateLimitInfo) {
119
126
  if (overageResetsAt !== undefined) {
120
127
  update.overage_resets_at = overageResetsAt;
121
128
  }
122
- if (typeof info.overageDisabledReason === "string" && info.overageDisabledReason.length > 0) {
129
+ if (typeof info.overageDisabledReason === "string" &&
130
+ info.overageDisabledReason.length > 0) {
123
131
  update.overage_disabled_reason = info.overageDisabledReason;
124
132
  }
125
133
  if (typeof info.overageInUse === "boolean") {
@@ -136,7 +144,8 @@ export function buildRateLimitUpdate(rateLimitInfo) {
136
144
  update.can_user_purchase_credits = info.canUserPurchaseCredits;
137
145
  }
138
146
  if (typeof info.hasChargeableSavedPaymentMethod === "boolean") {
139
- update.has_chargeable_saved_payment_method = info.hasChargeableSavedPaymentMethod;
147
+ update.has_chargeable_saved_payment_method =
148
+ info.hasChargeableSavedPaymentMethod;
140
149
  }
141
150
  return update;
142
151
  }
@@ -144,11 +153,15 @@ export function buildApiRetryUpdate(message) {
144
153
  const attempt = numberField(message, "attempt");
145
154
  const maxRetries = numberField(message, "max_retries", "maxRetries");
146
155
  const retryDelayMs = nonNegativeNumberField(message, "retry_delay_ms", "retryDelayMs");
147
- if (attempt === undefined || maxRetries === undefined || retryDelayMs === undefined) {
156
+ if (attempt === undefined ||
157
+ maxRetries === undefined ||
158
+ retryDelayMs === undefined) {
148
159
  return null;
149
160
  }
150
161
  const rawStatus = message.error_status ?? message.errorStatus;
151
- const errorStatus = typeof rawStatus === "number" && Number.isFinite(rawStatus) ? rawStatus : null;
162
+ const errorStatus = typeof rawStatus === "number" && Number.isFinite(rawStatus)
163
+ ? rawStatus
164
+ : null;
152
165
  return {
153
166
  type: "api_retry_update",
154
167
  attempt,
@@ -168,7 +181,9 @@ export function normalizeSettingsParseError(value) {
168
181
  return null;
169
182
  }
170
183
  const path = typeof record.path === "string" ? record.path : "";
171
- const file = typeof record.file === "string" && record.file.trim() ? record.file : undefined;
184
+ const file = typeof record.file === "string" && record.file.trim()
185
+ ? record.file
186
+ : undefined;
172
187
  return {
173
188
  ...(file ? { file } : {}),
174
189
  path,