claude-code-rust 0.14.3 → 0.14.4

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.
@@ -11,12 +11,20 @@ function isEffortLevel(value) {
11
11
  function normalizeModelKey(id) {
12
12
  const original = id.trim();
13
13
  if (!original) {
14
- return { original, family: "unknown", versionParts: [], variantParts: [], buildParts: [] };
14
+ return {
15
+ original,
16
+ family: "unknown",
17
+ versionParts: [],
18
+ variantParts: [],
19
+ buildParts: [],
20
+ };
15
21
  }
16
22
  const lower = original.toLowerCase();
17
23
  const contextMatch = lower.match(/\[([^[\]]+)\]$/);
18
24
  const contextSuffix = contextMatch?.[1];
19
- const withoutContext = contextMatch ? lower.slice(0, contextMatch.index) : lower;
25
+ const withoutContext = contextMatch
26
+ ? lower.slice(0, contextMatch.index)
27
+ : lower;
20
28
  const withoutPrefix = withoutContext.startsWith("claude-")
21
29
  ? withoutContext.slice("claude-".length)
22
30
  : withoutContext;
@@ -82,7 +90,8 @@ function modelKeysAreCompatible(leftId, rightId) {
82
90
  function sameContextSuffix(leftId, rightId) {
83
91
  const left = normalizeModelKey(leftId);
84
92
  const right = normalizeModelKey(rightId);
85
- return (left.contextSuffix?.toLowerCase() ?? "") === (right.contextSuffix?.toLowerCase() ?? "");
93
+ return ((left.contextSuffix?.toLowerCase() ?? "") ===
94
+ (right.contextSuffix?.toLowerCase() ?? ""));
86
95
  }
87
96
  function sameFamilyAndVersion(leftId, rightId) {
88
97
  const left = normalizeModelKey(leftId);
@@ -129,7 +138,9 @@ function humanizeModelId(id) {
129
138
  : normalized.family === "sonnet"
130
139
  ? "Sonnet"
131
140
  : "Haiku";
132
- const versionLabel = normalized.versionParts.length > 0 ? ` ${normalized.versionParts.join(".")}` : "";
141
+ const versionLabel = normalized.versionParts.length > 0
142
+ ? ` ${normalized.versionParts.join(".")}`
143
+ : "";
133
144
  const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
134
145
  ? " [1M]"
135
146
  : normalized.contextSuffix
@@ -181,7 +192,8 @@ export function mapAvailableModels(models) {
181
192
  })
182
193
  .map((entry) => ({
183
194
  id: entry.value,
184
- ...(typeof entry.resolvedModel === "string" && entry.resolvedModel.trim().length > 0
195
+ ...(typeof entry.resolvedModel === "string" &&
196
+ entry.resolvedModel.trim().length > 0
185
197
  ? { resolved_model: entry.resolvedModel.trim() }
186
198
  : {}),
187
199
  display_name: entry.displayName,
@@ -198,7 +210,8 @@ export function mapAvailableModels(models) {
198
210
  ...(typeof entry.supportsAutoMode === "boolean"
199
211
  ? { supports_auto_mode: entry.supportsAutoMode }
200
212
  : {}),
201
- ...(typeof entry.description === "string" && entry.description.trim().length > 0
213
+ ...(typeof entry.description === "string" &&
214
+ entry.description.trim().length > 0
202
215
  ? { description: entry.description }
203
216
  : {}),
204
217
  }));
@@ -1,7 +1,13 @@
1
1
  const SESSION_PERMISSION_DESTINATIONS = new Set(["session", "cliArg"]);
2
- const PERSISTENT_PERMISSION_DESTINATIONS = new Set(["userSettings", "projectSettings", "localSettings"]);
2
+ const PERSISTENT_PERMISSION_DESTINATIONS = new Set([
3
+ "userSettings",
4
+ "projectSettings",
5
+ "localSettings",
6
+ ]);
3
7
  function formatPermissionRule(rule) {
4
- return rule.ruleContent === undefined ? rule.toolName : `${rule.toolName}(${rule.ruleContent})`;
8
+ return rule.ruleContent === undefined
9
+ ? rule.toolName
10
+ : `${rule.toolName}(${rule.ruleContent})`;
5
11
  }
6
12
  export function formatPermissionUpdates(updates) {
7
13
  if (!updates || updates.length === 0) {
@@ -9,8 +15,12 @@ export function formatPermissionUpdates(updates) {
9
15
  }
10
16
  return updates
11
17
  .map((update) => {
12
- if (update.type === "addRules" || update.type === "replaceRules" || update.type === "removeRules") {
13
- const rules = update.rules.map((rule) => formatPermissionRule(rule)).join(", ");
18
+ if (update.type === "addRules" ||
19
+ update.type === "replaceRules" ||
20
+ update.type === "removeRules") {
21
+ const rules = update.rules
22
+ .map((rule) => formatPermissionRule(rule))
23
+ .join(", ");
14
24
  return `${update.type}:${update.behavior}:${update.destination}=[${rules}]`;
15
25
  }
16
26
  if (update.type === "setMode") {
@@ -44,7 +54,9 @@ export function permissionOptionsFromSuggestions(suggestions) {
44
54
  const hasSessionScoped = scoped.session.length > 0;
45
55
  const hasPersistentScoped = scoped.persistent.length > 0;
46
56
  const sessionOnly = hasSessionScoped && !hasPersistentScoped;
47
- const options = [{ option_id: "allow_once", name: "Allow once", kind: "allow_once" }];
57
+ const options = [
58
+ { option_id: "allow_once", name: "Allow once", kind: "allow_once" },
59
+ ];
48
60
  options.push({
49
61
  option_id: sessionOnly ? "allow_session" : "allow_always",
50
62
  name: sessionOnly ? "Allow for session" : "Always allow",
@@ -57,7 +69,11 @@ export function permissionResultFromOutcome(outcome, toolCallId, inputData, sugg
57
69
  const scopedSuggestions = splitPermissionSuggestionsByScope(suggestions);
58
70
  if (outcome.outcome === "selected") {
59
71
  if (outcome.option_id === "allow_once") {
60
- return { behavior: "allow", updatedInput: inputData, toolUseID: toolCallId };
72
+ return {
73
+ behavior: "allow",
74
+ updatedInput: inputData,
75
+ toolUseID: toolCallId,
76
+ };
61
77
  }
62
78
  if (outcome.option_id === "allow_session") {
63
79
  const sessionSuggestions = scopedSuggestions.session;
@@ -105,7 +121,15 @@ export function permissionResultFromOutcome(outcome, toolCallId, inputData, sugg
105
121
  toolUseID: toolCallId,
106
122
  };
107
123
  }
108
- return { behavior: "deny", message: "Permission denied", toolUseID: toolCallId };
124
+ return {
125
+ behavior: "deny",
126
+ message: "Permission denied",
127
+ toolUseID: toolCallId,
128
+ };
109
129
  }
110
- return { behavior: "deny", message: "Permission cancelled", toolUseID: toolCallId };
130
+ return {
131
+ behavior: "deny",
132
+ message: "Permission cancelled",
133
+ toolUseID: toolCallId,
134
+ };
111
135
  }
@@ -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 }
@@ -724,10 +833,18 @@ export function buildQueryOptions(params) {
724
833
  // --setting-sources argument.
725
834
  settingSources: DEFAULT_SETTING_SOURCES,
726
835
  resume: params.resume,
727
- ...(params.resumeSessionAt ? { resumeSessionAt: params.resumeSessionAt } : {}),
836
+ ...(params.resumeSessionAt
837
+ ? { resumeSessionAt: params.resumeSessionAt }
838
+ : {}),
839
+ ...(params.resumeDropsTurn
840
+ ? { resumeDropsTurn: params.resumeDropsTurn }
841
+ : {}),
842
+ ...(params.forkSession
843
+ ? { forkSession: true, sessionId: params.provisionalSessionId }
844
+ : {}),
728
845
  ...(shouldPassCanUseTool ? { canUseTool: params.canUseTool } : {}),
729
- onElicitation: async (request) => {
730
- const requestId = randomUUID();
846
+ onElicitation: async (request, options) => {
847
+ const requestId = options.requestId;
731
848
  const mode = request.mode === "form" || request.mode === "url"
732
849
  ? request.mode
733
850
  : typeof request.url === "string" && request.url.trim().length > 0
@@ -735,21 +852,26 @@ export function buildQueryOptions(params) {
735
852
  : "form";
736
853
  const normalized = {
737
854
  request_id: requestId,
738
- server_name: typeof request.serverName === "string" && request.serverName.trim().length > 0
855
+ server_name: typeof request.serverName === "string" &&
856
+ request.serverName.trim().length > 0
739
857
  ? request.serverName
740
858
  : "unknown",
741
- message: typeof request.message === "string" && request.message.trim().length > 0
859
+ message: typeof request.message === "string" &&
860
+ request.message.trim().length > 0
742
861
  ? request.message
743
862
  : "<no message>",
744
863
  mode,
745
864
  ...(typeof request.url === "string" && request.url.trim().length > 0
746
865
  ? { url: request.url }
747
866
  : {}),
748
- ...(typeof request.elicitationId === "string" && request.elicitationId.trim().length > 0
867
+ ...(typeof request.elicitationId === "string" &&
868
+ request.elicitationId.trim().length > 0
749
869
  ? { elicitation_id: request.elicitationId }
750
870
  : {}),
751
871
  ...(request.requestedSchema
752
- ? { requested_schema: request.requestedSchema }
872
+ ? {
873
+ requested_schema: request.requestedSchema,
874
+ }
753
875
  : {}),
754
876
  };
755
877
  bridgeLogger.info({
@@ -781,8 +903,37 @@ export function buildQueryOptions(params) {
781
903
  resolve({ action: "cancel" });
782
904
  return;
783
905
  }
906
+ if (currentSession.pendingElicitations.has(requestId)) {
907
+ bridgeLogger.warn({
908
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
909
+ eventName: "elicitation_request_dropped",
910
+ message: "duplicate elicitation request dropped",
911
+ outcome: "dropped",
912
+ sessionId: params.sessionIdForLogs(),
913
+ requestId,
914
+ fields: { reason: "duplicate_request_id" },
915
+ });
916
+ resolve({ action: "cancel" });
917
+ return;
918
+ }
919
+ let settled = false;
920
+ const settle = (result) => {
921
+ if (settled) {
922
+ return;
923
+ }
924
+ settled = true;
925
+ currentSession.pendingElicitations.delete(requestId);
926
+ options.signal.removeEventListener("abort", onAbort);
927
+ resolve(result);
928
+ };
929
+ const onAbort = () => settle({ action: "cancel" });
930
+ if (options.signal.aborted) {
931
+ settle({ action: "cancel" });
932
+ return;
933
+ }
934
+ options.signal.addEventListener("abort", onAbort);
784
935
  currentSession.pendingElicitations.set(requestId, {
785
- resolve,
936
+ resolve: settle,
786
937
  serverName: normalized.server_name,
787
938
  elicitationId: normalized.elicitation_id,
788
939
  });
@@ -807,13 +958,15 @@ export function buildQueryOptions(params) {
807
958
  message: "request_user_dialog received for unknown kind; cancelled",
808
959
  outcome: "cancelled",
809
960
  sessionId: params.sessionIdForLogs(),
810
- ...(typeof request.toolUseID === "string" ? { toolCallId: request.toolUseID } : {}),
961
+ ...(typeof request.toolUseID === "string"
962
+ ? { toolCallId: request.toolUseID }
963
+ : {}),
811
964
  fields: { dialog_kind: request.dialogKind },
812
965
  });
813
966
  return { behavior: "cancelled" };
814
967
  }
815
968
  const payload = normalizeRefusalFallbackPayload(request.payload);
816
- const requestId = randomUUID();
969
+ const requestId = options.requestId;
817
970
  const dialogRequest = {
818
971
  request_id: requestId,
819
972
  dialog_kind: REFUSAL_FALLBACK_DIALOG_KIND,
@@ -827,7 +980,9 @@ export function buildQueryOptions(params) {
827
980
  outcome: "start",
828
981
  sessionId: params.sessionIdForLogs(),
829
982
  requestId,
830
- ...(typeof request.toolUseID === "string" ? { toolCallId: request.toolUseID } : {}),
983
+ ...(typeof request.toolUseID === "string"
984
+ ? { toolCallId: request.toolUseID }
985
+ : {}),
831
986
  fields: {
832
987
  dialog_kind: request.dialogKind,
833
988
  original_model: payload.original_model,
@@ -849,6 +1004,19 @@ export function buildQueryOptions(params) {
849
1004
  resolve("cancelled");
850
1005
  return;
851
1006
  }
1007
+ if (currentSession.pendingUserDialogs.has(requestId)) {
1008
+ bridgeLogger.warn({
1009
+ target: LOG_TARGETS.APP_SESSION,
1010
+ eventName: "user_dialog_request_dropped",
1011
+ message: "duplicate user dialog request dropped",
1012
+ outcome: "dropped",
1013
+ sessionId: params.sessionIdForLogs(),
1014
+ requestId,
1015
+ fields: { reason: "duplicate_request_id" },
1016
+ });
1017
+ resolve("cancelled");
1018
+ return;
1019
+ }
852
1020
  let settled = false;
853
1021
  const settle = (value) => {
854
1022
  if (settled) {
@@ -884,7 +1052,9 @@ export function handlePermissionResponse(command) {
884
1052
  toolCallId: command.tool_call_id,
885
1053
  fields: {
886
1054
  response_kind: command.outcome.outcome,
887
- selected_option: command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled",
1055
+ selected_option: command.outcome.outcome === "selected"
1056
+ ? command.outcome.option_id
1057
+ : "cancelled",
888
1058
  },
889
1059
  });
890
1060
  const session = sessionById(command.session_id);
@@ -1021,8 +1191,11 @@ export function handleQuestionResponse(command) {
1021
1191
  fields: {
1022
1192
  tool_name: resolver.toolName,
1023
1193
  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,
1194
+ selected_option_count: command.outcome.outcome === "answered"
1195
+ ? command.outcome.selected_option_ids.length
1196
+ : 0,
1197
+ has_annotation: command.outcome.outcome === "answered" &&
1198
+ command.outcome.annotation !== undefined,
1026
1199
  },
1027
1200
  });
1028
1201
  resolver.onOutcome(command.outcome);
@@ -1037,7 +1210,9 @@ export function handleUserDialogResponse(command) {
1037
1210
  requestId: command.request_id,
1038
1211
  fields: {
1039
1212
  response_kind: command.outcome.outcome,
1040
- selected_option: command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled",
1213
+ selected_option: command.outcome.outcome === "selected"
1214
+ ? command.outcome.option_id
1215
+ : "cancelled",
1041
1216
  },
1042
1217
  });
1043
1218
  const session = sessionById(command.session_id);
@@ -1069,7 +1244,9 @@ export function handleUserDialogResponse(command) {
1069
1244
  return;
1070
1245
  }
1071
1246
  session.pendingUserDialogs.delete(command.request_id);
1072
- const choice = command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled";
1247
+ const choice = command.outcome.outcome === "selected"
1248
+ ? command.outcome.option_id
1249
+ : "cancelled";
1073
1250
  bridgeLogger.info({
1074
1251
  target: LOG_TARGETS.BRIDGE_PERMISSION,
1075
1252
  eventName: "user_dialog_response_applied",
@@ -1136,9 +1313,11 @@ export function handleElicitationResponse(command) {
1136
1313
  });
1137
1314
  pending.resolve({
1138
1315
  action: command.action,
1139
- ...(normalizeSdkElicitationContent(command.content) ? {
1140
- content: normalizeSdkElicitationContent(command.content),
1141
- } : {}),
1316
+ ...(normalizeSdkElicitationContent(command.content)
1317
+ ? {
1318
+ content: normalizeSdkElicitationContent(command.content),
1319
+ }
1320
+ : {}),
1142
1321
  });
1143
1322
  }
1144
1323
  export function shouldInvalidateResolvedRuntimeModel(previousRequestedId, previousSessionModel, nextRequestedId) {