@getpaseo/server 0.1.3 → 0.1.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.
Files changed (120) hide show
  1. package/dist/server/client/daemon-client-relay-e2ee-transport.d.ts +8 -0
  2. package/dist/server/client/daemon-client-relay-e2ee-transport.d.ts.map +1 -0
  3. package/dist/server/client/daemon-client-relay-e2ee-transport.js +161 -0
  4. package/dist/server/client/daemon-client-relay-e2ee-transport.js.map +1 -0
  5. package/dist/server/client/daemon-client-terminal-stream-manager.d.ts +43 -0
  6. package/dist/server/client/daemon-client-terminal-stream-manager.d.ts.map +1 -0
  7. package/dist/server/client/daemon-client-terminal-stream-manager.js +130 -0
  8. package/dist/server/client/daemon-client-terminal-stream-manager.js.map +1 -0
  9. package/dist/server/client/daemon-client-transport-types.d.ts +34 -0
  10. package/dist/server/client/daemon-client-transport-types.d.ts.map +1 -0
  11. package/dist/server/client/daemon-client-transport-types.js +2 -0
  12. package/dist/server/client/daemon-client-transport-types.js.map +1 -0
  13. package/dist/server/client/daemon-client-transport-utils.d.ts +9 -0
  14. package/dist/server/client/daemon-client-transport-utils.d.ts.map +1 -0
  15. package/dist/server/client/daemon-client-transport-utils.js +121 -0
  16. package/dist/server/client/daemon-client-transport-utils.js.map +1 -0
  17. package/dist/server/client/daemon-client-transport.d.ts +5 -0
  18. package/dist/server/client/daemon-client-transport.d.ts.map +1 -0
  19. package/dist/server/client/daemon-client-transport.js +4 -0
  20. package/dist/server/client/daemon-client-transport.js.map +1 -0
  21. package/dist/server/client/daemon-client-websocket-transport.d.ts +7 -0
  22. package/dist/server/client/daemon-client-websocket-transport.d.ts.map +1 -0
  23. package/dist/server/client/daemon-client-websocket-transport.js +64 -0
  24. package/dist/server/client/daemon-client-websocket-transport.js.map +1 -0
  25. package/dist/server/client/daemon-client.d.ts +44 -32
  26. package/dist/server/client/daemon-client.d.ts.map +1 -1
  27. package/dist/server/client/daemon-client.js +463 -774
  28. package/dist/server/client/daemon-client.js.map +1 -1
  29. package/dist/server/server/agent/agent-manager.d.ts +45 -0
  30. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  31. package/dist/server/server/agent/agent-manager.js +197 -9
  32. package/dist/server/server/agent/agent-manager.js.map +1 -1
  33. package/dist/server/server/agent/agent-storage.d.ts +4 -4
  34. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
  35. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +120 -6
  36. package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
  37. package/dist/server/server/agent/providers/claude-agent.d.ts +7 -0
  38. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  39. package/dist/server/server/agent/providers/claude-agent.js +83 -9
  40. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  41. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  42. package/dist/server/server/agent/providers/codex-app-server-agent.js +25 -0
  43. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  44. package/dist/server/server/agent/stt-manager.d.ts +3 -2
  45. package/dist/server/server/agent/stt-manager.d.ts.map +1 -1
  46. package/dist/server/server/agent/stt-manager.js +5 -3
  47. package/dist/server/server/agent/stt-manager.js.map +1 -1
  48. package/dist/server/server/agent/timeline-projection.d.ts +19 -0
  49. package/dist/server/server/agent/timeline-projection.d.ts.map +1 -0
  50. package/dist/server/server/agent/timeline-projection.js +142 -0
  51. package/dist/server/server/agent/timeline-projection.js.map +1 -0
  52. package/dist/server/server/agent/tts-manager.d.ts +3 -2
  53. package/dist/server/server/agent/tts-manager.d.ts.map +1 -1
  54. package/dist/server/server/agent/tts-manager.js +5 -3
  55. package/dist/server/server/agent/tts-manager.js.map +1 -1
  56. package/dist/server/server/agent-attention-policy.d.ts +20 -0
  57. package/dist/server/server/agent-attention-policy.d.ts.map +1 -0
  58. package/dist/server/server/agent-attention-policy.js +40 -0
  59. package/dist/server/server/agent-attention-policy.js.map +1 -0
  60. package/dist/server/server/bootstrap.d.ts.map +1 -1
  61. package/dist/server/server/bootstrap.js +13 -18
  62. package/dist/server/server/bootstrap.js.map +1 -1
  63. package/dist/server/server/dictation/dictation-stream-manager.d.ts +10 -2
  64. package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -1
  65. package/dist/server/server/dictation/dictation-stream-manager.js +81 -14
  66. package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
  67. package/dist/server/server/exports.d.ts +1 -1
  68. package/dist/server/server/exports.d.ts.map +1 -1
  69. package/dist/server/server/persisted-config.d.ts +4 -4
  70. package/dist/server/server/relay-transport.d.ts +3 -2
  71. package/dist/server/server/relay-transport.d.ts.map +1 -1
  72. package/dist/server/server/relay-transport.js +21 -5
  73. package/dist/server/server/relay-transport.js.map +1 -1
  74. package/dist/server/server/session.d.ts +32 -8
  75. package/dist/server/server/session.d.ts.map +1 -1
  76. package/dist/server/server/session.js +499 -79
  77. package/dist/server/server/session.js.map +1 -1
  78. package/dist/server/server/speech/provider-resolver.d.ts +3 -0
  79. package/dist/server/server/speech/provider-resolver.d.ts.map +1 -0
  80. package/dist/server/server/speech/provider-resolver.js +7 -0
  81. package/dist/server/server/speech/provider-resolver.js.map +1 -0
  82. package/dist/server/server/speech/providers/local/runtime.d.ts +1 -0
  83. package/dist/server/server/speech/providers/local/runtime.d.ts.map +1 -1
  84. package/dist/server/server/speech/providers/local/runtime.js +4 -3
  85. package/dist/server/server/speech/providers/local/runtime.js.map +1 -1
  86. package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts.map +1 -1
  87. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +3 -66
  88. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js.map +1 -1
  89. package/dist/server/server/speech/speech-runtime.d.ts +26 -3
  90. package/dist/server/server/speech/speech-runtime.d.ts.map +1 -1
  91. package/dist/server/server/speech/speech-runtime.js +466 -112
  92. package/dist/server/server/speech/speech-runtime.js.map +1 -1
  93. package/dist/server/server/websocket-server.d.ts +23 -7
  94. package/dist/server/server/websocket-server.d.ts.map +1 -1
  95. package/dist/server/server/websocket-server.js +288 -64
  96. package/dist/server/server/websocket-server.js.map +1 -1
  97. package/dist/server/shared/binary-mux.d.ts +31 -0
  98. package/dist/server/shared/binary-mux.d.ts.map +1 -0
  99. package/dist/server/shared/binary-mux.js +101 -0
  100. package/dist/server/shared/binary-mux.js.map +1 -0
  101. package/dist/server/shared/messages.d.ts +4655 -3615
  102. package/dist/server/shared/messages.d.ts.map +1 -1
  103. package/dist/server/shared/messages.js +184 -26
  104. package/dist/server/shared/messages.js.map +1 -1
  105. package/dist/server/shared/terminal-key-input.d.ts +9 -0
  106. package/dist/server/shared/terminal-key-input.d.ts.map +1 -0
  107. package/dist/server/shared/terminal-key-input.js +132 -0
  108. package/dist/server/shared/terminal-key-input.js.map +1 -0
  109. package/dist/server/terminal/terminal.d.ts +17 -0
  110. package/dist/server/terminal/terminal.d.ts.map +1 -1
  111. package/dist/server/terminal/terminal.js +89 -0
  112. package/dist/server/terminal/terminal.js.map +1 -1
  113. package/dist/server/utils/checkout-git.d.ts +4 -0
  114. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  115. package/dist/server/utils/checkout-git.js +92 -0
  116. package/dist/server/utils/checkout-git.js.map +1 -1
  117. package/dist/server/utils/worktree.d.ts.map +1 -1
  118. package/dist/server/utils/worktree.js +33 -4
  119. package/dist/server/utils/worktree.js.map +1 -1
  120. package/package.json +2 -2
