claude-code-rust 0.14.0 → 0.14.2

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.
@@ -3,18 +3,23 @@ import { readFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import readline from "node:readline";
5
5
  import { pathToFileURL } from "node:url";
6
- import { getSessionMessages, listSessions, renameSession, } from "@anthropic-ai/claude-agent-sdk";
7
- import { buildModeState, markModeUnavailableForSession, parseCommandEnvelope, permissionModeFailureLooksUnsupported, refreshSupportedModesForSession, toPermissionMode, } from "./bridge/commands.js";
8
- import { writeEvent, failConnection, slashError, emitRuntimeReloadCompleted, emitRuntimeReloadFailed, emitSessionUpdate, emitSessionsList, currentSessionListOptions, setSessionListingDir, } from "./bridge/events.js";
6
+ import { getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
7
+ import { parseCommandEnvelope } from "./bridge/commands.js";
8
+ import { parseFastModeDisabledReason, parseFastModeState, } from "./bridge/state_parsing.js";
9
+ import { writeEvent, failConnection, slashError, emitRuntimeReloadCompleted, emitRuntimeReloadFailed, emitSessionUpdate, setSessionListingDir, } from "./bridge/events.js";
9
10
  import { contentFromPrompt } from "./bridge/message_handlers.js";
10
- import { sessions, sessionById, createSession, closeAllSessions, handleElicitationResponse, handlePermissionResponse, handleQuestionResponse, handleUserDialogResponse, emitCurrentModelUpdate, refreshCurrentModel, shouldInvalidateResolvedRuntimeModel, } from "./bridge/session_lifecycle.js";
11
+ import { sessions, sessionById, createSession, closeAllSessions, } from "./bridge/session_lifecycle.js";
11
12
  import { mapSessionMessagesToUpdates } from "./bridge/history.js";
12
13
  import { emitAvailableAgentsIfChanged, mapAvailableAgents } from "./bridge/agents.js";
13
14
  import { mapSdkSlashCommands, updateAvailableCommands } from "./bridge/available_commands.js";
14
- import { mapSdkAccountInfo } from "./bridge/account_metadata.js";
15
- import { MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS, emitReconciledMcpSnapshotFromStatuses, handleMcpAuthenticateCommand, handleMcpClearAuthCommand, handleMcpOauthCallbackUrlCommand, handleMcpReconnectCommand, handleMcpSetServersCommand, handleMcpStatusCommand, handleMcpToggleCommand, staleMcpAuthCandidates, } from "./bridge/mcp.js";
15
+ import { MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS, emitReconciledMcpSnapshotFromStatuses, staleMcpAuthCandidates, } from "./bridge/mcp.js";
16
16
  import { bridgeLogger, LOG_TARGETS, logBridgeCommandReceived } from "./bridge/logger.js";
17
- import { dispatchCancelTurnCommand } from "./bridge/command_dispatch.js";
17
+ import { BridgeCommandScheduler } from "./bridge/command_scheduler.js";
18
+ import { handleLifecycleCommand } from "./bridge/command_lifecycle.js";
19
+ import { handleInteractionCommand } from "./bridge/command_interactions.js";
20
+ import { handleMcpCommand } from "./bridge/command_mcp.js";
21
+ import { handleSessionControlCommand } from "./bridge/command_session_control.js";
22
+ import { handleSessionDataCommand } from "./bridge/command_session_data.js";
18
23
  // Re-exports: all symbols that tests and external consumers import from bridge.js.
19
24
  export { AsyncQueue } from "./bridge/shared.js";
20
25
  export { asRecordOrNull } from "./bridge/shared.js";
@@ -108,9 +113,52 @@ export async function generatePersistedSessionTitle(query, description) {
108
113
  return title;
109
114
  }
110
115
  export async function applySessionEffort(query, effort) {
111
- const settings = { effortLevel: effort };
112
- // applyFlagSettings controls live session settings; SDK Settings typings model persisted effort levels.
113
- await query.applyFlagSettings(settings);
116
+ await query.applyFlagSettings({ effortLevel: effort });
117
+ }
118
+ export async function applySessionFastMode(query, enabled) {
119
+ try {
120
+ await query.applyFlagSettings({ fastMode: enabled });
121
+ }
122
+ catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ throw new Error(`SDK rejected the fast-mode change: ${message}`);
125
+ }
126
+ let result;
127
+ try {
128
+ result = await query.reinitialize();
129
+ }
130
+ catch (error) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ throw new Error(`SDK accepted the fast-mode change but state verification failed: ${message}`);
133
+ }
134
+ const state = parseFastModeState(result.fast_mode_state);
135
+ if (state) {
136
+ const disabledReason = parseFastModeDisabledReason(result.fast_mode_disabled_reason);
137
+ return {
138
+ state,
139
+ ...(disabledReason ? { disabled_reason: disabledReason } : {}),
140
+ };
141
+ }
142
+ if (!enabled && result.fast_mode_state === undefined) {
143
+ return { state: "off" };
144
+ }
145
+ throw new Error("SDK accepted the fast-mode change but did not report its resulting state");
146
+ }
147
+ export function buildPromptUserMessage(command, sessionId) {
148
+ const content = contentFromPrompt(command);
149
+ if (content.length === 0) {
150
+ return undefined;
151
+ }
152
+ return {
153
+ type: "user",
154
+ session_id: sessionId,
155
+ parent_tool_use_id: null,
156
+ origin: { kind: "human" },
157
+ message: {
158
+ role: "user",
159
+ content,
160
+ },
161
+ };
114
162
  }
