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