@@ -6,6 +6,7 @@ import { promisify } from "util";
6
6
  import { join, resolve, sep } from "path";
7
7
  import { z } from "zod";
8
8
  import { serializeAgentStreamEvent, } from "./messages.js";
9
+ import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from "../shared/binary-mux.js";
9
10
  import { TTSManager } from "./agent/tts-manager.js";
10
11
  import { STTManager } from "./agent/stt-manager.js";
11
12
  import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
@@ -16,13 +17,14 @@ import { experimental_createMCPClient } from "ai";
16
17
  import { buildProviderRegistry } from "./agent/provider-registry.js";
17
18
  import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
18
19
  import { toAgentPayload } from "./agent/agent-projections.js";
20
+ import { projectTimelineRows } from "./agent/timeline-projection.js";
19
21
  import { StructuredAgentResponseError, generateStructuredAgentResponse, } from "./agent/agent-response-loop.js";
20
22
  import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
21
23
  import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
22
24
  import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
23
25
  import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
24
26
  import { createWorktree, runWorktreeSetupCommands, WorktreeSetupError, slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
25
- import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from "../utils/checkout-git.js";
27
+ import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from "../utils/checkout-git.js";
26
28
  import { getProjectIcon } from "../utils/project-icon.js";
27
29
  import { expandTilde } from "../utils/path.js";
28
30
  import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from "./speech/providers/local/models.js";
@@ -39,6 +41,9 @@ const PROJECT_PLACEMENT_CACHE_TTL_MS = 10000;
39
41
  const MAX_AGENTS_PER_PROJECT = 5;
40
42
  const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
41
43
  const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
44
+ const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
45
+ const TERMINAL_STREAM_MAX_PENDING_BYTES = 2 * 1024 * 1024;
46
+ const TERMINAL_STREAM_MAX_PENDING_CHUNKS = 2048;
42
47
  /**
43
48
  * Default model used for auto-generating commit messages and PR descriptions.
44
49
  * Uses Claude Haiku for speed and cost efficiency.
@@ -116,6 +121,15 @@ const VOICE_INTERNAL_DICTATION_ID_PREFIX = "__voice_turn__:";
116
121
  const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
117
122
  const AgentIdSchema = z.string().uuid();
118
123
  const VOICE_MCP_SERVER_NAME = "paseo_voice";
124
+ class VoiceFeatureUnavailableError extends Error {
125
+ constructor(context) {
126
+ super(context.message);
127
+ this.name = "VoiceFeatureUnavailableError";
128
+ this.reasonCode = context.reasonCode;
129
+ this.retryable = context.retryable;
130
+ this.missingModelIds = [...context.missingModelIds];
131
+ }
132
+ }
119
133
  function convertPCMToWavBuffer(pcmBuffer, sampleRate, channels, bitsPerSample) {
120
134
  const headerSize = 44;
121
135
  const wavBuffer = Buffer.alloc(headerSize + pcmBuffer.length);
@@ -199,14 +213,18 @@ export class Session {
199
213
  this.clientActivity = null;
200
214
  this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
201
215
  this.terminalSubscriptions = new Map();
216
+ this.terminalStreams = new Map();
217
+ this.terminalStreamByTerminalId = new Map();
218
+ this.nextTerminalStreamId = 1;
202
219
  this.checkoutDiffSubscriptions = new Map();
203
220
  this.checkoutDiffTargets = new Map();
204
221
  this.voiceModeAgentId = null;
205
222
  this.voiceModeBaseConfig = null;
206
- const { clientId, onMessage, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
223
+ const { clientId, onMessage, onBinaryMessage, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
207
224
  this.clientId = clientId;
208
225
  this.sessionId = uuidv4();
209
226
  this.onMessage = onMessage;
227
+ this.onBinaryMessage = onBinaryMessage ?? null;
210
228
  this.downloadTokenStore = downloadTokenStore;
211
229
  this.pushTokenStore = pushTokenStore;
212
230
  this.paseoHome = paseoHome;
@@ -230,6 +248,7 @@ export class Session {
230
248
  this.unregisterVoiceCallerContext = voiceBridge?.unregisterVoiceCallerContext;
231
249
  this.ensureVoiceMcpSocketForAgent = voiceBridge?.ensureVoiceMcpSocketForAgent;
232
250
  this.removeVoiceMcpSocketForAgent = voiceBridge?.removeVoiceMcpSocketForAgent;
251
+ this.getSpeechReadiness = dictation?.getSpeechReadiness;
233
252
  this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
234
253
  this.abortController = new AbortController();
235
254
  this.sessionLogger = logger.child({
@@ -415,9 +434,7 @@ export class Session {
415
434
  }
416
435
  // Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
417
436
  // for the focused agent, with a short grace window while backgrounded.
418
- //
419
- // History catch-up is handled via explicit `initialize_agent_request` which emits a
420
- // batched `agent_stream_snapshot`.
437
+ // History catch-up is handled via pull-based `fetch_agent_timeline_request`.
421
438
  const activity = this.clientActivity;
422
439
  if (activity?.deviceType === "mobile") {
423
440
  if (!activity.focusedAgentId) {
@@ -441,6 +458,8 @@ export class Session {
441
458
  agentId: event.agentId,
442
459
  event: serializedEvent,
443
460
  timestamp: new Date().toISOString(),
461
+ ...(typeof event.seq === "number" ? { seq: event.seq } : {}),
462
+ ...(typeof event.epoch === "string" ? { epoch: event.epoch } : {}),
444
463
  };
445
464
  this.emit({
446
465
  type: "agent_stream",
@@ -699,6 +718,22 @@ export class Session {
699
718
  await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
700
719
  break;
701
720
  case "dictation_stream_start":
721
+ {
722
+ const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation");
723
+ if (unavailable) {
724
+ this.emit({
725
+ type: "dictation_stream_error",
726
+ payload: {
727
+ dictationId: msg.dictationId,
728
+ error: unavailable.message,
729
+ retryable: unavailable.retryable,
730
+ reasonCode: unavailable.reasonCode,
731
+ missingModelIds: unavailable.missingModelIds,
732
+ },
733
+ });
734
+ break;
735
+ }
736
+ }
702
737
  await this.dictationStreamManager.handleStart(msg.dictationId, msg.format);
703
738
  break;
704
739
  case "dictation_stream_chunk":
@@ -730,8 +765,8 @@ export class Session {
730
765
  case "restart_server_request":
731
766
  await this.handleRestartServerRequest(msg.requestId, msg.reason);
732
767
  break;
733
- case "initialize_agent_request":
734
- await this.handleInitializeAgentRequest(msg.agentId, msg.requestId);
768
+ case "fetch_agent_timeline_request":
769
+ await this.handleFetchAgentTimelineRequest(msg);
735
770
  break;
736
771
  case "set_agent_mode_request":
737
772
  await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId);
@@ -751,6 +786,9 @@ export class Session {
751
786
  case "validate_branch_request":
752
787
  await this.handleValidateBranchRequest(msg);
753
788
  break;
789
+ case "branch_suggestions_request":
790
+ await this.handleBranchSuggestionsRequest(msg);
791
+ break;
754
792
  case "subscribe_checkout_diff_request":
755
793
  await this.handleSubscribeCheckoutDiffRequest(msg);
756
794
  break;
@@ -848,6 +886,12 @@ export class Session {
848
886
  case "kill_terminal_request":
849
887
  await this.handleKillTerminalRequest(msg);
850
888
  break;
889
+ case "attach_terminal_stream_request":
890
+ await this.handleAttachTerminalStreamRequest(msg);
891
+ break;
892
+ case "detach_terminal_stream_request":
893
+ this.handleDetachTerminalStreamRequest(msg);
894
+ break;
851
895
  }
852
896
  }
853
897
  catch (error) {
@@ -881,6 +925,58 @@ export class Session {
881
925
  });
882
926
  }
883
927
  }
928
+ handleBinaryFrame(frame) {
929
+ switch (frame.channel) {
930
+ case BinaryMuxChannel.Terminal:
931
+ this.handleTerminalBinaryFrame(frame);
932
+ break;
933
+ default:
934
+ this.sessionLogger.warn({ channel: frame.channel, messageType: frame.messageType }, "Unhandled binary mux channel");
935
+ break;
936
+ }
937
+ }
938
+ handleTerminalBinaryFrame(frame) {
939
+ if (frame.messageType === TerminalBinaryMessageType.InputUtf8) {
940
+ const binding = this.terminalStreams.get(frame.streamId);
941
+ if (!binding) {
942
+ this.sessionLogger.warn({ streamId: frame.streamId }, "Terminal stream not found for input");
943
+ return;
944
+ }
945
+ if (!this.terminalManager) {
946
+ return;
947
+ }
948
+ const session = this.terminalManager.getTerminal(binding.terminalId);
949
+ if (!session) {
950
+ this.detachTerminalStream(frame.streamId, { emitExit: true });
951
+ return;
952
+ }
953
+ const payload = frame.payload ?? new Uint8Array(0);
954
+ if (payload.byteLength === 0) {
955
+ return;
956
+ }
957
+ const text = Buffer.from(payload).toString("utf8");
958
+ if (!text) {
959
+ return;
960
+ }
961
+ session.send({ type: "input", data: text });
962
+ return;
963
+ }
964
+ if (frame.messageType === TerminalBinaryMessageType.Ack) {
965
+ const binding = this.terminalStreams.get(frame.streamId);
966
+ if (binding) {
967
+ if (!Number.isFinite(frame.offset) || frame.offset < 0) {
968
+ return;
969
+ }
970
+ const nextAckOffset = Math.max(binding.lastAckOffset, Math.min(Math.floor(frame.offset), binding.lastOutputOffset));
971
+ if (nextAckOffset > binding.lastAckOffset) {
972
+ binding.lastAckOffset = nextAckOffset;
973
+ this.flushPendingTerminalStreamChunks(frame.streamId, binding);
974
+ }
975
+ }
976
+ return;
977
+ }
978
+ this.sessionLogger.warn({ streamId: frame.streamId, messageType: frame.messageType }, "Unhandled terminal binary frame");
979
+ }
884
980
  async handleRestartServerRequest(requestId, reason) {
885
981
  if (restartRequested) {
886
982
  this.sessionLogger.debug("Restart already requested, ignoring duplicate");
@@ -1032,12 +1128,57 @@ export class Session {
1032
1128
  });
1033
1129
  }
1034
1130
  }
1131
+ toVoiceFeatureUnavailableContext(state) {
1132
+ return {
1133
+ reasonCode: state.reasonCode,
1134
+ message: state.message,
1135
+ retryable: state.retryable,
1136
+ missingModelIds: [...state.missingModelIds],
1137
+ };
1138
+ }
1139
+ resolveModeReadinessState(readiness, mode) {
1140
+ if (mode === "voice_mode") {
1141
+ return readiness.realtimeVoice;
1142
+ }
1143
+ return readiness.dictation;
1144
+ }
1145
+ getVoiceFeatureUnavailableResponseMetadata(error) {
1146
+ if (!(error instanceof VoiceFeatureUnavailableError)) {
1147
+ return {};
1148
+ }
1149
+ return {
1150
+ reasonCode: error.reasonCode,
1151
+ retryable: error.retryable,
1152
+ missingModelIds: error.missingModelIds,
1153
+ };
1154
+ }
1155
+ resolveVoiceFeatureUnavailableContext(mode) {
1156
+ const readiness = this.getSpeechReadiness?.();
1157
+ if (!readiness) {
1158
+ return null;
1159
+ }
1160
+ const modeReadiness = this.resolveModeReadinessState(readiness, mode);
1161
+ if (!modeReadiness.enabled) {
1162
+ return this.toVoiceFeatureUnavailableContext(modeReadiness);
1163
+ }
1164
+ if (!readiness.voiceFeature.available) {
1165
+ return this.toVoiceFeatureUnavailableContext(readiness.voiceFeature);
1166
+ }
1167
+ if (!modeReadiness.available) {
1168
+ return this.toVoiceFeatureUnavailableContext(modeReadiness);
1169
+ }
1170
+ return null;
1171
+ }
1035
1172
  /**
1036
1173
  * Handle voice mode toggle
1037
1174
  */
1038
1175
  async handleSetVoiceMode(enabled, agentId, requestId) {
1039
1176
  try {
1040
1177
  if (enabled) {
1178
+ const unavailable = this.resolveVoiceFeatureUnavailableContext("voice_mode");
1179
+ if (unavailable) {
1180
+ throw new VoiceFeatureUnavailableError(unavailable);
1181
+ }
1041
1182
  const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? "", "set_voice_mode");
1042
1183
  if (this.isVoiceMode &&
1043
1184
  this.voiceModeAgentId &&
@@ -1084,6 +1225,7 @@ export class Session {
1084
1225
  }
1085
1226
  catch (error) {
1086
1227
  const errorMessage = error instanceof Error ? error.message : "Failed to set voice mode";
1228
+ const unavailable = this.getVoiceFeatureUnavailableResponseMetadata(error);
1087
1229
  this.sessionLogger.error({
1088
1230
  err: error,
1089
1231
  enabled,
@@ -1098,6 +1240,7 @@ export class Session {
1098
1240
  agentId: this.voiceModeAgentId,
1099
1241
  accepted: false,
1100
1242
  error: errorMessage,
1243
+ ...unavailable,
1101
1244
  },
1102
1245
  });
1103
1246
  return;
@@ -1410,37 +1553,6 @@ export class Session {
1410
1553
  }
1411
1554
  this.startAgentStream(agentId, prompt, runOptions);
1412
1555
  }
1413
- /**
1414
- * Handle on-demand agent initialization request from client
1415
- */
1416
- async handleInitializeAgentRequest(agentId, requestId) {
1417
- try {
1418
- const snapshot = await this.ensureAgentLoaded(agentId);
1419
- await this.forwardAgentUpdate(snapshot);
1420
- // Send timeline snapshot after hydration (if any)
1421
- const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
1422
- this.emit({
1423
- type: "initialize_agent_request",
1424
- payload: {
1425
- agentId,
1426
- agentStatus: snapshot.lifecycle,
1427
- timelineSize,
1428
- requestId,
1429
- },
1430
- });
1431
- }
1432
- catch (error) {
1433
- this.sessionLogger.error({ err: error, agentId }, `Failed to initialize agent ${agentId}`);
1434
- this.emit({
1435
- type: "initialize_agent_request",
1436
- payload: {
1437
- agentId,
1438
- requestId,
1439
- error: error?.message ?? "Failed to initialize agent",
1440
- },
1441
- });
1442
- }
1443
- }
1444
1556
  /**
1445
1557
  * Handle create agent request
1446
1558
  */
@@ -1541,7 +1653,7 @@ export class Session {
1541
1653
  const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
1542
1654
  await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
1543
1655
  await this.forwardAgentUpdate(snapshot);
1544
- const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
1656
+ const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
1545
1657
  if (requestId) {
1546
1658
  const agentPayload = await this.getAgentPayloadById(snapshot.id);
1547
1659
  if (!agentPayload) {
@@ -1600,7 +1712,7 @@ export class Session {
1600
1712
  }
1601
1713
  await this.agentManager.hydrateTimelineFromProvider(agentId);
1602
1714
  await this.forwardAgentUpdate(snapshot);
1603
- const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
1715
+ const timelineSize = this.agentManager.getTimeline(agentId).length;
1604
1716
  if (requestId) {
1605
1717
  this.emit({
1606
1718
  type: "status",
@@ -2627,6 +2739,31 @@ export class Session {
2627
2739
  });
2628
2740
  }
2629
2741
  }
2742
+ async handleBranchSuggestionsRequest(msg) {
2743
+ const { cwd, query, limit, requestId } = msg;
2744
+ try {
2745
+ const resolvedCwd = expandTilde(cwd);
2746
+ const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
2747
+ this.emit({
2748
+ type: "branch_suggestions_response",
2749
+ payload: {
2750
+ branches,
2751
+ error: null,
2752
+ requestId,
2753
+ },
2754
+ });
2755
+ }
2756
+ catch (error) {
2757
+ this.emit({
2758
+ type: "branch_suggestions_response",
2759
+ payload: {
2760
+ branches: [],
2761
+ error: error instanceof Error ? error.message : String(error),
2762
+ requestId,
2763
+ },
2764
+ });
2765
+ }
2766
+ }
2630
2767
  normalizeCheckoutDiffCompare(compare) {
2631
2768
  if (compare.mode === "uncommitted") {
2632
2769
  return { mode: "uncommitted" };
@@ -2798,30 +2935,42 @@ export class Session {
2798
2935
  latestPayload: null,
2799
2936
  latestFingerprint: null,
2800
2937
  };
2801
- const watchPaths = new Set([cwd]);
2802
- if (watchRoot) {
2803
- watchPaths.add(watchRoot);
2804
- }
2938
+ const repoWatchPath = watchRoot ?? cwd;
2939
+ const watchPaths = new Set([repoWatchPath]);
2805
2940
  const gitDir = await this.resolveCheckoutGitDir(cwd);
2806
2941
  if (gitDir) {
2807
2942
  watchPaths.add(gitDir);
2808
2943
  }
2809
- let hasWatchRootCoverage = false;
2944
+ let hasRecursiveRepoCoverage = false;
2945
+ const allowRecursiveRepoWatch = process.platform !== "linux";
2810
2946
  for (const watchPath of watchPaths) {
2947
+ const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
2811
2948
  const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
2812
2949
  this.scheduleCheckoutDiffTargetRefresh(target);
2813
2950
  });
2814
2951
  let watcher = null;
2952
+ let watcherIsRecursive = false;
2815
2953
  try {
2816
- watcher = createWatcher(true);
2954
+ if (shouldTryRecursive) {
2955
+ watcher = createWatcher(true);
2956
+ watcherIsRecursive = true;
2957
+ }
2958
+ else {
2959
+ watcher = createWatcher(false);
2960
+ }
2817
2961
  }
2818
2962
  catch (error) {
2819
- try {
2820
- watcher = createWatcher(false);
2821
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff recursive watch unavailable; using non-recursive fallback");
2963
+ if (shouldTryRecursive) {
2964
+ try {
2965
+ watcher = createWatcher(false);
2966
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff recursive watch unavailable; using non-recursive fallback");
2967
+ }
2968
+ catch (fallbackError) {
2969
+ this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
2970
+ }
2822
2971
  }
2823
- catch (fallbackError) {
2824
- this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
2972
+ else {
2973
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
2825
2974
  }
2826
2975
  }
2827
2976
  if (!watcher) {
@@ -2831,11 +2980,11 @@ export class Session {
2831
2980
  this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff watcher error");
2832
2981
  });
2833
2982
  target.watchers.push(watcher);
2834
- if (watchRoot && watchPath === watchRoot) {
2835
- hasWatchRootCoverage = true;
2983
+ if (watchPath === repoWatchPath && watcherIsRecursive) {
2984
+ hasRecursiveRepoCoverage = true;
2836
2985
  }
2837
2986
  }
2838
- const missingRepoCoverage = Boolean(watchRoot) && !hasWatchRootCoverage;
2987
+ const missingRepoCoverage = !hasRecursiveRepoCoverage;
2839
2988
  if (target.watchers.length === 0 || missingRepoCoverage) {
2840
2989
  target.fallbackRefreshInterval = setInterval(() => {
2841
2990
  this.scheduleCheckoutDiffTargetRefresh(target);
@@ -2846,7 +2995,7 @@ export class Session {
2846
2995
  intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
2847
2996
  reason: target.watchers.length === 0
2848
2997
  ? "no_watchers"
2849
- : "missing_repo_root_coverage",
2998
+ : "missing_recursive_repo_root_coverage",
2850
2999
  }, "Checkout diff watchers unavailable; using timed refresh fallback");
2851
3000
  }
2852
3001
  this.checkoutDiffTargets.set(targetKey, target);
@@ -3606,6 +3755,77 @@ export class Session {
3606
3755
  payload: { requestId, agent, error: null },
3607
3756
  });
3608
3757
  }
3758
+ async handleFetchAgentTimelineRequest(msg) {
3759
+ const direction = msg.direction ?? (msg.cursor ? "after" : "tail");
3760
+ const projection = msg.projection ?? "projected";
3761
+ const limit = msg.limit ?? (direction === "after" ? 0 : undefined);
3762
+ const cursor = msg.cursor
3763
+ ? {
3764
+ epoch: msg.cursor.epoch,
3765
+ seq: msg.cursor.seq,
3766
+ }
3767
+ : undefined;
3768
+ try {
3769
+ const snapshot = await this.ensureAgentLoaded(msg.agentId);
3770
+ const timeline = this.agentManager.fetchTimeline(msg.agentId, {
3771
+ direction,
3772
+ cursor,
3773
+ limit,
3774
+ });
3775
+ const projected = projectTimelineRows(timeline.rows, snapshot.provider, projection);
3776
+ const firstRow = timeline.rows[0];
3777
+ const lastRow = timeline.rows[timeline.rows.length - 1];
3778
+ const startCursor = firstRow
3779
+ ? { epoch: timeline.epoch, seq: firstRow.seq }
3780
+ : null;
3781
+ const endCursor = lastRow
3782
+ ? { epoch: timeline.epoch, seq: lastRow.seq }
3783
+ : null;
3784
+ this.emit({
3785
+ type: "fetch_agent_timeline_response",
3786
+ payload: {
3787
+ requestId: msg.requestId,
3788
+ agentId: msg.agentId,
3789
+ direction,
3790
+ projection,
3791
+ epoch: timeline.epoch,
3792
+ reset: timeline.reset,
3793
+ staleCursor: timeline.staleCursor,
3794
+ gap: timeline.gap,
3795
+ window: timeline.window,
3796
+ startCursor,
3797
+ endCursor,
3798
+ hasOlder: timeline.hasOlder,
3799
+ hasNewer: timeline.hasNewer,
3800
+ entries: projected,
3801
+ error: null,
3802
+ },
3803
+ });
3804
+ }
3805
+ catch (error) {
3806
+ this.sessionLogger.error({ err: error, agentId: msg.agentId }, "Failed to handle fetch_agent_timeline_request");
3807
+ this.emit({
3808
+ type: "fetch_agent_timeline_response",
3809
+ payload: {
3810
+ requestId: msg.requestId,
3811
+ agentId: msg.agentId,
3812
+ direction,
3813
+ projection,
3814
+ epoch: "",
3815
+ reset: false,
3816
+ staleCursor: false,
3817
+ gap: false,
3818
+ window: { minSeq: 0, maxSeq: 0, nextSeq: 0 },
3819
+ startCursor: null,
3820
+ endCursor: null,
3821
+ hasOlder: false,
3822
+ hasNewer: false,
3823
+ entries: [],
3824
+ error: error instanceof Error ? error.message : String(error),
3825
+ },
3826
+ });
3827
+ }
3828
+ }
3609
3829
  async handleSendAgentMessageRequest(msg) {
3610
3830
  const resolved = await this.resolveAgentIdentifier(msg.agentId);
3611
3831
  if (!resolved.ok) {
@@ -3782,30 +4002,6 @@ export class Session {
3782
4002
  clearTimeout(timeoutHandle);
3783
4003
  }
3784
4004
  }
3785
- emitAgentTimelineSnapshot(agent) {
3786
- const timeline = this.agentManager.getTimeline(agent.id);
3787
- const events = timeline.flatMap((item) => {
3788
- const serializedEvent = serializeAgentStreamEvent({
3789
- type: "timeline",
3790
- provider: agent.provider,
3791
- item,
3792
- });
3793
- if (!serializedEvent) {
3794
- return [];
3795
- }
3796
- return [
3797
- {
3798
- event: serializedEvent,
3799
- timestamp: new Date().toISOString(),
3800
- },
3801
- ];
3802
- });
3803
- this.emit({
3804
- type: "agent_stream_snapshot",
3805
- payload: { agentId: agent.id, events },
3806
- });
3807
- return timeline.length;
3808
- }
3809
4005
  /**
3810
4006
  * Handle audio chunk for buffering and transcription
3811
4007
  */
@@ -4257,6 +4453,17 @@ export class Session {
4257
4453
  }
4258
4454
  this.onMessage(msg);
4259
4455
  }
4456
+ emitBinary(frame) {
4457
+ if (!this.onBinaryMessage) {
4458
+ return;
4459
+ }
4460
+ try {
4461
+ this.onBinaryMessage(frame);
4462
+ }
4463
+ catch (error) {
4464
+ this.sessionLogger.error({ err: error }, "Failed to emit binary frame");
4465
+ }
4466
+ }
4260
4467
  /**
4261
4468
  * Clean up session resources
4262
4469
  */
@@ -4299,6 +4506,7 @@ export class Session {
4299
4506
  unsubscribe();
4300
4507
  }
4301
4508
  this.terminalSubscriptions.clear();
4509
+ this.detachAllTerminalStreams({ emitExit: false });
4302
4510
  for (const target of this.checkoutDiffTargets.values()) {
4303
4511
  this.closeCheckoutDiffWatchTarget(target);
4304
4512
  }
@@ -4472,6 +4680,10 @@ export class Session {
4472
4680
  unsubscribe();
4473
4681
  this.terminalSubscriptions.delete(msg.terminalId);
4474
4682
  }
4683
+ const streamId = this.terminalStreamByTerminalId.get(msg.terminalId);
4684
+ if (typeof streamId === "number") {
4685
+ this.detachTerminalStream(streamId, { emitExit: true });
4686
+ }
4475
4687
  this.terminalManager.killTerminal(msg.terminalId);
4476
4688
  this.emit({
4477
4689
  type: "kill_terminal_response",
@@ -4482,5 +4694,213 @@ export class Session {
4482
4694
  },
4483
4695
  });
4484
4696
  }
4697
+ async handleAttachTerminalStreamRequest(msg) {
4698
+ if (!this.terminalManager || !this.onBinaryMessage) {
4699
+ this.emit({
4700
+ type: "attach_terminal_stream_response",
4701
+ payload: {
4702
+ terminalId: msg.terminalId,
4703
+ streamId: null,
4704
+ replayedFrom: 0,
4705
+ currentOffset: 0,
4706
+ earliestAvailableOffset: 0,
4707
+ reset: true,
4708
+ error: "Terminal streaming not available",
4709
+ requestId: msg.requestId,
4710
+ },
4711
+ });
4712
+ return;
4713
+ }
4714
+ const session = this.terminalManager.getTerminal(msg.terminalId);
4715
+ if (!session) {
4716
+ this.emit({
4717
+ type: "attach_terminal_stream_response",
4718
+ payload: {
4719
+ terminalId: msg.terminalId,
4720
+ streamId: null,
4721
+ replayedFrom: 0,
4722
+ currentOffset: 0,
4723
+ earliestAvailableOffset: 0,
4724
+ reset: true,
4725
+ error: "Terminal not found",
4726
+ requestId: msg.requestId,
4727
+ },
4728
+ });
4729
+ return;
4730
+ }
4731
+ if (msg.rows || msg.cols) {
4732
+ const state = session.getState();
4733
+ session.send({
4734
+ type: "resize",
4735
+ rows: msg.rows ?? state.rows,
4736
+ cols: msg.cols ?? state.cols,
4737
+ });
4738
+ }
4739
+ const existingStreamId = this.terminalStreamByTerminalId.get(msg.terminalId);
4740
+ if (typeof existingStreamId === "number") {
4741
+ this.detachTerminalStream(existingStreamId, { emitExit: false });
4742
+ }
4743
+ const streamId = this.allocateTerminalStreamId();
4744
+ const initialOffset = Math.max(0, Math.floor(msg.resumeOffset ?? 0));
4745
+ const binding = {
4746
+ terminalId: msg.terminalId,
4747
+ unsubscribe: () => { },
4748
+ lastOutputOffset: initialOffset,
4749
+ lastAckOffset: initialOffset,
4750
+ pendingChunks: [],
4751
+ pendingBytes: 0,
4752
+ };
4753
+ this.terminalStreams.set(streamId, binding);
4754
+ this.terminalStreamByTerminalId.set(msg.terminalId, streamId);
4755
+ let rawSub;
4756
+ try {
4757
+ rawSub = session.subscribeRaw((chunk) => {
4758
+ const currentBinding = this.terminalStreams.get(streamId);
4759
+ if (!currentBinding) {
4760
+ return;
4761
+ }
4762
+ this.enqueueOrEmitTerminalStreamChunk(streamId, currentBinding, {
4763
+ data: chunk.data,
4764
+ startOffset: chunk.startOffset,
4765
+ endOffset: chunk.endOffset,
4766
+ replay: chunk.replay,
4767
+ });
4768
+ }, { fromOffset: msg.resumeOffset ?? 0 });
4769
+ }
4770
+ catch (error) {
4771
+ this.terminalStreams.delete(streamId);
4772
+ this.terminalStreamByTerminalId.delete(msg.terminalId);
4773
+ throw error;
4774
+ }
4775
+ binding.unsubscribe = rawSub.unsubscribe;
4776
+ binding.lastAckOffset = rawSub.replayedFrom;
4777
+ if (binding.lastOutputOffset < rawSub.replayedFrom) {
4778
+ binding.lastOutputOffset = rawSub.replayedFrom;
4779
+ }
4780
+ this.flushPendingTerminalStreamChunks(streamId, binding);
4781
+ this.emit({
4782
+ type: "attach_terminal_stream_response",
4783
+ payload: {
4784
+ terminalId: msg.terminalId,
4785
+ streamId,
4786
+ replayedFrom: rawSub.replayedFrom,
4787
+ currentOffset: rawSub.currentOffset,
4788
+ earliestAvailableOffset: rawSub.earliestAvailableOffset,
4789
+ reset: rawSub.reset,
4790
+ error: null,
4791
+ requestId: msg.requestId,
4792
+ },
4793
+ });
4794
+ }
4795
+ getTerminalStreamChunkByteLength(chunk) {
4796
+ return Math.max(0, chunk.endOffset - chunk.startOffset);
4797
+ }
4798
+ canEmitTerminalStreamChunk(binding, chunk) {
4799
+ return chunk.startOffset < binding.lastAckOffset + TERMINAL_STREAM_WINDOW_BYTES;
4800
+ }
4801
+ emitTerminalStreamChunk(streamId, binding, chunk) {
4802
+ const payload = new Uint8Array(Buffer.from(chunk.data, "utf8"));
4803
+ this.emitBinary({
4804
+ channel: BinaryMuxChannel.Terminal,
4805
+ messageType: TerminalBinaryMessageType.OutputUtf8,
4806
+ streamId,
4807
+ offset: chunk.startOffset,
4808
+ flags: chunk.replay ? TerminalBinaryFlags.Replay : 0,
4809
+ payload,
4810
+ });
4811
+ binding.lastOutputOffset = chunk.endOffset;
4812
+ }
4813
+ enqueueOrEmitTerminalStreamChunk(streamId, binding, chunk) {
4814
+ const chunkBytes = this.getTerminalStreamChunkByteLength(chunk);
4815
+ if (binding.pendingChunks.length > 0 || !this.canEmitTerminalStreamChunk(binding, chunk)) {
4816
+ if (binding.pendingChunks.length >= TERMINAL_STREAM_MAX_PENDING_CHUNKS ||
4817
+ binding.pendingBytes + chunkBytes > TERMINAL_STREAM_MAX_PENDING_BYTES) {
4818
+ this.sessionLogger.warn({
4819
+ streamId,
4820
+ pendingChunks: binding.pendingChunks.length,
4821
+ pendingBytes: binding.pendingBytes,
4822
+ chunkBytes,
4823
+ }, "Terminal stream pending buffer overflow; closing stream");
4824
+ this.detachTerminalStream(streamId, { emitExit: true });
4825
+ return;
4826
+ }
4827
+ binding.pendingChunks.push(chunk);
4828
+ binding.pendingBytes += chunkBytes;
4829
+ return;
4830
+ }
4831
+ this.emitTerminalStreamChunk(streamId, binding, chunk);
4832
+ }
4833
+ flushPendingTerminalStreamChunks(streamId, binding) {
4834
+ while (binding.pendingChunks.length > 0) {
4835
+ const next = binding.pendingChunks[0];
4836
+ if (!next || !this.canEmitTerminalStreamChunk(binding, next)) {
4837
+ break;
4838
+ }
4839
+ binding.pendingChunks.shift();
4840
+ binding.pendingBytes -= this.getTerminalStreamChunkByteLength(next);
4841
+ if (binding.pendingBytes < 0) {
4842
+ binding.pendingBytes = 0;
4843
+ }
4844
+ this.emitTerminalStreamChunk(streamId, binding, next);
4845
+ }
4846
+ }
4847
+ handleDetachTerminalStreamRequest(msg) {
4848
+ const success = this.detachTerminalStream(msg.streamId, { emitExit: false });
4849
+ this.emit({
4850
+ type: "detach_terminal_stream_response",
4851
+ payload: {
4852
+ streamId: msg.streamId,
4853
+ success,
4854
+ requestId: msg.requestId,
4855
+ },
4856
+ });
4857
+ }
4858
+ detachAllTerminalStreams(options) {
4859
+ for (const streamId of Array.from(this.terminalStreams.keys())) {
4860
+ this.detachTerminalStream(streamId, options);
4861
+ }
4862
+ }
4863
+ detachTerminalStream(streamId, options) {
4864
+ const binding = this.terminalStreams.get(streamId);
4865
+ if (!binding) {
4866
+ return false;
4867
+ }
4868
+ try {
4869
+ binding.unsubscribe();
4870
+ }
4871
+ catch (error) {
4872
+ this.sessionLogger.warn({ err: error, streamId }, "Failed to unsubscribe terminal stream");
4873
+ }
4874
+ this.terminalStreams.delete(streamId);
4875
+ if (this.terminalStreamByTerminalId.get(binding.terminalId) === streamId) {
4876
+ this.terminalStreamByTerminalId.delete(binding.terminalId);
4877
+ }
4878
+ if (options?.emitExit) {
4879
+ this.emit({
4880
+ type: "terminal_stream_exit",
4881
+ payload: {
4882
+ streamId,
4883
+ terminalId: binding.terminalId,
4884
+ },
4885
+ });
4886
+ }
4887
+ return true;
4888
+ }
4889
+ allocateTerminalStreamId() {
4890
+ let attempts = 0;
4891
+ while (attempts < 0xffffffff) {
4892
+ const candidate = this.nextTerminalStreamId >>> 0;
4893
+ this.nextTerminalStreamId = ((this.nextTerminalStreamId + 1) & 0xffffffff) >>> 0;
4894
+ if (candidate === 0) {
4895
+ attempts += 1;
4896
+ continue;
4897
+ }
4898
+ if (!this.terminalStreams.has(candidate)) {
4899
+ return candidate;
4900
+ }
4901
+ attempts += 1;
4902
+ }
4903
+ throw new Error("Unable to allocate terminal stream id");
4904
+ }
4485
4905
  }
4486
4906
  //# sourceMappingURL=session.js.map