115
163
  export async function applySessionAgent(query, agent) {
116
164
  const settings = { agent };
@@ -130,7 +178,7 @@ export function emitAgentConfigOptionUpdate(sessionId, agent) {
130
178
  value: agent,
131
179
  });
132
180
  }
133
- const EXPECTED_AGENT_SDK_VERSION = "0.3.207";
181
+ const EXPECTED_AGENT_SDK_VERSION = "0.3.220";
134
182
  const require = createRequire(import.meta.url);
135
183
  export function resolveInstalledAgentSdkVersion() {
136
184
  try {
@@ -233,13 +281,19 @@ export function buildRewindConversationPlan(messages, targetUserMessageId) {
233
281
  resumeUpdates: mapSessionMessagesToUpdates(resolved.retainedMessages),
234
282
  };
235
283
  }
236
- function mapRewindFilesResult(result) {
284
+ export function mapRewindFilesResult(result) {
285
+ const skippedLinks = Number.isFinite(result.skippedLinks) &&
286
+ Number.isInteger(result.skippedLinks) &&
287
+ (result.skippedLinks ?? -1) >= 0
288
+ ? result.skippedLinks
289
+ : undefined;
237
290
  return {
238
291
  can_rewind: result.canRewind,
239
292
  ...(result.error ? { error: result.error } : {}),
240
293
  files_changed: result.filesChanged ?? [],
241
294
  ...(result.insertions !== undefined ? { insertions: result.insertions } : {}),
242
295
  ...(result.deletions !== undefined ? { deletions: result.deletions } : {}),
296
+ ...(skippedLinks !== undefined ? { skipped_links: skippedLinks } : {}),
243
297
  };
244
298
  }
245
299
  async function rewindFiles(session, targetUserMessageId, dryRun) {
@@ -513,608 +567,57 @@ async function handleCommand(command, requestId) {
513
567
  }
514
568
  switch (command.command) {
515
569
  case "initialize":
516
- if (sdkVersionError) {
517
- bridgeLogger.error({
518
- target: LOG_TARGETS.BRIDGE_LIFECYCLE,
519
- eventName: "bridge_initialize_failed",
520
- message: "bridge initialization failed due to unsupported SDK version",
521
- outcome: "failure",
522
- ...(requestId ? { requestId } : {}),
523
- fields: { error_message: sdkVersionError },
524
- });
525
- failConnection(sdkVersionError, requestId);
526
- return;
527
- }
528
- setSessionListingDir(command.cwd);
529
- writeEvent({
530
- event: "initialized",
531
- result: {
532
- agent_name: "claude-rs-agent-bridge",
533
- agent_version: "0.1.0",
534
- auth_methods: [
535
- {
536
- id: "claude-login",
537
- name: "Log in with Claude",
538
- description: "Run `claude /login` in a terminal",
539
- },
540
- ],
541
- capabilities: {
542
- prompt_image: true,
543
- prompt_embedded_context: true,
544
- supports_session_listing: true,
545
- supports_resume_session: true,
546
- },
547
- },
548
- }, requestId);
549
- await emitSessionsList(requestId);
550
- return;
551
570
  case "create_session":
552
- bridgeLogger.info({
553
- target: LOG_TARGETS.APP_SESSION,
554
- eventName: "session_create_requested",
555
- message: "session creation requested",
556
- outcome: "start",
557
- ...(requestId ? { requestId } : {}),
558
- fields: {
559
- cwd: command.cwd,
560
- resume_requested: command.resume !== undefined,
561
- },
562
- });
563
- setSessionListingDir(command.cwd);
564
- await createSession({
565
- cwd: command.cwd,
566
- resume: command.resume,
567
- launchSettings: command.launch_settings,
568
- connectEvent: "connected",
569
- requestId,
570
- });
571
- return;
572
- case "resume_session": {
573
- bridgeLogger.info({
574
- target: LOG_TARGETS.APP_SESSION,
575
- eventName: "session_resume_requested",
576
- message: "session resume requested",
577
- outcome: "start",
578
- ...(requestId ? { requestId } : {}),
579
- sessionId: command.session_id,
580
- });
581
- try {
582
- const sdkSessions = await listSessions(currentSessionListOptions());
583
- const matched = sdkSessions.find((entry) => entry.sessionId === command.session_id);
584
- if (!matched) {
585
- bridgeLogger.warn({
586
- target: LOG_TARGETS.APP_SESSION,
587
- eventName: "session_resume_lookup_failed",
588
- message: "session resume requested for an unknown session",
589
- outcome: "failure",
590
- ...(requestId ? { requestId } : {}),
591
- sessionId: command.session_id,
592
- fields: { reason: "unknown_session" },
593
- });
594
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
595
- return;
596
- }
597
- setSessionListingDir(matched.cwd ?? process.cwd());
598
- const historyMessages = await getSessionMessages(command.session_id, matched.cwd
599
- ? { dir: matched.cwd, includeSystemMessages: true }
600
- : { includeSystemMessages: true });
601
- const resumeUpdates = mapSessionMessagesToUpdates(historyMessages);
602
- const staleSessions = Array.from(sessions.values());
603
- const hadActiveSession = staleSessions.length > 0;
604
- bridgeLogger.info({
605
- target: LOG_TARGETS.APP_SESSION,
606
- eventName: "session_resume_history_loaded",
607
- message: "session resume history loaded",
608
- outcome: "success",
609
- ...(requestId ? { requestId } : {}),
610
- sessionId: command.session_id,
611
- fields: {
612
- history_update_count: resumeUpdates.length,
613
- stale_session_count: staleSessions.length,
614
- },
615
- });
616
- await createSession({
617
- cwd: matched.cwd ?? process.cwd(),
618
- resume: command.session_id,
619
- launchSettings: command.launch_settings,
620
- ...(resumeUpdates.length > 0 ? { resumeUpdates } : {}),
621
- connectEvent: hadActiveSession ? "session_replaced" : "connected",
622
- requestId,
623
- ...(hadActiveSession ? { sessionsToCloseAfterConnect: staleSessions } : {}),
624
- });
625
- }
626
- catch (error) {
627
- const message = error instanceof Error ? error.message : String(error);
628
- bridgeLogger.error({
629
- target: LOG_TARGETS.APP_SESSION,
630
- eventName: "session_resume_failed",
631
- message: "session resume failed",
632
- outcome: "failure",
633
- ...(requestId ? { requestId } : {}),
634
- sessionId: command.session_id,
635
- fields: { error_message: message },
636
- });
637
- slashError(command.session_id, `failed to resume session: ${message}`, requestId);
638
- }
639
- return;
640
- }
571
+ case "resume_session":
641
572
  case "new_session":
642
- bridgeLogger.info({
643
- target: LOG_TARGETS.APP_SESSION,
644
- eventName: "session_new_requested",
645
- message: "replacement session requested",
646
- outcome: "start",
647
- ...(requestId ? { requestId } : {}),
648
- fields: { cwd: command.cwd },
649
- });
650
- await closeAllSessions({ reason: "new_session_requested", requestId });
651
- setSessionListingDir(command.cwd);
652
- await createSession({
653
- cwd: command.cwd,
654
- launchSettings: command.launch_settings,
655
- connectEvent: "session_replaced",
656
- requestId,
657
- });
658
- return;
659
- case "prompt": {
660
- const session = sessionById(command.session_id);
661
- if (!session) {
662
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
663
- return;
664
- }
665
- const content = contentFromPrompt(command);
666
- if (content.length === 0) {
667
- return;
668
- }
669
- const message = {
670
- type: "user",
671
- session_id: session.sessionId,
672
- parent_tool_use_id: null,
673
- message: {
674
- role: "user",
675
- content,
676
- },
677
- };
678
- session.input.enqueue(message);
679
- return;
680
- }
681
- case "cancel_turn": {
682
- await dispatchCancelTurnCommand(command, { requestId, sessionById, slashError });
683
- return;
684
- }
685
- case "set_model": {
686
- const session = sessionById(command.session_id);
687
- if (!session) {
688
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
689
- return;
690
- }
691
- bridgeLogger.info({
692
- target: LOG_TARGETS.APP_SESSION,
693
- eventName: "set_model_started",
694
- message: "set model started",
695
- outcome: "start",
696
- sessionId: session.sessionId,
697
- requestId,
698
- fields: {
699
- requested_model: command.model,
700
- previous_requested_model: session.requestedModelId,
701
- previous_session_model: session.model,
702
- previous_resolved_runtime_model: session.resolvedRuntimeModelId,
703
- previous_current_model: session.currentModel?.resolved_id,
704
- },
573
+ case "shutdown":
574
+ await handleLifecycleCommand(command, requestId, sdkVersionError);
575
+ return;
576
+ case "prompt":
577
+ case "cancel_turn":
578
+ case "set_model":
579
+ case "set_mode":
580
+ case "set_effort":
581
+ case "set_agent":
582
+ case "set_fast_mode":
583
+ case "reload_plugins":
584
+ await handleSessionControlCommand(command, requestId, {
585
+ buildPromptUserMessage,
586
+ applySessionEffort,
587
+ applySessionAgent,
588
+ applySessionFastMode,
589
+ emitEffortConfigOptionUpdate,
590
+ emitAgentConfigOptionUpdate,
591
+ handleReloadPluginsCommand,
705
592
  });
706
- try {
707
- const previousRequestedModel = session.requestedModelId;
708
- const previousSessionModel = session.model;
709
- await session.query.setModel(command.model);
710
- session.requestedModelId = command.model;
711
- session.model = command.model;
712
- const invalidatedResolvedRuntimeModel = shouldInvalidateResolvedRuntimeModel(previousRequestedModel, previousSessionModel, command.model);
713
- if (invalidatedResolvedRuntimeModel) {
714
- session.resolvedRuntimeModelId = undefined;
715
- }
716
- const changed = refreshCurrentModel(session, true);
717
- const forcedCurrentModelUpdate = !changed && emitCurrentModelUpdate(session);
718
- bridgeLogger.info({
719
- target: LOG_TARGETS.APP_SESSION,
720
- eventName: "set_model_succeeded",
721
- message: "set model completed",
722
- outcome: "success",
723
- sessionId: session.sessionId,
724
- requestId,
725
- fields: {
726
- requested_model: command.model,
727
- session_model_after: session.model,
728
- resolved_runtime_model_after: session.resolvedRuntimeModelId,
729
- current_model_after: session.currentModel?.resolved_id,
730
- current_model_display_short: session.currentModel?.display_name_short,
731
- current_model_display_long: session.currentModel?.display_name_long,
732
- current_model_update_emitted: changed || forcedCurrentModelUpdate,
733
- current_model_update_forced: forcedCurrentModelUpdate,
734
- resolved_runtime_model_invalidated: invalidatedResolvedRuntimeModel,
735
- },
736
- });
737
- refreshSupportedModesForSession(session);
738
- if (session.mode) {
739
- emitSessionUpdate(session.sessionId, {
740
- type: "mode_state_update",
741
- mode: buildModeState(session, session.mode),
742
- });
743
- }
744
- }
745
- catch (error) {
746
- const message = error instanceof Error ? error.message : String(error);
747
- bridgeLogger.warn({
748
- target: LOG_TARGETS.APP_SESSION,
749
- eventName: "set_model_failed",
750
- message: "set model failed",
751
- outcome: "failure",
752
- sessionId: session.sessionId,
753
- requestId,
754
- fields: {
755
- requested_model: command.model,
756
- error_message: message,
757
- previous_requested_model: session.requestedModelId,
758
- previous_session_model: session.model,
759
- previous_resolved_runtime_model: session.resolvedRuntimeModelId,
760
- previous_current_model: session.currentModel?.resolved_id,
761
- },
762
- });
763
- slashError(command.session_id, `failed to set model: ${message}`, requestId);
764
- }
765
593
  return;
766
- }
767
- case "set_mode": {
768
- const session = sessionById(command.session_id);
769
- if (!session) {
770
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
771
- return;
772
- }
773
- const mode = toPermissionMode(command.mode);
774
- if (!mode) {
775
- slashError(command.session_id, `unsupported mode: ${command.mode}`, requestId);
776
- return;
777
- }
778
- try {
779
- await session.query.setPermissionMode(mode);
780
- session.mode = mode;
781
- refreshSupportedModesForSession(session);
782
- emitSessionUpdate(session.sessionId, {
783
- type: "current_mode_update",
784
- current_mode_id: mode,
785
- });
786
- }
787
- catch (error) {
788
- const message = error instanceof Error ? error.message : String(error);
789
- if (permissionModeFailureLooksUnsupported(mode, message)) {
790
- const changed = markModeUnavailableForSession(session, mode);
791
- if (changed && session.mode) {
792
- emitSessionUpdate(session.sessionId, {
793
- type: "mode_state_update",
794
- mode: buildModeState(session, session.mode),
795
- });
796
- }
797
- }
798
- slashError(command.session_id, `failed to set mode to ${mode}: ${message}`, requestId);
799
- }
800
- return;
801
- }
802
- case "set_effort": {
803
- const session = sessionById(command.session_id);
804
- if (!session) {
805
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
806
- return;
807
- }
808
- try {
809
- await applySessionEffort(session.query, command.effort);
810
- emitEffortConfigOptionUpdate(session.sessionId, command.effort);
811
- }
812
- catch (error) {
813
- const message = error instanceof Error ? error.message : String(error);
814
- slashError(command.session_id, `failed to set effort: ${message}`, requestId);
815
- }
816
- return;
817
- }
818
- case "set_agent": {
819
- const session = sessionById(command.session_id);
820
- if (!session) {
821
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
822
- return;
823
- }
824
- try {
825
- await applySessionAgent(session.query, command.agent);
826
- emitAgentConfigOptionUpdate(session.sessionId, command.agent);
827
- }
828
- catch (error) {
829
- const message = error instanceof Error ? error.message : String(error);
830
- slashError(command.session_id, `failed to set agent: ${message}`, requestId);
831
- }
832
- return;
833
- }
834
- case "generate_session_title": {
835
- const session = sessionById(command.session_id);
836
- if (!session) {
837
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
838
- return;
839
- }
840
- try {
841
- await generatePersistedSessionTitle(session.query, command.description);
842
- setSessionListingDir(session.cwd);
843
- await emitSessionsList(requestId);
844
- }
845
- catch (error) {
846
- const message = error instanceof Error ? error.message : String(error);
847
- slashError(command.session_id, `failed to generate session title: ${message}`, requestId);
848
- }
849
- return;
850
- }
851
- case "rename_session": {
852
- const session = sessionById(command.session_id);
853
- if (!session) {
854
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
855
- return;
856
- }
857
- try {
858
- await renameSession(command.session_id, command.title, buildSessionMutationOptions(session.cwd));
859
- setSessionListingDir(session.cwd);
860
- await emitSessionsList(requestId);
861
- }
862
- catch (error) {
863
- const message = error instanceof Error ? error.message : String(error);
864
- slashError(command.session_id, `failed to rename session: ${message}`, requestId);
865
- }
866
- return;
867
- }
868
- case "get_status_snapshot": {
869
- const session = sessionById(command.session_id);
870
- if (!session) {
871
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
872
- return;
873
- }
874
- try {
875
- const account = await session.query.accountInfo();
876
- bridgeLogger.info({
877
- target: LOG_TARGETS.APP_AUTH,
878
- eventName: "status_snapshot_emitted",
879
- message: "status snapshot emitted",
880
- outcome: "success",
881
- ...(requestId ? { requestId } : {}),
882
- sessionId: session.sessionId,
883
- fields: {
884
- has_email: typeof account.email === "string" && account.email.trim().length > 0,
885
- has_organization: account.organization !== undefined,
886
- subscription_type: account.subscriptionType,
887
- token_source: account.tokenSource,
888
- api_key_source: account.apiKeySource,
889
- api_provider: account.apiProvider,
890
- },
891
- });
892
- writeEvent({
893
- event: "status_snapshot",
894
- session_id: session.sessionId,
895
- account: mapSdkAccountInfo(account),
896
- }, requestId);
897
- }
898
- catch (error) {
899
- const message = error instanceof Error ? error.message : String(error);
900
- bridgeLogger.warn({
901
- target: LOG_TARGETS.APP_AUTH,
902
- eventName: "status_snapshot_failed",
903
- message: "failed to build status snapshot",
904
- outcome: "failure",
905
- ...(requestId ? { requestId } : {}),
906
- sessionId: session.sessionId,
907
- fields: { error_message: message },
908
- });
909
- throw error;
910
- }
911
- return;
912
- }
913
- case "get_context_usage": {
914
- const session = sessionById(command.session_id);
915
- if (!session) {
916
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
917
- return;
918
- }
919
- try {
920
- const usage = await session.query.getContextUsage();
921
- if (typeof usage.model === "string" && usage.model.trim().length > 0) {
922
- session.resolvedRuntimeModelId = usage.model.trim();
923
- refreshCurrentModel(session, true);
924
- }
925
- const rawPercentage = typeof usage.percentage === "number" ? usage.percentage : undefined;
926
- const normalizedPercentage = rawPercentage === undefined || !Number.isFinite(rawPercentage)
927
- ? undefined
928
- : Math.max(0, Math.min(100, Math.round(rawPercentage)));
929
- bridgeLogger.debug({
930
- target: LOG_TARGETS.APP_SESSION,
931
- eventName: "context_usage_succeeded",
932
- message: "session context usage received from SDK",
933
- outcome: "success",
934
- ...(requestId ? { requestId } : {}),
935
- sessionId: session.sessionId,
936
- fields: {
937
- raw_percentage: rawPercentage,
938
- normalized_percentage: normalizedPercentage,
939
- total_tokens: typeof usage.totalTokens === "number" ? usage.totalTokens : undefined,
940
- max_tokens: typeof usage.maxTokens === "number" ? usage.maxTokens : undefined,
941
- raw_max_tokens: typeof usage.rawMaxTokens === "number" ? usage.rawMaxTokens : undefined,
942
- model: typeof usage.model === "string" ? usage.model : undefined,
943
- },
944
- });
945
- writeEvent({
946
- event: "context_usage",
947
- session_id: session.sessionId,
948
- ...(normalizedPercentage !== undefined ? { percentage: normalizedPercentage } : {}),
949
- }, requestId);
950
- }
951
- catch (error) {
952
- const message = error instanceof Error ? error.message : String(error);
953
- bridgeLogger.warn({
954
- target: LOG_TARGETS.APP_SESSION,
955
- eventName: "context_usage_failed",
956
- message: "failed to get session context usage",
957
- outcome: "failure",
958
- ...(requestId ? { requestId } : {}),
959
- sessionId: session.sessionId,
960
- fields: { error_message: message },
961
- });
962
- writeEvent({
963
- event: "context_usage",
964
- session_id: session.sessionId,
965
- }, requestId);
966
- }
967
- return;
968
- }
969
- case "get_rewind_targets": {
970
- const session = sessionById(command.session_id);
971
- if (!session) {
972
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
973
- return;
974
- }
975
- try {
976
- const historyMessages = await getSessionMessages(command.session_id, {
977
- dir: session.cwd,
978
- includeSystemMessages: true,
979
- });
980
- const targets = rewindTargetsFromSessionMessages(historyMessages);
981
- bridgeLogger.info({
982
- target: LOG_TARGETS.APP_SESSION,
983
- eventName: "rewind_targets_loaded",
984
- message: "rewind targets loaded from session history",
985
- outcome: "success",
986
- ...(requestId ? { requestId } : {}),
987
- sessionId: session.sessionId,
988
- fields: {
989
- history_message_count: historyMessages.length,
990
- target_count: targets.length,
991
- },
992
- });
993
- writeEvent({
994
- event: "rewind_targets",
995
- session_id: session.sessionId,
996
- targets,
997
- }, requestId);
998
- }
999
- catch (error) {
1000
- const message = error instanceof Error ? error.message : String(error);
1001
- bridgeLogger.warn({
1002
- target: LOG_TARGETS.APP_SESSION,
1003
- eventName: "rewind_targets_failed",
1004
- message: "failed to load rewind targets",
1005
- outcome: "failure",
1006
- ...(requestId ? { requestId } : {}),
1007
- sessionId: session.sessionId,
1008
- fields: { error_message: message },
1009
- });
1010
- slashError(command.session_id, `failed to load rewind targets: ${message}`, requestId);
1011
- }
1012
- return;
1013
- }
594
+ case "generate_session_title":
595
+ case "rename_session":
596
+ case "get_status_snapshot":
597
+ case "get_context_usage":
598
+ case "get_rewind_targets":
1014
599
  case "rewind":
1015
- await handleRewind(command, requestId);
1016
- return;
1017
- case "reload_plugins": {
1018
- const session = sessionById(command.session_id);
1019
- if (!session) {
1020
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1021
- return;
1022
- }
1023
- await handleReloadPluginsCommand(session, requestId);
1024
- return;
1025
- }
1026
- case "mcp_status": {
1027
- const session = sessionById(command.session_id);
1028
- if (!session) {
1029
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1030
- return;
1031
- }
1032
- await handleMcpStatusCommand(session, requestId);
1033
- return;
1034
- }
1035
- case "mcp_reconnect": {
1036
- const session = sessionById(command.session_id);
1037
- if (!session) {
1038
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1039
- return;
1040
- }
1041
- await handleMcpReconnectCommand(session, command, requestId);
1042
- return;
1043
- }
1044
- case "mcp_toggle": {
1045
- const session = sessionById(command.session_id);
1046
- if (!session) {
1047
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1048
- return;
1049
- }
1050
- await handleMcpToggleCommand(session, command, requestId);
1051
- return;
1052
- }
1053
- case "mcp_set_servers": {
1054
- const session = sessionById(command.session_id);
1055
- if (!session) {
1056
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1057
- return;
1058
- }
1059
- await handleMcpSetServersCommand(session, command, requestId);
1060
- return;
1061
- }
1062
- case "mcp_authenticate": {
1063
- const session = sessionById(command.session_id);
1064
- if (!session) {
1065
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1066
- return;
1067
- }
1068
- await handleMcpAuthenticateCommand(session, command, requestId);
600
+ await handleSessionDataCommand(command, requestId, {
601
+ generatePersistedSessionTitle,
602
+ buildSessionMutationOptions,
603
+ rewindTargetsFromSessionMessages,
604
+ handleRewind,
605
+ });
1069
606
  return;
1070
- }
1071
- case "mcp_clear_auth": {
1072
- const session = sessionById(command.session_id);
1073
- if (!session) {
1074
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1075
- return;
1076
- }
1077
- await handleMcpClearAuthCommand(session, command, requestId);
607
+ case "mcp_status":
608
+ case "mcp_reconnect":
609
+ case "mcp_toggle":
610
+ case "mcp_set_servers":
611
+ case "mcp_authenticate":
612
+ case "mcp_clear_auth":
613
+ case "mcp_oauth_callback_url":
614
+ await handleMcpCommand(command, requestId);
1078
615
  return;
1079
- }
1080
- case "mcp_oauth_callback_url": {
1081
- const session = sessionById(command.session_id);
1082
- if (!session) {
1083
- slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1084
- return;
1085
- }
1086
- await handleMcpOauthCallbackUrlCommand(session, command, requestId);
1087
- return;
1088
- }
1089
616
  case "permission_response":
1090
- handlePermissionResponse(command);
1091
- return;
1092
617
  case "question_response":
1093
- handleQuestionResponse(command);
1094
- return;
1095
618
  case "user_dialog_response":
1096
- handleUserDialogResponse(command);
1097
- return;
1098
619
  case "elicitation_response":
1099
- handleElicitationResponse(command);
1100
- return;
1101
- case "shutdown":
1102
- bridgeLogger.info({
1103
- target: LOG_TARGETS.BRIDGE_LIFECYCLE,
1104
- eventName: "bridge_shutdown_requested",
1105
- message: "bridge shutdown requested",
1106
- outcome: "start",
1107
- ...(requestId ? { requestId } : {}),
1108
- });
1109
- await closeAllSessions({ reason: "bridge_shutdown_requested", requestId });
1110
- bridgeLogger.info({
1111
- target: LOG_TARGETS.BRIDGE_LIFECYCLE,
1112
- eventName: "bridge_shutdown_completed",
1113
- message: "bridge shutdown completed",
1114
- outcome: "success",
1115
- ...(requestId ? { requestId } : {}),
1116
- });
1117
- process.exit(0);
620
+ handleInteractionCommand(command);
1118
621
  return;
1119
622
  default:
1120
623
  bridgeLogger.error({
@@ -1143,34 +646,35 @@ function main() {
1143
646
  input: process.stdin,
1144
647
  crlfDelay: Number.POSITIVE_INFINITY,
1145
648
  });
649
+ const commandScheduler = new BridgeCommandScheduler();
1146
650
  rl.on("line", (line) => {
1147
651
  if (line.trim().length === 0) {
1148
652
  return;
1149
653
  }
1150
- void (async () => {
1151
- let parsed;
1152
- try {
1153
- parsed = parseCommandEnvelope(line);
1154
- }
1155
- catch (error) {
1156
- const message = error instanceof Error ? error.message : String(error);
1157
- const requestId = requestIdFromCommandLine(line);
1158
- bridgeLogger.error({
1159
- target: LOG_TARGETS.BRIDGE_PROTOCOL,
1160
- eventName: "bridge_command_decode_failed",
1161
- message: "failed to decode bridge command envelope",
1162
- outcome: "failure",
1163
- ...(requestId ? { requestId } : {}),
1164
- sizeBytes: Buffer.byteLength(line),
1165
- fields: {
1166
- preview: line.slice(0, 240),
1167
- preview_chars: Math.min(line.length, 240),
1168
- error_message: message,
1169
- },
1170
- });
1171
- failConnection(`invalid command envelope: ${message}`, requestId);
1172
- return;
1173
- }
654
+ let parsed;
655
+ try {
656
+ parsed = parseCommandEnvelope(line);
657
+ }
658
+ catch (error) {
659
+ const message = error instanceof Error ? error.message : String(error);
660
+ const requestId = requestIdFromCommandLine(line);
661
+ bridgeLogger.error({
662
+ target: LOG_TARGETS.BRIDGE_PROTOCOL,
663
+ eventName: "bridge_command_decode_failed",
664
+ message: "failed to decode bridge command envelope",
665
+ outcome: "failure",
666
+ ...(requestId ? { requestId } : {}),
667
+ sizeBytes: Buffer.byteLength(line),
668
+ fields: {
669
+ preview: line.slice(0, 240),
670
+ preview_chars: Math.min(line.length, 240),
671
+ error_message: message,
672
+ },
673
+ });
674
+ failConnection(`invalid command envelope: ${message}`, requestId);
675
+ return;
676
+ }
677
+ commandScheduler.schedule(parsed.command, async () => {
1174
678
  try {
1175
679
  await handleCommand(parsed.command, parsed.requestId);
1176
680
  }
@@ -1182,7 +686,8 @@ function main() {
1182
686
  message: "bridge command handler failed",
1183
687
  outcome: "failure",
1184
688
  ...(parsed.requestId ? { requestId: parsed.requestId } : {}),
1185
- ...(parsed.command.command === "create_session" || parsed.command.command === "new_session"
689
+ ...(parsed.command.command === "create_session" ||
690
+ parsed.command.command === "new_session"
1186
691
  ? {}
1187
692
  : "session_id" in parsed.command
1188
693
  ? { sessionId: parsed.command.session_id }
@@ -1194,9 +699,10 @@ function main() {
1194
699
  });
1195
700
  failConnection(`bridge command failed (${parsed.command.command}): ${message}`, parsed.requestId);
1196
701
  }
1197
- })();
702
+ });
1198
703
  });
1199
704
  rl.on("close", () => {
705
+ commandScheduler.stopAccepting();
1200
706
  bridgeLogger.info({
1201
707
  target: LOG_TARGETS.BRIDGE_LIFECYCLE,
1202
708
  eventName: "bridge_input_closed",