@getpaseo/server 0.1.3 → 0.1.6

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 (148) 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 +134 -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 +63 -46
  26. package/dist/server/client/daemon-client.d.ts.map +1 -1
  27. package/dist/server/client/daemon-client.js +497 -796
  28. package/dist/server/client/daemon-client.js.map +1 -1
  29. package/dist/server/server/agent/agent-management-mcp.d.ts +2 -0
  30. package/dist/server/server/agent/agent-management-mcp.d.ts.map +1 -1
  31. package/dist/server/server/agent/agent-management-mcp.js +29 -4
  32. package/dist/server/server/agent/agent-management-mcp.js.map +1 -1
  33. package/dist/server/server/agent/agent-manager.d.ts +48 -0
  34. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  35. package/dist/server/server/agent/agent-manager.js +224 -14
  36. package/dist/server/server/agent/agent-manager.js.map +1 -1
  37. package/dist/server/server/agent/agent-sdk-types.d.ts +14 -0
  38. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  39. package/dist/server/server/agent/agent-storage.d.ts +4 -4
  40. package/dist/server/server/agent/mcp-server.d.ts +2 -0
  41. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  42. package/dist/server/server/agent/mcp-server.js +30 -5
  43. package/dist/server/server/agent/mcp-server.js.map +1 -1
  44. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
  45. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +120 -6
  46. package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
  47. package/dist/server/server/agent/providers/claude-agent.d.ts +7 -0
  48. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  49. package/dist/server/server/agent/providers/claude-agent.js +83 -9
  50. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  51. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  52. package/dist/server/server/agent/providers/codex-app-server-agent.js +25 -0
  53. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  54. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +42 -0
  55. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  56. package/dist/server/server/agent/stt-manager.d.ts +3 -2
  57. package/dist/server/server/agent/stt-manager.d.ts.map +1 -1
  58. package/dist/server/server/agent/stt-manager.js +5 -3
  59. package/dist/server/server/agent/stt-manager.js.map +1 -1
  60. package/dist/server/server/agent/timeline-append.d.ts +10 -0
  61. package/dist/server/server/agent/timeline-append.d.ts.map +1 -0
  62. package/dist/server/server/agent/timeline-append.js +27 -0
  63. package/dist/server/server/agent/timeline-append.js.map +1 -0
  64. package/dist/server/server/agent/timeline-projection.d.ts +19 -0
  65. package/dist/server/server/agent/timeline-projection.d.ts.map +1 -0
  66. package/dist/server/server/agent/timeline-projection.js +142 -0
  67. package/dist/server/server/agent/timeline-projection.js.map +1 -0
  68. package/dist/server/server/agent/tts-manager.d.ts +3 -2
  69. package/dist/server/server/agent/tts-manager.d.ts.map +1 -1
  70. package/dist/server/server/agent/tts-manager.js +5 -3
  71. package/dist/server/server/agent/tts-manager.js.map +1 -1
  72. package/dist/server/server/agent-attention-policy.d.ts +20 -0
  73. package/dist/server/server/agent-attention-policy.d.ts.map +1 -0
  74. package/dist/server/server/agent-attention-policy.js +40 -0
  75. package/dist/server/server/agent-attention-policy.js.map +1 -0
  76. package/dist/server/server/bootstrap.d.ts.map +1 -1
  77. package/dist/server/server/bootstrap.js +16 -18
  78. package/dist/server/server/bootstrap.js.map +1 -1
  79. package/dist/server/server/dictation/dictation-stream-manager.d.ts +10 -2
  80. package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -1
  81. package/dist/server/server/dictation/dictation-stream-manager.js +81 -14
  82. package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
  83. package/dist/server/server/exports.d.ts +1 -1
  84. package/dist/server/server/exports.d.ts.map +1 -1
  85. package/dist/server/server/persisted-config.d.ts +12 -12
  86. package/dist/server/server/relay-transport.d.ts +3 -2
  87. package/dist/server/server/relay-transport.d.ts.map +1 -1
  88. package/dist/server/server/relay-transport.js +21 -5
  89. package/dist/server/server/relay-transport.js.map +1 -1
  90. package/dist/server/server/session.d.ts +51 -14
  91. package/dist/server/server/session.d.ts.map +1 -1
  92. package/dist/server/server/session.js +872 -250
  93. package/dist/server/server/session.js.map +1 -1
  94. package/dist/server/server/speech/provider-resolver.d.ts +3 -0
  95. package/dist/server/server/speech/provider-resolver.d.ts.map +1 -0
  96. package/dist/server/server/speech/provider-resolver.js +7 -0
  97. package/dist/server/server/speech/provider-resolver.js.map +1 -0
  98. package/dist/server/server/speech/providers/local/runtime.d.ts +1 -0
  99. package/dist/server/server/speech/providers/local/runtime.d.ts.map +1 -1
  100. package/dist/server/server/speech/providers/local/runtime.js +4 -3
  101. package/dist/server/server/speech/providers/local/runtime.js.map +1 -1
  102. package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts.map +1 -1
  103. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +3 -66
  104. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js.map +1 -1
  105. package/dist/server/server/speech/speech-runtime.d.ts +26 -3
  106. package/dist/server/server/speech/speech-runtime.d.ts.map +1 -1
  107. package/dist/server/server/speech/speech-runtime.js +466 -112
  108. package/dist/server/server/speech/speech-runtime.js.map +1 -1
  109. package/dist/server/server/websocket-server.d.ts +23 -7
  110. package/dist/server/server/websocket-server.d.ts.map +1 -1
  111. package/dist/server/server/websocket-server.js +288 -64
  112. package/dist/server/server/websocket-server.js.map +1 -1
  113. package/dist/server/server/worktree-bootstrap.d.ts +29 -0
  114. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -0
  115. package/dist/server/server/worktree-bootstrap.js +407 -0
  116. package/dist/server/server/worktree-bootstrap.js.map +1 -0
  117. package/dist/server/shared/binary-mux.d.ts +31 -0
  118. package/dist/server/shared/binary-mux.d.ts.map +1 -0
  119. package/dist/server/shared/binary-mux.js +114 -0
  120. package/dist/server/shared/binary-mux.js.map +1 -0
  121. package/dist/server/shared/messages.d.ts +6437 -5839
  122. package/dist/server/shared/messages.d.ts.map +1 -1
  123. package/dist/server/shared/messages.js +238 -50
  124. package/dist/server/shared/messages.js.map +1 -1
  125. package/dist/server/shared/terminal-key-input.d.ts +9 -0
  126. package/dist/server/shared/terminal-key-input.d.ts.map +1 -0
  127. package/dist/server/shared/terminal-key-input.js +132 -0
  128. package/dist/server/shared/terminal-key-input.js.map +1 -0
  129. package/dist/server/shared/tool-call-display.d.ts.map +1 -1
  130. package/dist/server/shared/tool-call-display.js +4 -0
  131. package/dist/server/shared/tool-call-display.js.map +1 -1
  132. package/dist/server/terminal/terminal-manager.d.ts +11 -0
  133. package/dist/server/terminal/terminal-manager.d.ts.map +1 -1
  134. package/dist/server/terminal/terminal-manager.js +75 -24
  135. package/dist/server/terminal/terminal-manager.js.map +1 -1
  136. package/dist/server/terminal/terminal.d.ts +18 -0
  137. package/dist/server/terminal/terminal.d.ts.map +1 -1
  138. package/dist/server/terminal/terminal.js +142 -5
  139. package/dist/server/terminal/terminal.js.map +1 -1
  140. package/dist/server/utils/checkout-git.d.ts +4 -0
  141. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  142. package/dist/server/utils/checkout-git.js +92 -0
  143. package/dist/server/utils/checkout-git.js.map +1 -1
  144. package/dist/server/utils/worktree.d.ts +32 -0
  145. package/dist/server/utils/worktree.d.ts.map +1 -1
  146. package/dist/server/utils/worktree.js +160 -10
  147. package/dist/server/utils/worktree.js.map +1 -1
  148. 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,16 @@ 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 { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
21
+ import { projectTimelineRows } from "./agent/timeline-projection.js";
19
22
  import { StructuredAgentResponseError, generateStructuredAgentResponse, } from "./agent/agent-response-loop.js";
20
23
  import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
21
24
  import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
22
25
  import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
23
26
  import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
24
- 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 { slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
28
+ import { createAgentWorktree, runAsyncWorktreeBootstrap, } from "./worktree-bootstrap.js";
29
+ import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from "../utils/checkout-git.js";
26
30
  import { getProjectIcon } from "../utils/project-icon.js";
27
31
  import { expandTilde } from "../utils/path.js";
28
32
  import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from "./speech/providers/local/models.js";
@@ -35,10 +39,11 @@ const pendingAgentInitializations = new Map();
35
39
  let restartRequested = false;
36
40
  const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
37
41
  const RESTART_EXIT_DELAY_MS = 250;
38
- const PROJECT_PLACEMENT_CACHE_TTL_MS = 10000;
39
- 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.
@@ -85,17 +90,17 @@ function deriveRemoteProjectKey(remoteUrl) {
85
90
  }
86
91
  return `remote:${cleanedHost}/${cleanedPath}`;
87
92
  }
88
- function deriveProjectGroupingKey(cwd, remoteUrl) {
89
- const remoteKey = deriveRemoteProjectKey(remoteUrl);
93
+ function deriveProjectGroupingKey(options) {
94
+ const remoteKey = deriveRemoteProjectKey(options.remoteUrl);
90
95
  if (remoteKey) {
91
96
  return remoteKey;
92
97
  }
93
98
  const worktreeMarker = ".paseo/worktrees/";
94
- const idx = cwd.indexOf(worktreeMarker);
99
+ const idx = options.cwd.indexOf(worktreeMarker);
95
100
  if (idx !== -1) {
96
- return cwd.slice(0, idx).replace(/\/$/, "");
101
+ return options.cwd.slice(0, idx).replace(/\/$/, "");
97
102
  }
98
- return cwd;
103
+ return options.cwd;
99
104
  }
100
105
  function deriveProjectGroupingName(projectKey) {
101
106
  const githubRemotePrefix = "remote:github.com/";
@@ -105,6 +110,13 @@ function deriveProjectGroupingName(projectKey) {
105
110
  const segments = projectKey.split(/[\\/]/).filter(Boolean);
106
111
  return segments[segments.length - 1] || projectKey;
107
112
  }
113
+ class SessionRequestError extends Error {
114
+ constructor(code, message) {
115
+ super(message);
116
+ this.code = code;
117
+ this.name = "SessionRequestError";
118
+ }
119
+ }
108
120
  const PCM_SAMPLE_RATE = 16000;
109
121
  const PCM_CHANNELS = 1;
110
122
  const PCM_BITS_PER_SAMPLE = 16;
@@ -116,6 +128,15 @@ const VOICE_INTERNAL_DICTATION_ID_PREFIX = "__voice_turn__:";
116
128
  const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
117
129
  const AgentIdSchema = z.string().uuid();
118
130
  const VOICE_MCP_SERVER_NAME = "paseo_voice";
131
+ class VoiceFeatureUnavailableError extends Error {
132
+ constructor(context) {
133
+ super(context.message);
134
+ this.name = "VoiceFeatureUnavailableError";
135
+ this.reasonCode = context.reasonCode;
136
+ this.retryable = context.retryable;
137
+ this.missingModelIds = [...context.missingModelIds];
138
+ }
139
+ }
119
140
  function convertPCMToWavBuffer(pcmBuffer, sampleRate, channels, bitsPerSample) {
120
141
  const headerSize = 44;
121
142
  const wavBuffer = Buffer.alloc(headerSize + pcmBuffer.length);
@@ -195,18 +216,24 @@ export class Session {
195
216
  this.agentTools = null;
196
217
  this.unsubscribeAgentEvents = null;
197
218
  this.agentUpdatesSubscription = null;
198
- this.projectPlacementCache = new Map();
199
219
  this.clientActivity = null;
200
220
  this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
221
+ this.subscribedTerminalDirectories = new Set();
222
+ this.unsubscribeTerminalsChanged = null;
201
223
  this.terminalSubscriptions = new Map();
224
+ this.terminalExitSubscriptions = new Map();
225
+ this.terminalStreams = new Map();
226
+ this.terminalStreamByTerminalId = new Map();
227
+ this.nextTerminalStreamId = 1;
202
228
  this.checkoutDiffSubscriptions = new Map();
203
229
  this.checkoutDiffTargets = new Map();
204
230
  this.voiceModeAgentId = null;
205
231
  this.voiceModeBaseConfig = null;
206
- const { clientId, onMessage, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
232
+ const { clientId, onMessage, onBinaryMessage, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
207
233
  this.clientId = clientId;
208
234
  this.sessionId = uuidv4();
209
235
  this.onMessage = onMessage;
236
+ this.onBinaryMessage = onBinaryMessage ?? null;
210
237
  this.downloadTokenStore = downloadTokenStore;
211
238
  this.pushTokenStore = pushTokenStore;
212
239
  this.paseoHome = paseoHome;
@@ -214,6 +241,9 @@ export class Session {
214
241
  this.agentStorage = agentStorage;
215
242
  this.createAgentMcpTransport = createAgentMcpTransport;
216
243
  this.terminalManager = terminalManager;
244
+ if (this.terminalManager) {
245
+ this.unsubscribeTerminalsChanged = this.terminalManager.subscribeTerminalsChanged((event) => this.handleTerminalsChanged(event));
246
+ }
217
247
  this.voiceAgentMcpStdio = voice?.voiceAgentMcpStdio ?? null;
218
248
  const configuredModelsDir = dictation?.localModels?.modelsDir?.trim();
219
249
  this.localSpeechModelsDir =
@@ -230,6 +260,7 @@ export class Session {
230
260
  this.unregisterVoiceCallerContext = voiceBridge?.unregisterVoiceCallerContext;
231
261
  this.ensureVoiceMcpSocketForAgent = voiceBridge?.ensureVoiceMcpSocketForAgent;
232
262
  this.removeVoiceMcpSocketForAgent = voiceBridge?.removeVoiceMcpSocketForAgent;
263
+ this.getSpeechReadiness = dictation?.getSpeechReadiness;
233
264
  this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
234
265
  this.abortController = new AbortController();
235
266
  this.sessionLogger = logger.child({
@@ -415,9 +446,7 @@ export class Session {
415
446
  }
416
447
  // Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
417
448
  // 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`.
449
+ // History catch-up is handled via pull-based `fetch_agent_timeline_request`.
421
450
  const activity = this.clientActivity;
422
451
  if (activity?.deviceType === "mobile") {
423
452
  if (!activity.focusedAgentId) {
@@ -441,6 +470,8 @@ export class Session {
441
470
  agentId: event.agentId,
442
471
  event: serializedEvent,
443
472
  timestamp: new Date().toISOString(),
473
+ ...(typeof event.seq === "number" ? { seq: event.seq } : {}),
474
+ ...(typeof event.epoch === "string" ? { epoch: event.epoch } : {}),
444
475
  };
445
476
  this.emit({
446
477
  type: "agent_stream",
@@ -600,26 +631,16 @@ export class Session {
600
631
  const checkout = await getCheckoutStatusLite(cwd, { paseoHome: this.paseoHome })
601
632
  .then((status) => this.toProjectCheckoutLite(cwd, status))
602
633
  .catch(() => this.buildFallbackProjectCheckout(cwd));
603
- const projectKey = deriveProjectGroupingKey(cwd, checkout.remoteUrl);
634
+ const projectKey = deriveProjectGroupingKey({
635
+ cwd,
636
+ remoteUrl: checkout.remoteUrl,
637
+ });
604
638
  return {
605
639
  projectKey,
606
640
  projectName: deriveProjectGroupingName(projectKey),
607
641
  checkout,
608
642
  };
609
643
  }
610
- getProjectPlacement(cwd) {
611
- const now = Date.now();
612
- const cached = this.projectPlacementCache.get(cwd);
613
- if (cached && cached.expiresAt > now) {
614
- return cached.promise;
615
- }
616
- const promise = this.buildProjectPlacement(cwd);
617
- this.projectPlacementCache.set(cwd, {
618
- expiresAt: now + PROJECT_PLACEMENT_CACHE_TTL_MS,
619
- promise,
620
- });
621
- return promise;
622
- }
623
644
  async forwardAgentUpdate(agent) {
624
645
  try {
625
646
  const subscription = this.agentUpdatesSubscription;
@@ -629,7 +650,7 @@ export class Session {
629
650
  const payload = await this.buildAgentPayload(agent);
630
651
  const matches = this.matchesAgentFilter(payload, subscription.filter);
631
652
  if (matches) {
632
- const project = await this.getProjectPlacement(payload.cwd);
653
+ const project = await this.buildProjectPlacement(payload.cwd);
633
654
  this.emit({
634
655
  type: "agent_update",
635
656
  payload: { kind: "upsert", agent: payload, project },
@@ -661,10 +682,7 @@ export class Session {
661
682
  this.handleAudioPlayed(msg.id);
662
683
  break;
663
684
  case "fetch_agents_request":
664
- await this.handleFetchAgents(msg.requestId, msg.filter);
665
- break;
666
- case "fetch_agents_grouped_by_project_request":
667
- await this.handleFetchAgentsGroupedByProject(msg.requestId, msg.filter);
685
+ await this.handleFetchAgents(msg);
668
686
  break;
669
687
  case "fetch_agent_request":
670
688
  await this.handleFetchAgent(msg.agentId, msg.requestId);
@@ -699,6 +717,22 @@ export class Session {
699
717
  await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
700
718
  break;
701
719
  case "dictation_stream_start":
720
+ {
721
+ const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation");
722
+ if (unavailable) {
723
+ this.emit({
724
+ type: "dictation_stream_error",
725
+ payload: {
726
+ dictationId: msg.dictationId,
727
+ error: unavailable.message,
728
+ retryable: unavailable.retryable,
729
+ reasonCode: unavailable.reasonCode,
730
+ missingModelIds: unavailable.missingModelIds,
731
+ },
732
+ });
733
+ break;
734
+ }
735
+ }
702
736
  await this.dictationStreamManager.handleStart(msg.dictationId, msg.format);
703
737
  break;
704
738
  case "dictation_stream_chunk":
@@ -730,8 +764,8 @@ export class Session {
730
764
  case "restart_server_request":
731
765
  await this.handleRestartServerRequest(msg.requestId, msg.reason);
732
766
  break;
733
- case "initialize_agent_request":
734
- await this.handleInitializeAgentRequest(msg.agentId, msg.requestId);
767
+ case "fetch_agent_timeline_request":
768
+ await this.handleFetchAgentTimelineRequest(msg);
735
769
  break;
736
770
  case "set_agent_mode_request":
737
771
  await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId);
@@ -751,6 +785,9 @@ export class Session {
751
785
  case "validate_branch_request":
752
786
  await this.handleValidateBranchRequest(msg);
753
787
  break;
788
+ case "branch_suggestions_request":
789
+ await this.handleBranchSuggestionsRequest(msg);
790
+ break;
754
791
  case "subscribe_checkout_diff_request":
755
792
  await this.handleSubscribeCheckoutDiffRequest(msg);
756
793
  break;
@@ -830,6 +867,12 @@ export class Session {
830
867
  case "register_push_token":
831
868
  this.handleRegisterPushToken(msg.token);
832
869
  break;
870
+ case "subscribe_terminals_request":
871
+ this.handleSubscribeTerminalsRequest(msg);
872
+ break;
873
+ case "unsubscribe_terminals_request":
874
+ this.handleUnsubscribeTerminalsRequest(msg);
875
+ break;
833
876
  case "list_terminals_request":
834
877
  await this.handleListTerminalsRequest(msg);
835
878
  break;
@@ -848,6 +891,12 @@ export class Session {
848
891
  case "kill_terminal_request":
849
892
  await this.handleKillTerminalRequest(msg);
850
893
  break;
894
+ case "attach_terminal_stream_request":
895
+ await this.handleAttachTerminalStreamRequest(msg);
896
+ break;
897
+ case "detach_terminal_stream_request":
898
+ this.handleDetachTerminalStreamRequest(msg);
899
+ break;
851
900
  }
852
901
  }
853
902
  catch (error) {
@@ -881,6 +930,58 @@ export class Session {
881
930
  });
882
931
  }
883
932
  }
933
+ handleBinaryFrame(frame) {
934
+ switch (frame.channel) {
935
+ case BinaryMuxChannel.Terminal:
936
+ this.handleTerminalBinaryFrame(frame);
937
+ break;
938
+ default:
939
+ this.sessionLogger.warn({ channel: frame.channel, messageType: frame.messageType }, "Unhandled binary mux channel");
940
+ break;
941
+ }
942
+ }
943
+ handleTerminalBinaryFrame(frame) {
944
+ if (frame.messageType === TerminalBinaryMessageType.InputUtf8) {
945
+ const binding = this.terminalStreams.get(frame.streamId);
946
+ if (!binding) {
947
+ this.sessionLogger.warn({ streamId: frame.streamId }, "Terminal stream not found for input");
948
+ return;
949
+ }
950
+ if (!this.terminalManager) {
951
+ return;
952
+ }
953
+ const session = this.terminalManager.getTerminal(binding.terminalId);
954
+ if (!session) {
955
+ this.detachTerminalStream(frame.streamId, { emitExit: true });
956
+ return;
957
+ }
958
+ const payload = frame.payload ?? new Uint8Array(0);
959
+ if (payload.byteLength === 0) {
960
+ return;
961
+ }
962
+ const text = Buffer.from(payload).toString("utf8");
963
+ if (!text) {
964
+ return;
965
+ }
966
+ session.send({ type: "input", data: text });
967
+ return;
968
+ }
969
+ if (frame.messageType === TerminalBinaryMessageType.Ack) {
970
+ const binding = this.terminalStreams.get(frame.streamId);
971
+ if (binding) {
972
+ if (!Number.isFinite(frame.offset) || frame.offset < 0) {
973
+ return;
974
+ }
975
+ const nextAckOffset = Math.max(binding.lastAckOffset, Math.min(Math.floor(frame.offset), binding.lastOutputOffset));
976
+ if (nextAckOffset > binding.lastAckOffset) {
977
+ binding.lastAckOffset = nextAckOffset;
978
+ this.flushPendingTerminalStreamChunks(frame.streamId, binding);
979
+ }
980
+ }
981
+ return;
982
+ }
983
+ this.sessionLogger.warn({ streamId: frame.streamId, messageType: frame.messageType }, "Unhandled terminal binary frame");
984
+ }
884
985
  async handleRestartServerRequest(requestId, reason) {
885
986
  if (restartRequested) {
886
987
  this.sessionLogger.debug("Restart already requested, ignoring duplicate");
@@ -1032,12 +1133,57 @@ export class Session {
1032
1133
  });
1033
1134
  }
1034
1135
  }
1136
+ toVoiceFeatureUnavailableContext(state) {
1137
+ return {
1138
+ reasonCode: state.reasonCode,
1139
+ message: state.message,
1140
+ retryable: state.retryable,
1141
+ missingModelIds: [...state.missingModelIds],
1142
+ };
1143
+ }
1144
+ resolveModeReadinessState(readiness, mode) {
1145
+ if (mode === "voice_mode") {
1146
+ return readiness.realtimeVoice;
1147
+ }
1148
+ return readiness.dictation;
1149
+ }
1150
+ getVoiceFeatureUnavailableResponseMetadata(error) {
1151
+ if (!(error instanceof VoiceFeatureUnavailableError)) {
1152
+ return {};
1153
+ }
1154
+ return {
1155
+ reasonCode: error.reasonCode,
1156
+ retryable: error.retryable,
1157
+ missingModelIds: error.missingModelIds,
1158
+ };
1159
+ }
1160
+ resolveVoiceFeatureUnavailableContext(mode) {
1161
+ const readiness = this.getSpeechReadiness?.();
1162
+ if (!readiness) {
1163
+ return null;
1164
+ }
1165
+ const modeReadiness = this.resolveModeReadinessState(readiness, mode);
1166
+ if (!modeReadiness.enabled) {
1167
+ return this.toVoiceFeatureUnavailableContext(modeReadiness);
1168
+ }
1169
+ if (!readiness.voiceFeature.available) {
1170
+ return this.toVoiceFeatureUnavailableContext(readiness.voiceFeature);
1171
+ }
1172
+ if (!modeReadiness.available) {
1173
+ return this.toVoiceFeatureUnavailableContext(modeReadiness);
1174
+ }
1175
+ return null;
1176
+ }
1035
1177
  /**
1036
1178
  * Handle voice mode toggle
1037
1179
  */
1038
1180
  async handleSetVoiceMode(enabled, agentId, requestId) {
1039
1181
  try {
1040
1182
  if (enabled) {
1183
+ const unavailable = this.resolveVoiceFeatureUnavailableContext("voice_mode");
1184
+ if (unavailable) {
1185
+ throw new VoiceFeatureUnavailableError(unavailable);
1186
+ }
1041
1187
  const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? "", "set_voice_mode");
1042
1188
  if (this.isVoiceMode &&
1043
1189
  this.voiceModeAgentId &&
@@ -1084,6 +1230,7 @@ export class Session {
1084
1230
  }
1085
1231
  catch (error) {
1086
1232
  const errorMessage = error instanceof Error ? error.message : "Failed to set voice mode";
1233
+ const unavailable = this.getVoiceFeatureUnavailableResponseMetadata(error);
1087
1234
  this.sessionLogger.error({
1088
1235
  err: error,
1089
1236
  enabled,
@@ -1098,6 +1245,7 @@ export class Session {
1098
1245
  agentId: this.voiceModeAgentId,
1099
1246
  accepted: false,
1100
1247
  error: errorMessage,
1248
+ ...unavailable,
1101
1249
  },
1102
1250
  });
1103
1251
  return;
@@ -1403,44 +1551,16 @@ export class Session {
1403
1551
  }
1404
1552
  const prompt = this.buildAgentPrompt(text, images);
1405
1553
  try {
1406
- this.agentManager.recordUserMessage(agentId, text, { messageId });
1554
+ this.agentManager.recordUserMessage(agentId, text, {
1555
+ messageId,
1556
+ emitState: false,
1557
+ });
1407
1558
  }
1408
1559
  catch (error) {
1409
1560
  this.sessionLogger.error({ err: error, agentId }, `Failed to record user message for agent ${agentId}`);
1410
1561
  }
1411
1562
  this.startAgentStream(agentId, prompt, runOptions);
1412
1563
  }
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
1564
  /**
1445
1565
  * Handle create agent request
1446
1566
  */
@@ -1494,7 +1614,22 @@ export class Session {
1494
1614
  });
1495
1615
  }
1496
1616
  if (worktreeConfig) {
1497
- void this.runAsyncWorktreeSetup(snapshot.id, worktreeConfig);
1617
+ void runAsyncWorktreeBootstrap({
1618
+ agentId: snapshot.id,
1619
+ worktree: worktreeConfig,
1620
+ terminalManager: this.terminalManager,
1621
+ appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
1622
+ agentManager: this.agentManager,
1623
+ agentId: snapshot.id,
1624
+ item,
1625
+ }),
1626
+ emitLiveTimelineItem: (item) => emitLiveTimelineItemIfAgentKnown({
1627
+ agentManager: this.agentManager,
1628
+ agentId: snapshot.id,
1629
+ item,
1630
+ }),
1631
+ logger: this.sessionLogger,
1632
+ });
1498
1633
  }
1499
1634
  this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1500
1635
  }
@@ -1541,7 +1676,7 @@ export class Session {
1541
1676
  const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
1542
1677
  await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
1543
1678
  await this.forwardAgentUpdate(snapshot);
1544
- const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
1679
+ const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
1545
1680
  if (requestId) {
1546
1681
  const agentPayload = await this.getAgentPayloadById(snapshot.id);
1547
1682
  if (!agentPayload) {
@@ -1600,7 +1735,7 @@ export class Session {
1600
1735
  }
1601
1736
  await this.agentManager.hydrateTimelineFromProvider(agentId);
1602
1737
  await this.forwardAgentUpdate(snapshot);
1603
- const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
1738
+ const timelineSize = this.agentManager.getTimeline(agentId).length;
1604
1739
  if (requestId) {
1605
1740
  this.emit({
1606
1741
  type: "status",
@@ -1664,12 +1799,11 @@ export class Session {
1664
1799
  throw new Error("A branch name is required when creating a worktree.");
1665
1800
  }
1666
1801
  this.sessionLogger.info({ worktreeSlug: normalized.worktreeSlug ?? targetBranch, branch: targetBranch }, `Creating worktree '${normalized.worktreeSlug ?? targetBranch}' for branch ${targetBranch}`);
1667
- const createdWorktree = await createWorktree({
1802
+ const createdWorktree = await createAgentWorktree({
1668
1803
  branchName: targetBranch,
1669
1804
  cwd,
1670
1805
  baseBranch: normalized.baseBranch,
1671
1806
  worktreeSlug: normalized.worktreeSlug ?? targetBranch,
1672
- runSetup: false,
1673
1807
  paseoHome: this.paseoHome,
1674
1808
  });
1675
1809
  cwd = createdWorktree.worktreePath;
@@ -1693,100 +1827,6 @@ export class Session {
1693
1827
  worktreeConfig,
1694
1828
  };
1695
1829
  }
1696
- async runAsyncWorktreeSetup(agentId, worktree) {
1697
- const callId = uuidv4();
1698
- let results = [];
1699
- try {
1700
- const started = await this.safeAppendTimelineItem(agentId, {
1701
- type: "tool_call",
1702
- name: "paseo_worktree_setup",
1703
- callId,
1704
- status: "running",
1705
- detail: {
1706
- type: "unknown",
1707
- input: {
1708
- worktreePath: worktree.worktreePath,
1709
- branchName: worktree.branchName,
1710
- },
1711
- output: null,
1712
- },
1713
- error: null,
1714
- });
1715
- if (!started) {
1716
- return;
1717
- }
1718
- results = await runWorktreeSetupCommands({
1719
- worktreePath: worktree.worktreePath,
1720
- branchName: worktree.branchName,
1721
- cleanupOnFailure: false,
1722
- });
1723
- await this.safeAppendTimelineItem(agentId, {
1724
- type: "tool_call",
1725
- name: "paseo_worktree_setup",
1726
- callId,
1727
- status: "completed",
1728
- detail: {
1729
- type: "unknown",
1730
- input: {
1731
- worktreePath: worktree.worktreePath,
1732
- branchName: worktree.branchName,
1733
- },
1734
- output: {
1735
- worktreePath: worktree.worktreePath,
1736
- commands: results.map((result) => ({
1737
- command: result.command,
1738
- cwd: result.cwd,
1739
- exitCode: result.exitCode,
1740
- output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
1741
- })),
1742
- },
1743
- },
1744
- error: null,
1745
- });
1746
- }
1747
- catch (error) {
1748
- if (error instanceof WorktreeSetupError) {
1749
- results = error.results;
1750
- }
1751
- const message = error instanceof Error ? error.message : String(error);
1752
- await this.safeAppendTimelineItem(agentId, {
1753
- type: "tool_call",
1754
- name: "paseo_worktree_setup",
1755
- callId,
1756
- status: "failed",
1757
- detail: {
1758
- type: "unknown",
1759
- input: {
1760
- worktreePath: worktree.worktreePath,
1761
- branchName: worktree.branchName,
1762
- },
1763
- output: {
1764
- worktreePath: worktree.worktreePath,
1765
- commands: results.map((result) => ({
1766
- command: result.command,
1767
- cwd: result.cwd,
1768
- exitCode: result.exitCode,
1769
- output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
1770
- })),
1771
- },
1772
- },
1773
- error: { message },
1774
- });
1775
- }
1776
- }
1777
- async safeAppendTimelineItem(agentId, item) {
1778
- try {
1779
- await this.agentManager.appendTimelineItem(agentId, item);
1780
- return true;
1781
- }
1782
- catch (error) {
1783
- const message = error instanceof Error ? error.message : String(error);
1784
- if (message.includes("Unknown agent")) {
1785
- return false;
1786
- }
1787
- throw error;
1788
- }
1789
- }
1790
1830
  async handleListProviderModelsRequest(msg) {
1791
1831
  const fetchedAt = new Date().toISOString();
1792
1832
  try {
@@ -2627,6 +2667,31 @@ export class Session {
2627
2667
  });
2628
2668
  }
2629
2669
  }
2670
+ async handleBranchSuggestionsRequest(msg) {
2671
+ const { cwd, query, limit, requestId } = msg;
2672
+ try {
2673
+ const resolvedCwd = expandTilde(cwd);
2674
+ const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
2675
+ this.emit({
2676
+ type: "branch_suggestions_response",
2677
+ payload: {
2678
+ branches,
2679
+ error: null,
2680
+ requestId,
2681
+ },
2682
+ });
2683
+ }
2684
+ catch (error) {
2685
+ this.emit({
2686
+ type: "branch_suggestions_response",
2687
+ payload: {
2688
+ branches: [],
2689
+ error: error instanceof Error ? error.message : String(error),
2690
+ requestId,
2691
+ },
2692
+ });
2693
+ }
2694
+ }
2630
2695
  normalizeCheckoutDiffCompare(compare) {
2631
2696
  if (compare.mode === "uncommitted") {
2632
2697
  return { mode: "uncommitted" };
@@ -2798,30 +2863,42 @@ export class Session {
2798
2863
  latestPayload: null,
2799
2864
  latestFingerprint: null,
2800
2865
  };
2801
- const watchPaths = new Set([cwd]);
2802
- if (watchRoot) {
2803
- watchPaths.add(watchRoot);
2804
- }
2866
+ const repoWatchPath = watchRoot ?? cwd;
2867
+ const watchPaths = new Set([repoWatchPath]);
2805
2868
  const gitDir = await this.resolveCheckoutGitDir(cwd);
2806
2869
  if (gitDir) {
2807
2870
  watchPaths.add(gitDir);
2808
2871
  }
2809
- let hasWatchRootCoverage = false;
2872
+ let hasRecursiveRepoCoverage = false;
2873
+ const allowRecursiveRepoWatch = process.platform !== "linux";
2810
2874
  for (const watchPath of watchPaths) {
2875
+ const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
2811
2876
  const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
2812
2877
  this.scheduleCheckoutDiffTargetRefresh(target);
2813
2878
  });
2814
2879
  let watcher = null;
2880
+ let watcherIsRecursive = false;
2815
2881
  try {
2816
- watcher = createWatcher(true);
2882
+ if (shouldTryRecursive) {
2883
+ watcher = createWatcher(true);
2884
+ watcherIsRecursive = true;
2885
+ }
2886
+ else {
2887
+ watcher = createWatcher(false);
2888
+ }
2817
2889
  }
2818
2890
  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");
2891
+ if (shouldTryRecursive) {
2892
+ try {
2893
+ watcher = createWatcher(false);
2894
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff recursive watch unavailable; using non-recursive fallback");
2895
+ }
2896
+ catch (fallbackError) {
2897
+ this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
2898
+ }
2822
2899
  }
2823
- catch (fallbackError) {
2824
- this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
2900
+ else {
2901
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
2825
2902
  }
2826
2903
  }
2827
2904
  if (!watcher) {
@@ -2831,11 +2908,11 @@ export class Session {
2831
2908
  this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff watcher error");
2832
2909
  });
2833
2910
  target.watchers.push(watcher);
2834
- if (watchRoot && watchPath === watchRoot) {
2835
- hasWatchRootCoverage = true;
2911
+ if (watchPath === repoWatchPath && watcherIsRecursive) {
2912
+ hasRecursiveRepoCoverage = true;
2836
2913
  }
2837
2914
  }
2838
- const missingRepoCoverage = Boolean(watchRoot) && !hasWatchRootCoverage;
2915
+ const missingRepoCoverage = !hasRecursiveRepoCoverage;
2839
2916
  if (target.watchers.length === 0 || missingRepoCoverage) {
2840
2917
  target.fallbackRefreshInterval = setInterval(() => {
2841
2918
  this.scheduleCheckoutDiffTargetRefresh(target);
@@ -2846,7 +2923,7 @@ export class Session {
2846
2923
  intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
2847
2924
  reason: target.watchers.length === 0
2848
2925
  ? "no_watchers"
2849
- : "missing_repo_root_coverage",
2926
+ : "missing_recursive_repo_root_coverage",
2850
2927
  }, "Checkout diff watchers unavailable; using timed refresh fallback");
2851
2928
  }
2852
2929
  this.checkoutDiffTargets.set(targetKey, target);
@@ -3523,64 +3600,230 @@ export class Session {
3523
3600
  }
3524
3601
  return this.buildStoredAgentPayload(record);
3525
3602
  }
3526
- async handleFetchAgents(requestId, filter) {
3527
- try {
3528
- const agents = await this.listAgentPayloads(filter);
3529
- this.emit({
3530
- type: "fetch_agents_response",
3531
- payload: { requestId, agents },
3532
- });
3603
+ normalizeFetchAgentsSort(sort) {
3604
+ const fallback = [
3605
+ { key: "updated_at", direction: "desc" },
3606
+ ];
3607
+ if (!sort || sort.length === 0) {
3608
+ return fallback;
3609
+ }
3610
+ const deduped = [];
3611
+ const seen = new Set();
3612
+ for (const entry of sort) {
3613
+ if (seen.has(entry.key)) {
3614
+ continue;
3615
+ }
3616
+ seen.add(entry.key);
3617
+ deduped.push(entry);
3533
3618
  }
3534
- catch (error) {
3535
- this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
3536
- this.emit({
3537
- type: "fetch_agents_response",
3538
- payload: { requestId, agents: [] },
3539
- });
3619
+ return deduped.length > 0 ? deduped : fallback;
3620
+ }
3621
+ getStatusPriority(agent) {
3622
+ const requiresAttention = agent.requiresAttention ?? false;
3623
+ const attentionReason = agent.attentionReason ?? null;
3624
+ if (requiresAttention && attentionReason === "permission") {
3625
+ return 0;
3540
3626
  }
3627
+ if (agent.status === "error" || attentionReason === "error") {
3628
+ return 1;
3629
+ }
3630
+ if (agent.status === "running") {
3631
+ return 2;
3632
+ }
3633
+ if (agent.status === "initializing") {
3634
+ return 3;
3635
+ }
3636
+ return 4;
3541
3637
  }
3542
- async listAgentsGroupedByProjectPayload(filter) {
3543
- const agents = await this.listAgentPayloads(filter);
3544
- const visibleAgents = agents
3545
- .filter((agent) => !agent.archivedAt)
3546
- .sort((left, right) => Date.parse(right.updatedAt || "") - Date.parse(left.updatedAt || ""));
3547
- const grouped = new Map();
3548
- // Warm project placement status for all visible roots up front to avoid serial N+1 latency.
3549
- for (const agent of visibleAgents) {
3550
- void this.getProjectPlacement(agent.cwd);
3551
- }
3552
- for (const agent of visibleAgents) {
3553
- const project = await this.getProjectPlacement(agent.cwd);
3554
- const projectKey = project.projectKey;
3555
- let group = grouped.get(projectKey);
3556
- if (!group) {
3557
- group = {
3558
- projectKey,
3559
- projectName: project.projectName,
3560
- agents: [],
3561
- };
3562
- grouped.set(projectKey, group);
3638
+ getFetchAgentsSortValue(entry, key) {
3639
+ switch (key) {
3640
+ case "status_priority":
3641
+ return this.getStatusPriority(entry.agent);
3642
+ case "created_at":
3643
+ return Date.parse(entry.agent.createdAt);
3644
+ case "updated_at":
3645
+ return Date.parse(entry.agent.updatedAt);
3646
+ case "title":
3647
+ return entry.agent.title?.toLocaleLowerCase() ?? "";
3648
+ }
3649
+ }
3650
+ compareSortValues(left, right) {
3651
+ if (left === right) {
3652
+ return 0;
3653
+ }
3654
+ if (left === null) {
3655
+ return -1;
3656
+ }
3657
+ if (right === null) {
3658
+ return 1;
3659
+ }
3660
+ if (typeof left === "number" && typeof right === "number") {
3661
+ return left < right ? -1 : 1;
3662
+ }
3663
+ return String(left).localeCompare(String(right));
3664
+ }
3665
+ compareFetchAgentsEntries(left, right, sort) {
3666
+ for (const spec of sort) {
3667
+ const leftValue = this.getFetchAgentsSortValue(left, spec.key);
3668
+ const rightValue = this.getFetchAgentsSortValue(right, spec.key);
3669
+ const base = this.compareSortValues(leftValue, rightValue);
3670
+ if (base === 0) {
3671
+ continue;
3672
+ }
3673
+ return spec.direction === "asc" ? base : -base;
3674
+ }
3675
+ return left.agent.id.localeCompare(right.agent.id);
3676
+ }
3677
+ encodeFetchAgentsCursor(entry, sort) {
3678
+ const values = {};
3679
+ for (const spec of sort) {
3680
+ values[spec.key] = this.getFetchAgentsSortValue(entry, spec.key);
3681
+ }
3682
+ return Buffer.from(JSON.stringify({
3683
+ sort,
3684
+ values,
3685
+ id: entry.agent.id,
3686
+ }), "utf8").toString("base64url");
3687
+ }
3688
+ decodeFetchAgentsCursor(cursor, sort) {
3689
+ let parsed;
3690
+ try {
3691
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
3692
+ }
3693
+ catch {
3694
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3695
+ }
3696
+ if (!parsed || typeof parsed !== "object") {
3697
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3698
+ }
3699
+ const payload = parsed;
3700
+ if (!Array.isArray(payload.sort) || typeof payload.id !== "string") {
3701
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3702
+ }
3703
+ if (!payload.values || typeof payload.values !== "object") {
3704
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3705
+ }
3706
+ const cursorSort = [];
3707
+ for (const item of payload.sort) {
3708
+ if (!item ||
3709
+ typeof item !== "object" ||
3710
+ typeof item.key !== "string" ||
3711
+ typeof item.direction !== "string") {
3712
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3713
+ }
3714
+ const key = item.key;
3715
+ const direction = item.direction;
3716
+ if ((key !== "status_priority" &&
3717
+ key !== "created_at" &&
3718
+ key !== "updated_at" &&
3719
+ key !== "title") ||
3720
+ (direction !== "asc" && direction !== "desc")) {
3721
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3563
3722
  }
3564
- if (group.agents.length >= MAX_AGENTS_PER_PROJECT) {
3723
+ cursorSort.push({ key, direction });
3724
+ }
3725
+ if (cursorSort.length !== sort.length ||
3726
+ cursorSort.some((entry, index) => entry.key !== sort[index]?.key ||
3727
+ entry.direction !== sort[index]?.direction)) {
3728
+ throw new SessionRequestError("invalid_cursor", "fetch_agents cursor does not match current sort");
3729
+ }
3730
+ return {
3731
+ sort: cursorSort,
3732
+ values: payload.values,
3733
+ id: payload.id,
3734
+ };
3735
+ }
3736
+ compareEntryWithCursor(entry, cursor, sort) {
3737
+ for (const spec of sort) {
3738
+ const leftValue = this.getFetchAgentsSortValue(entry, spec.key);
3739
+ const rightValue = cursor.values[spec.key] !== undefined ? cursor.values[spec.key] ?? null : null;
3740
+ const base = this.compareSortValues(leftValue, rightValue);
3741
+ if (base === 0) {
3565
3742
  continue;
3566
3743
  }
3567
- group.agents.push({ agent, checkout: project.checkout });
3744
+ return spec.direction === "asc" ? base : -base;
3568
3745
  }
3569
- return Array.from(grouped.values());
3746
+ return entry.agent.id.localeCompare(cursor.id);
3570
3747
  }
3571
- async handleFetchAgentsGroupedByProject(requestId, filter) {
3748
+ async listFetchAgentsEntries(request) {
3749
+ const filter = request.filter;
3750
+ const sort = this.normalizeFetchAgentsSort(request.sort);
3751
+ const includeArchived = filter?.includeArchived ?? false;
3752
+ let agents = await this.listAgentPayloads({
3753
+ labels: filter?.labels,
3754
+ });
3755
+ if (!includeArchived) {
3756
+ agents = agents.filter((agent) => !agent.archivedAt);
3757
+ }
3758
+ if (filter?.statuses && filter.statuses.length > 0) {
3759
+ const statuses = new Set(filter.statuses);
3760
+ agents = agents.filter((agent) => statuses.has(agent.status));
3761
+ }
3762
+ if (typeof filter?.requiresAttention === "boolean") {
3763
+ agents = agents.filter((agent) => (agent.requiresAttention ?? false) === filter.requiresAttention);
3764
+ }
3765
+ const placementByCwd = new Map();
3766
+ const getPlacement = (cwd) => {
3767
+ const existing = placementByCwd.get(cwd);
3768
+ if (existing) {
3769
+ return existing;
3770
+ }
3771
+ const placementPromise = this.buildProjectPlacement(cwd);
3772
+ placementByCwd.set(cwd, placementPromise);
3773
+ return placementPromise;
3774
+ };
3775
+ let entries = await Promise.all(agents.map(async (agent) => ({
3776
+ agent,
3777
+ project: await getPlacement(agent.cwd),
3778
+ })));
3779
+ if (filter?.projectKeys && filter.projectKeys.length > 0) {
3780
+ const projectKeys = new Set(filter.projectKeys.filter((item) => item.trim().length > 0));
3781
+ entries = entries.filter((entry) => projectKeys.has(entry.project.projectKey));
3782
+ }
3783
+ entries.sort((left, right) => this.compareFetchAgentsEntries(left, right, sort));
3784
+ const cursorToken = request.page?.cursor;
3785
+ if (cursorToken) {
3786
+ const cursor = this.decodeFetchAgentsCursor(cursorToken, sort);
3787
+ entries = entries.filter((entry) => this.compareEntryWithCursor(entry, cursor, sort) > 0);
3788
+ }
3789
+ const limit = request.page?.limit ?? entries.length;
3790
+ const pagedEntries = entries.slice(0, limit);
3791
+ const hasMore = entries.length > limit;
3792
+ const nextCursor = hasMore && pagedEntries.length > 0
3793
+ ? this.encodeFetchAgentsCursor(pagedEntries[pagedEntries.length - 1], sort)
3794
+ : null;
3795
+ return {
3796
+ entries: pagedEntries,
3797
+ pageInfo: {
3798
+ nextCursor,
3799
+ prevCursor: request.page?.cursor ?? null,
3800
+ hasMore,
3801
+ },
3802
+ };
3803
+ }
3804
+ async handleFetchAgents(request) {
3572
3805
  try {
3573
- const groups = await this.listAgentsGroupedByProjectPayload(filter);
3806
+ const payload = await this.listFetchAgentsEntries(request);
3574
3807
  this.emit({
3575
- type: "fetch_agents_grouped_by_project_response",
3576
- payload: { requestId, groups },
3808
+ type: "fetch_agents_response",
3809
+ payload: {
3810
+ requestId: request.requestId,
3811
+ ...payload,
3812
+ },
3577
3813
  });
3578
3814
  }
3579
3815
  catch (error) {
3580
- this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_grouped_by_project_request");
3816
+ const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
3817
+ const message = error instanceof Error ? error.message : "Failed to fetch agents";
3818
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
3581
3819
  this.emit({
3582
- type: "fetch_agents_grouped_by_project_response",
3583
- payload: { requestId, groups: [] },
3820
+ type: "rpc_error",
3821
+ payload: {
3822
+ requestId: request.requestId,
3823
+ requestType: request.type,
3824
+ error: message,
3825
+ code,
3826
+ },
3584
3827
  });
3585
3828
  }
3586
3829
  }
@@ -3606,6 +3849,77 @@ export class Session {
3606
3849
  payload: { requestId, agent, error: null },
3607
3850
  });
3608
3851
  }
3852
+ async handleFetchAgentTimelineRequest(msg) {
3853
+ const direction = msg.direction ?? (msg.cursor ? "after" : "tail");
3854
+ const projection = msg.projection ?? "projected";
3855
+ const limit = msg.limit ?? (direction === "after" ? 0 : undefined);
3856
+ const cursor = msg.cursor
3857
+ ? {
3858
+ epoch: msg.cursor.epoch,
3859
+ seq: msg.cursor.seq,
3860
+ }
3861
+ : undefined;
3862
+ try {
3863
+ const snapshot = await this.ensureAgentLoaded(msg.agentId);
3864
+ const timeline = this.agentManager.fetchTimeline(msg.agentId, {
3865
+ direction,
3866
+ cursor,
3867
+ limit,
3868
+ });
3869
+ const projected = projectTimelineRows(timeline.rows, snapshot.provider, projection);
3870
+ const firstRow = timeline.rows[0];
3871
+ const lastRow = timeline.rows[timeline.rows.length - 1];
3872
+ const startCursor = firstRow
3873
+ ? { epoch: timeline.epoch, seq: firstRow.seq }
3874
+ : null;
3875
+ const endCursor = lastRow
3876
+ ? { epoch: timeline.epoch, seq: lastRow.seq }
3877
+ : null;
3878
+ this.emit({
3879
+ type: "fetch_agent_timeline_response",
3880
+ payload: {
3881
+ requestId: msg.requestId,
3882
+ agentId: msg.agentId,
3883
+ direction,
3884
+ projection,
3885
+ epoch: timeline.epoch,
3886
+ reset: timeline.reset,
3887
+ staleCursor: timeline.staleCursor,
3888
+ gap: timeline.gap,
3889
+ window: timeline.window,
3890
+ startCursor,
3891
+ endCursor,
3892
+ hasOlder: timeline.hasOlder,
3893
+ hasNewer: timeline.hasNewer,
3894
+ entries: projected,
3895
+ error: null,
3896
+ },
3897
+ });
3898
+ }
3899
+ catch (error) {
3900
+ this.sessionLogger.error({ err: error, agentId: msg.agentId }, "Failed to handle fetch_agent_timeline_request");
3901
+ this.emit({
3902
+ type: "fetch_agent_timeline_response",
3903
+ payload: {
3904
+ requestId: msg.requestId,
3905
+ agentId: msg.agentId,
3906
+ direction,
3907
+ projection,
3908
+ epoch: "",
3909
+ reset: false,
3910
+ staleCursor: false,
3911
+ gap: false,
3912
+ window: { minSeq: 0, maxSeq: 0, nextSeq: 0 },
3913
+ startCursor: null,
3914
+ endCursor: null,
3915
+ hasOlder: false,
3916
+ hasNewer: false,
3917
+ entries: [],
3918
+ error: error instanceof Error ? error.message : String(error),
3919
+ },
3920
+ });
3921
+ }
3922
+ }
3609
3923
  async handleSendAgentMessageRequest(msg) {
3610
3924
  const resolved = await this.resolveAgentIdentifier(msg.agentId);
3611
3925
  if (!resolved.ok) {
@@ -3625,7 +3939,10 @@ export class Session {
3625
3939
  await this.ensureAgentLoaded(agentId);
3626
3940
  await this.interruptAgentIfRunning(agentId);
3627
3941
  try {
3628
- this.agentManager.recordUserMessage(agentId, msg.text, { messageId: msg.messageId });
3942
+ this.agentManager.recordUserMessage(agentId, msg.text, {
3943
+ messageId: msg.messageId,
3944
+ emitState: false,
3945
+ });
3629
3946
  }
3630
3947
  catch (error) {
3631
3948
  this.sessionLogger.error({ err: error, agentId }, "Failed to record user message for send_agent_message_request");
@@ -3782,30 +4099,6 @@ export class Session {
3782
4099
  clearTimeout(timeoutHandle);
3783
4100
  }
3784
4101
  }
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
4102
  /**
3810
4103
  * Handle audio chunk for buffering and transcription
3811
4104
  */
@@ -4257,6 +4550,17 @@ export class Session {
4257
4550
  }
4258
4551
  this.onMessage(msg);
4259
4552
  }
4553
+ emitBinary(frame) {
4554
+ if (!this.onBinaryMessage) {
4555
+ return;
4556
+ }
4557
+ try {
4558
+ this.onBinaryMessage(frame);
4559
+ }
4560
+ catch (error) {
4561
+ this.sessionLogger.error({ err: error }, "Failed to emit binary frame");
4562
+ }
4563
+ }
4260
4564
  /**
4261
4565
  * Clean up session resources
4262
4566
  */
@@ -4295,10 +4599,20 @@ export class Session {
4295
4599
  await this.disableVoiceModeForActiveAgent(true);
4296
4600
  this.isVoiceMode = false;
4297
4601
  // Unsubscribe from all terminals
4602
+ if (this.unsubscribeTerminalsChanged) {
4603
+ this.unsubscribeTerminalsChanged();
4604
+ this.unsubscribeTerminalsChanged = null;
4605
+ }
4606
+ this.subscribedTerminalDirectories.clear();
4298
4607
  for (const unsubscribe of this.terminalSubscriptions.values()) {
4299
4608
  unsubscribe();
4300
4609
  }
4301
4610
  this.terminalSubscriptions.clear();
4611
+ for (const unsubscribeExit of this.terminalExitSubscriptions.values()) {
4612
+ unsubscribeExit();
4613
+ }
4614
+ this.terminalExitSubscriptions.clear();
4615
+ this.detachAllTerminalStreams({ emitExit: false });
4302
4616
  for (const target of this.checkoutDiffTargets.values()) {
4303
4617
  this.closeCheckoutDiffWatchTarget(target);
4304
4618
  }
@@ -4308,6 +4622,96 @@ export class Session {
4308
4622
  // ============================================================================
4309
4623
  // Terminal Handlers
4310
4624
  // ============================================================================
4625
+ ensureTerminalExitSubscription(terminal) {
4626
+ if (this.terminalExitSubscriptions.has(terminal.id)) {
4627
+ return;
4628
+ }
4629
+ const unsubscribeExit = terminal.onExit(() => {
4630
+ this.handleTerminalExited(terminal.id);
4631
+ });
4632
+ this.terminalExitSubscriptions.set(terminal.id, unsubscribeExit);
4633
+ }
4634
+ handleTerminalExited(terminalId) {
4635
+ const unsubscribeExit = this.terminalExitSubscriptions.get(terminalId);
4636
+ if (unsubscribeExit) {
4637
+ unsubscribeExit();
4638
+ this.terminalExitSubscriptions.delete(terminalId);
4639
+ }
4640
+ const unsubscribe = this.terminalSubscriptions.get(terminalId);
4641
+ if (unsubscribe) {
4642
+ try {
4643
+ unsubscribe();
4644
+ }
4645
+ catch (error) {
4646
+ this.sessionLogger.warn({ err: error, terminalId }, "Failed to unsubscribe terminal after process exit");
4647
+ }
4648
+ this.terminalSubscriptions.delete(terminalId);
4649
+ }
4650
+ const streamId = this.terminalStreamByTerminalId.get(terminalId);
4651
+ if (typeof streamId === "number") {
4652
+ this.detachTerminalStream(streamId, { emitExit: true });
4653
+ }
4654
+ }
4655
+ emitTerminalsChangedSnapshot(input) {
4656
+ this.emit({
4657
+ type: "terminals_changed",
4658
+ payload: {
4659
+ cwd: input.cwd,
4660
+ terminals: input.terminals,
4661
+ },
4662
+ });
4663
+ }
4664
+ handleTerminalsChanged(event) {
4665
+ if (!this.subscribedTerminalDirectories.has(event.cwd)) {
4666
+ return;
4667
+ }
4668
+ this.emitTerminalsChangedSnapshot({
4669
+ cwd: event.cwd,
4670
+ terminals: event.terminals.map((terminal) => ({
4671
+ id: terminal.id,
4672
+ name: terminal.name,
4673
+ })),
4674
+ });
4675
+ }
4676
+ handleSubscribeTerminalsRequest(msg) {
4677
+ this.subscribedTerminalDirectories.add(msg.cwd);
4678
+ void this.emitInitialTerminalsChangedSnapshot(msg.cwd);
4679
+ }
4680
+ handleUnsubscribeTerminalsRequest(msg) {
4681
+ this.subscribedTerminalDirectories.delete(msg.cwd);
4682
+ }
4683
+ async emitInitialTerminalsChangedSnapshot(cwd) {
4684
+ if (!this.terminalManager || !this.subscribedTerminalDirectories.has(cwd)) {
4685
+ return;
4686
+ }
4687
+ const hadDirectoryBeforeSubscribe = this.terminalManager
4688
+ .listDirectories()
4689
+ .includes(cwd);
4690
+ try {
4691
+ const terminals = await this.terminalManager.getTerminals(cwd);
4692
+ for (const terminal of terminals) {
4693
+ this.ensureTerminalExitSubscription(terminal);
4694
+ }
4695
+ // New directories auto-create Terminal 1, which already emits through
4696
+ // terminal-manager change listeners.
4697
+ if (!hadDirectoryBeforeSubscribe) {
4698
+ return;
4699
+ }
4700
+ if (!this.subscribedTerminalDirectories.has(cwd)) {
4701
+ return;
4702
+ }
4703
+ this.emitTerminalsChangedSnapshot({
4704
+ cwd,
4705
+ terminals: terminals.map((terminal) => ({
4706
+ id: terminal.id,
4707
+ name: terminal.name,
4708
+ })),
4709
+ });
4710
+ }
4711
+ catch (error) {
4712
+ this.sessionLogger.warn({ err: error, cwd }, "Failed to emit initial terminal snapshot");
4713
+ }
4714
+ }
4311
4715
  async handleListTerminalsRequest(msg) {
4312
4716
  if (!this.terminalManager) {
4313
4717
  this.emit({
@@ -4322,6 +4726,9 @@ export class Session {
4322
4726
  }
4323
4727
  try {
4324
4728
  const terminals = await this.terminalManager.getTerminals(msg.cwd);
4729
+ for (const terminal of terminals) {
4730
+ this.ensureTerminalExitSubscription(terminal);
4731
+ }
4325
4732
  this.emit({
4326
4733
  type: "list_terminals_response",
4327
4734
  payload: {
@@ -4360,6 +4767,7 @@ export class Session {
4360
4767
  cwd: msg.cwd,
4361
4768
  name: msg.name,
4362
4769
  });
4770
+ this.ensureTerminalExitSubscription(session);
4363
4771
  this.emit({
4364
4772
  type: "create_terminal_response",
4365
4773
  payload: {
@@ -4407,6 +4815,7 @@ export class Session {
4407
4815
  });
4408
4816
  return;
4409
4817
  }
4818
+ this.ensureTerminalExitSubscription(session);
4410
4819
  // Unsubscribe from previous subscription if any
4411
4820
  const existing = this.terminalSubscriptions.get(msg.terminalId);
4412
4821
  if (existing) {
@@ -4452,6 +4861,7 @@ export class Session {
4452
4861
  this.sessionLogger.warn({ terminalId: msg.terminalId }, "Terminal not found for input");
4453
4862
  return;
4454
4863
  }
4864
+ this.ensureTerminalExitSubscription(session);
4455
4865
  session.send(msg.message);
4456
4866
  }
4457
4867
  async handleKillTerminalRequest(msg) {
@@ -4472,6 +4882,10 @@ export class Session {
4472
4882
  unsubscribe();
4473
4883
  this.terminalSubscriptions.delete(msg.terminalId);
4474
4884
  }
4885
+ const streamId = this.terminalStreamByTerminalId.get(msg.terminalId);
4886
+ if (typeof streamId === "number") {
4887
+ this.detachTerminalStream(streamId, { emitExit: true });
4888
+ }
4475
4889
  this.terminalManager.killTerminal(msg.terminalId);
4476
4890
  this.emit({
4477
4891
  type: "kill_terminal_response",
@@ -4482,5 +4896,213 @@ export class Session {
4482
4896
  },
4483
4897
  });
4484
4898
  }
4899
+ async handleAttachTerminalStreamRequest(msg) {
4900
+ if (!this.terminalManager || !this.onBinaryMessage) {
4901
+ this.emit({
4902
+ type: "attach_terminal_stream_response",
4903
+ payload: {
4904
+ terminalId: msg.terminalId,
4905
+ streamId: null,
4906
+ replayedFrom: 0,
4907
+ currentOffset: 0,
4908
+ earliestAvailableOffset: 0,
4909
+ reset: true,
4910
+ error: "Terminal streaming not available",
4911
+ requestId: msg.requestId,
4912
+ },
4913
+ });
4914
+ return;
4915
+ }
4916
+ const session = this.terminalManager.getTerminal(msg.terminalId);
4917
+ if (!session) {
4918
+ this.emit({
4919
+ type: "attach_terminal_stream_response",
4920
+ payload: {
4921
+ terminalId: msg.terminalId,
4922
+ streamId: null,
4923
+ replayedFrom: 0,
4924
+ currentOffset: 0,
4925
+ earliestAvailableOffset: 0,
4926
+ reset: true,
4927
+ error: "Terminal not found",
4928
+ requestId: msg.requestId,
4929
+ },
4930
+ });
4931
+ return;
4932
+ }
4933
+ if (msg.rows || msg.cols) {
4934
+ const state = session.getState();
4935
+ session.send({
4936
+ type: "resize",
4937
+ rows: msg.rows ?? state.rows,
4938
+ cols: msg.cols ?? state.cols,
4939
+ });
4940
+ }
4941
+ const existingStreamId = this.terminalStreamByTerminalId.get(msg.terminalId);
4942
+ if (typeof existingStreamId === "number") {
4943
+ this.detachTerminalStream(existingStreamId, { emitExit: false });
4944
+ }
4945
+ const streamId = this.allocateTerminalStreamId();
4946
+ const initialOffset = Math.max(0, Math.floor(msg.resumeOffset ?? 0));
4947
+ const binding = {
4948
+ terminalId: msg.terminalId,
4949
+ unsubscribe: () => { },
4950
+ lastOutputOffset: initialOffset,
4951
+ lastAckOffset: initialOffset,
4952
+ pendingChunks: [],
4953
+ pendingBytes: 0,
4954
+ };
4955
+ this.terminalStreams.set(streamId, binding);
4956
+ this.terminalStreamByTerminalId.set(msg.terminalId, streamId);
4957
+ let rawSub;
4958
+ try {
4959
+ rawSub = session.subscribeRaw((chunk) => {
4960
+ const currentBinding = this.terminalStreams.get(streamId);
4961
+ if (!currentBinding) {
4962
+ return;
4963
+ }
4964
+ this.enqueueOrEmitTerminalStreamChunk(streamId, currentBinding, {
4965
+ data: chunk.data,
4966
+ startOffset: chunk.startOffset,
4967
+ endOffset: chunk.endOffset,
4968
+ replay: chunk.replay,
4969
+ });
4970
+ }, { fromOffset: msg.resumeOffset ?? 0 });
4971
+ }
4972
+ catch (error) {
4973
+ this.terminalStreams.delete(streamId);
4974
+ this.terminalStreamByTerminalId.delete(msg.terminalId);
4975
+ throw error;
4976
+ }
4977
+ binding.unsubscribe = rawSub.unsubscribe;
4978
+ binding.lastAckOffset = rawSub.replayedFrom;
4979
+ if (binding.lastOutputOffset < rawSub.replayedFrom) {
4980
+ binding.lastOutputOffset = rawSub.replayedFrom;
4981
+ }
4982
+ this.flushPendingTerminalStreamChunks(streamId, binding);
4983
+ this.emit({
4984
+ type: "attach_terminal_stream_response",
4985
+ payload: {
4986
+ terminalId: msg.terminalId,
4987
+ streamId,
4988
+ replayedFrom: rawSub.replayedFrom,
4989
+ currentOffset: rawSub.currentOffset,
4990
+ earliestAvailableOffset: rawSub.earliestAvailableOffset,
4991
+ reset: rawSub.reset,
4992
+ error: null,
4993
+ requestId: msg.requestId,
4994
+ },
4995
+ });
4996
+ }
4997
+ getTerminalStreamChunkByteLength(chunk) {
4998
+ return Math.max(0, chunk.endOffset - chunk.startOffset);
4999
+ }
5000
+ canEmitTerminalStreamChunk(binding, chunk) {
5001
+ return chunk.startOffset < binding.lastAckOffset + TERMINAL_STREAM_WINDOW_BYTES;
5002
+ }
5003
+ emitTerminalStreamChunk(streamId, binding, chunk) {
5004
+ const payload = new Uint8Array(Buffer.from(chunk.data, "utf8"));
5005
+ this.emitBinary({
5006
+ channel: BinaryMuxChannel.Terminal,
5007
+ messageType: TerminalBinaryMessageType.OutputUtf8,
5008
+ streamId,
5009
+ offset: chunk.startOffset,
5010
+ flags: chunk.replay ? TerminalBinaryFlags.Replay : 0,
5011
+ payload,
5012
+ });
5013
+ binding.lastOutputOffset = chunk.endOffset;
5014
+ }
5015
+ enqueueOrEmitTerminalStreamChunk(streamId, binding, chunk) {
5016
+ const chunkBytes = this.getTerminalStreamChunkByteLength(chunk);
5017
+ if (binding.pendingChunks.length > 0 || !this.canEmitTerminalStreamChunk(binding, chunk)) {
5018
+ if (binding.pendingChunks.length >= TERMINAL_STREAM_MAX_PENDING_CHUNKS ||
5019
+ binding.pendingBytes + chunkBytes > TERMINAL_STREAM_MAX_PENDING_BYTES) {
5020
+ this.sessionLogger.warn({
5021
+ streamId,
5022
+ pendingChunks: binding.pendingChunks.length,
5023
+ pendingBytes: binding.pendingBytes,
5024
+ chunkBytes,
5025
+ }, "Terminal stream pending buffer overflow; closing stream");
5026
+ this.detachTerminalStream(streamId, { emitExit: true });
5027
+ return;
5028
+ }
5029
+ binding.pendingChunks.push(chunk);
5030
+ binding.pendingBytes += chunkBytes;
5031
+ return;
5032
+ }
5033
+ this.emitTerminalStreamChunk(streamId, binding, chunk);
5034
+ }
5035
+ flushPendingTerminalStreamChunks(streamId, binding) {
5036
+ while (binding.pendingChunks.length > 0) {
5037
+ const next = binding.pendingChunks[0];
5038
+ if (!next || !this.canEmitTerminalStreamChunk(binding, next)) {
5039
+ break;
5040
+ }
5041
+ binding.pendingChunks.shift();
5042
+ binding.pendingBytes -= this.getTerminalStreamChunkByteLength(next);
5043
+ if (binding.pendingBytes < 0) {
5044
+ binding.pendingBytes = 0;
5045
+ }
5046
+ this.emitTerminalStreamChunk(streamId, binding, next);
5047
+ }
5048
+ }
5049
+ handleDetachTerminalStreamRequest(msg) {
5050
+ const success = this.detachTerminalStream(msg.streamId, { emitExit: false });
5051
+ this.emit({
5052
+ type: "detach_terminal_stream_response",
5053
+ payload: {
5054
+ streamId: msg.streamId,
5055
+ success,
5056
+ requestId: msg.requestId,
5057
+ },
5058
+ });
5059
+ }
5060
+ detachAllTerminalStreams(options) {
5061
+ for (const streamId of Array.from(this.terminalStreams.keys())) {
5062
+ this.detachTerminalStream(streamId, options);
5063
+ }
5064
+ }
5065
+ detachTerminalStream(streamId, options) {
5066
+ const binding = this.terminalStreams.get(streamId);
5067
+ if (!binding) {
5068
+ return false;
5069
+ }
5070
+ try {
5071
+ binding.unsubscribe();
5072
+ }
5073
+ catch (error) {
5074
+ this.sessionLogger.warn({ err: error, streamId }, "Failed to unsubscribe terminal stream");
5075
+ }
5076
+ this.terminalStreams.delete(streamId);
5077
+ if (this.terminalStreamByTerminalId.get(binding.terminalId) === streamId) {
5078
+ this.terminalStreamByTerminalId.delete(binding.terminalId);
5079
+ }
5080
+ if (options?.emitExit) {
5081
+ this.emit({
5082
+ type: "terminal_stream_exit",
5083
+ payload: {
5084
+ streamId,
5085
+ terminalId: binding.terminalId,
5086
+ },
5087
+ });
5088
+ }
5089
+ return true;
5090
+ }
5091
+ allocateTerminalStreamId() {
5092
+ let attempts = 0;
5093
+ while (attempts < 0xffffffff) {
5094
+ const candidate = this.nextTerminalStreamId >>> 0;
5095
+ this.nextTerminalStreamId = ((this.nextTerminalStreamId + 1) & 0xffffffff) >>> 0;
5096
+ if (candidate === 0) {
5097
+ attempts += 1;
5098
+ continue;
5099
+ }
5100
+ if (!this.terminalStreams.has(candidate)) {
5101
+ return candidate;
5102
+ }
5103
+ attempts += 1;
5104
+ }
5105
+ throw new Error("Unable to allocate terminal stream id");
5106
+ }
4485
5107
  }
4486
5108
  //# sourceMappingURL=session.js.map