@getpaseo/server 0.1.2 → 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 (175) 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 +54 -36
  26. package/dist/server/client/daemon-client.d.ts.map +1 -1
  27. package/dist/server/client/daemon-client.js +497 -800
  28. package/dist/server/client/daemon-client.js.map +1 -1
  29. package/dist/server/server/agent/agent-manager.d.ts +52 -0
  30. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  31. package/dist/server/server/agent/agent-manager.js +238 -10
  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-detail-parser.d.ts.map +1 -1
  35. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +62 -12
  36. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
  37. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts +4 -4
  38. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
  39. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +213 -50
  40. package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
  41. package/dist/server/server/agent/providers/claude-agent.d.ts +7 -0
  42. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  43. package/dist/server/server/agent/providers/claude-agent.js +100 -19
  44. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  45. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts +0 -1
  46. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -1
  47. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +67 -30
  48. package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -1
  49. package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts +1 -1
  50. package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts.map +1 -1
  51. package/dist/server/server/agent/providers/codex/tool-call-mapper.js +293 -266
  52. package/dist/server/server/agent/providers/codex/tool-call-mapper.js.map +1 -1
  53. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +1 -1
  54. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  55. package/dist/server/server/agent/providers/codex-app-server-agent.js +49 -14
  56. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  57. package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts.map +1 -1
  58. package/dist/server/server/agent/providers/codex-rollout-timeline.js +5 -4
  59. package/dist/server/server/agent/providers/codex-rollout-timeline.js.map +1 -1
  60. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts.map +1 -1
  61. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +8 -2
  62. package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js.map +1 -1
  63. package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts +1 -1
  64. package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts.map +1 -1
  65. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +121 -45
  66. package/dist/server/server/agent/providers/opencode/tool-call-mapper.js.map +1 -1
  67. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  68. package/dist/server/server/agent/providers/opencode-agent.js +87 -35
  69. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  70. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +2 -2
  71. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  72. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +23 -6
  73. package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
  74. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +0 -1
  75. package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts.map +1 -1
  76. package/dist/server/server/agent/providers/tool-call-mapper-utils.js +0 -10
  77. package/dist/server/server/agent/providers/tool-call-mapper-utils.js.map +1 -1
  78. package/dist/server/server/agent/stt-manager.d.ts +3 -2
  79. package/dist/server/server/agent/stt-manager.d.ts.map +1 -1
  80. package/dist/server/server/agent/stt-manager.js +5 -3
  81. package/dist/server/server/agent/stt-manager.js.map +1 -1
  82. package/dist/server/server/agent/timeline-projection.d.ts +19 -0
  83. package/dist/server/server/agent/timeline-projection.d.ts.map +1 -0
  84. package/dist/server/server/agent/timeline-projection.js +142 -0
  85. package/dist/server/server/agent/timeline-projection.js.map +1 -0
  86. package/dist/server/server/agent/tts-manager.d.ts +3 -2
  87. package/dist/server/server/agent/tts-manager.d.ts.map +1 -1
  88. package/dist/server/server/agent/tts-manager.js +5 -17
  89. package/dist/server/server/agent/tts-manager.js.map +1 -1
  90. package/dist/server/server/agent-attention-policy.d.ts +20 -0
  91. package/dist/server/server/agent-attention-policy.d.ts.map +1 -0
  92. package/dist/server/server/agent-attention-policy.js +40 -0
  93. package/dist/server/server/agent-attention-policy.js.map +1 -0
  94. package/dist/server/server/bootstrap.d.ts.map +1 -1
  95. package/dist/server/server/bootstrap.js +13 -20
  96. package/dist/server/server/bootstrap.js.map +1 -1
  97. package/dist/server/server/dictation/dictation-stream-manager.d.ts +10 -2
  98. package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -1
  99. package/dist/server/server/dictation/dictation-stream-manager.js +83 -27
  100. package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
  101. package/dist/server/server/exports.d.ts +2 -1
  102. package/dist/server/server/exports.d.ts.map +1 -1
  103. package/dist/server/server/exports.js +1 -0
  104. package/dist/server/server/exports.js.map +1 -1
  105. package/dist/server/server/persisted-config.d.ts +36 -22
  106. package/dist/server/server/persisted-config.d.ts.map +1 -1
  107. package/dist/server/server/persisted-config.js +2 -0
  108. package/dist/server/server/persisted-config.js.map +1 -1
  109. package/dist/server/server/relay-transport.d.ts +3 -2
  110. package/dist/server/server/relay-transport.d.ts.map +1 -1
  111. package/dist/server/server/relay-transport.js +99 -16
  112. package/dist/server/server/relay-transport.js.map +1 -1
  113. package/dist/server/server/session.d.ts +34 -16
  114. package/dist/server/server/session.d.ts.map +1 -1
  115. package/dist/server/server/session.js +619 -328
  116. package/dist/server/server/session.js.map +1 -1
  117. package/dist/server/server/speech/provider-resolver.d.ts +3 -0
  118. package/dist/server/server/speech/provider-resolver.d.ts.map +1 -0
  119. package/dist/server/server/speech/provider-resolver.js +7 -0
  120. package/dist/server/server/speech/provider-resolver.js.map +1 -0
  121. package/dist/server/server/speech/providers/local/config.d.ts.map +1 -1
  122. package/dist/server/server/speech/providers/local/config.js +11 -8
  123. package/dist/server/server/speech/providers/local/config.js.map +1 -1
  124. package/dist/server/server/speech/providers/local/runtime.d.ts +1 -0
  125. package/dist/server/server/speech/providers/local/runtime.d.ts.map +1 -1
  126. package/dist/server/server/speech/providers/local/runtime.js +13 -9
  127. package/dist/server/server/speech/providers/local/runtime.js.map +1 -1
  128. package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts.map +1 -1
  129. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +73 -54
  130. package/dist/server/server/speech/providers/local/sherpa/model-downloader.js.map +1 -1
  131. package/dist/server/server/speech/providers/openai/config.d.ts.map +1 -1
  132. package/dist/server/server/speech/providers/openai/config.js +10 -5
  133. package/dist/server/server/speech/providers/openai/config.js.map +1 -1
  134. package/dist/server/server/speech/providers/openai/runtime.d.ts.map +1 -1
  135. package/dist/server/server/speech/providers/openai/runtime.js +17 -6
  136. package/dist/server/server/speech/providers/openai/runtime.js.map +1 -1
  137. package/dist/server/server/speech/speech-config-resolver.d.ts.map +1 -1
  138. package/dist/server/server/speech/speech-config-resolver.js +25 -4
  139. package/dist/server/server/speech/speech-config-resolver.js.map +1 -1
  140. package/dist/server/server/speech/speech-runtime.d.ts +26 -3
  141. package/dist/server/server/speech/speech-runtime.d.ts.map +1 -1
  142. package/dist/server/server/speech/speech-runtime.js +468 -85
  143. package/dist/server/server/speech/speech-runtime.js.map +1 -1
  144. package/dist/server/server/speech/speech-types.d.ts +3 -0
  145. package/dist/server/server/speech/speech-types.d.ts.map +1 -1
  146. package/dist/server/server/speech/speech-types.js +1 -0
  147. package/dist/server/server/speech/speech-types.js.map +1 -1
  148. package/dist/server/server/websocket-server.d.ts +23 -7
  149. package/dist/server/server/websocket-server.d.ts.map +1 -1
  150. package/dist/server/server/websocket-server.js +288 -102
  151. package/dist/server/server/websocket-server.js.map +1 -1
  152. package/dist/server/shared/binary-mux.d.ts +31 -0
  153. package/dist/server/shared/binary-mux.d.ts.map +1 -0
  154. package/dist/server/shared/binary-mux.js +101 -0
  155. package/dist/server/shared/binary-mux.js.map +1 -0
  156. package/dist/server/shared/messages.d.ts +5206 -4814
  157. package/dist/server/shared/messages.d.ts.map +1 -1
  158. package/dist/server/shared/messages.js +225 -58
  159. package/dist/server/shared/messages.js.map +1 -1
  160. package/dist/server/shared/terminal-key-input.d.ts +9 -0
  161. package/dist/server/shared/terminal-key-input.d.ts.map +1 -0
  162. package/dist/server/shared/terminal-key-input.js +132 -0
  163. package/dist/server/shared/terminal-key-input.js.map +1 -0
  164. package/dist/server/terminal/terminal.d.ts +17 -0
  165. package/dist/server/terminal/terminal.d.ts.map +1 -1
  166. package/dist/server/terminal/terminal.js +89 -0
  167. package/dist/server/terminal/terminal.js.map +1 -1
  168. package/dist/server/utils/checkout-git.d.ts +9 -1
  169. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  170. package/dist/server/utils/checkout-git.js +314 -75
  171. package/dist/server/utils/checkout-git.js.map +1 -1
  172. package/dist/server/utils/worktree.d.ts.map +1 -1
  173. package/dist/server/utils/worktree.js +33 -4
  174. package/dist/server/utils/worktree.js.map +1 -1
  175. package/package.json +2 -2
@@ -6,7 +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 { parseAndHighlightDiff } from "./utils/diff-highlighter.js";
9
+ import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from "../shared/binary-mux.js";
10
10
  import { TTSManager } from "./agent/tts-manager.js";
11
11
  import { STTManager } from "./agent/stt-manager.js";
12
12
  import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
@@ -17,13 +17,14 @@ import { experimental_createMCPClient } from "ai";
17
17
  import { buildProviderRegistry } from "./agent/provider-registry.js";
18
18
  import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
19
19
  import { toAgentPayload } from "./agent/agent-projections.js";
20
+ import { projectTimelineRows } from "./agent/timeline-projection.js";
20
21
  import { StructuredAgentResponseError, generateStructuredAgentResponse, } from "./agent/agent-response-loop.js";
21
22
  import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
22
23
  import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
23
24
  import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
24
25
  import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
25
26
  import { createWorktree, runWorktreeSetupCommands, WorktreeSetupError, slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
26
- 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";
27
28
  import { getProjectIcon } from "../utils/project-icon.js";
28
29
  import { expandTilde } from "../utils/path.js";
29
30
  import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from "./speech/providers/local/models.js";
@@ -40,6 +41,9 @@ const PROJECT_PLACEMENT_CACHE_TTL_MS = 10000;
40
41
  const MAX_AGENTS_PER_PROJECT = 5;
41
42
  const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
42
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;
43
47
  /**
44
48
  * Default model used for auto-generating commit messages and PR descriptions.
45
49
  * Uses Claude Haiku for speed and cost efficiency.
@@ -117,6 +121,15 @@ const VOICE_INTERNAL_DICTATION_ID_PREFIX = "__voice_turn__:";
117
121
  const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
118
122
  const AgentIdSchema = z.string().uuid();
119
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
+ }
120
133
  function convertPCMToWavBuffer(pcmBuffer, sampleRate, channels, bitsPerSample) {
121
134
  const headerSize = 44;
122
135
  const wavBuffer = Buffer.alloc(headerSize + pcmBuffer.length);
@@ -200,14 +213,18 @@ export class Session {
200
213
  this.clientActivity = null;
201
214
  this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
202
215
  this.terminalSubscriptions = new Map();
216
+ this.terminalStreams = new Map();
217
+ this.terminalStreamByTerminalId = new Map();
218
+ this.nextTerminalStreamId = 1;
203
219
  this.checkoutDiffSubscriptions = new Map();
204
220
  this.checkoutDiffTargets = new Map();
205
221
  this.voiceModeAgentId = null;
206
222
  this.voiceModeBaseConfig = null;
207
- 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;
208
224
  this.clientId = clientId;
209
225
  this.sessionId = uuidv4();
210
226
  this.onMessage = onMessage;
227
+ this.onBinaryMessage = onBinaryMessage ?? null;
211
228
  this.downloadTokenStore = downloadTokenStore;
212
229
  this.pushTokenStore = pushTokenStore;
213
230
  this.paseoHome = paseoHome;
@@ -231,6 +248,7 @@ export class Session {
231
248
  this.unregisterVoiceCallerContext = voiceBridge?.unregisterVoiceCallerContext;
232
249
  this.ensureVoiceMcpSocketForAgent = voiceBridge?.ensureVoiceMcpSocketForAgent;
233
250
  this.removeVoiceMcpSocketForAgent = voiceBridge?.removeVoiceMcpSocketForAgent;
251
+ this.getSpeechReadiness = dictation?.getSpeechReadiness;
234
252
  this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
235
253
  this.abortController = new AbortController();
236
254
  this.sessionLogger = logger.child({
@@ -261,7 +279,7 @@ export class Session {
261
279
  // Initialize agent MCP client asynchronously
262
280
  void this.initializeAgentMcp();
263
281
  this.subscribeToAgentEvents();
264
- this.sessionLogger.info("Session created");
282
+ this.sessionLogger.trace("Session created");
265
283
  }
266
284
  /**
267
285
  * Get the client's current activity state
@@ -331,11 +349,11 @@ export class Session {
331
349
  /**
332
350
  * Start streaming an agent run and forward results via the websocket broadcast
333
351
  */
334
- startAgentStream(agentId, prompt) {
352
+ startAgentStream(agentId, prompt, runOptions) {
335
353
  this.sessionLogger.info({ agentId }, `Starting agent stream for ${agentId}`);
336
354
  let iterator;
337
355
  try {
338
- iterator = this.agentManager.streamAgent(agentId, prompt);
356
+ iterator = this.agentManager.streamAgent(agentId, prompt, runOptions);
339
357
  }
340
358
  catch (error) {
341
359
  this.handleAgentRunError(agentId, error, "Failed to start agent run");
@@ -379,7 +397,7 @@ export class Session {
379
397
  });
380
398
  this.agentTools = (await this.agentMcpClient.tools());
381
399
  const agentToolCount = Object.keys(this.agentTools ?? {}).length;
382
- this.sessionLogger.info({ agentToolCount }, `Agent MCP initialized with ${agentToolCount} tools`);
400
+ this.sessionLogger.trace({ agentToolCount }, `Agent MCP initialized with ${agentToolCount} tools`);
383
401
  }
384
402
  catch (error) {
385
403
  this.sessionLogger.error({ err: error }, "Failed to initialize Agent MCP");
@@ -416,9 +434,7 @@ export class Session {
416
434
  }
417
435
  // Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
418
436
  // for the focused agent, with a short grace window while backgrounded.
419
- //
420
- // History catch-up is handled via explicit `initialize_agent_request` which emits a
421
- // batched `agent_stream_snapshot`.
437
+ // History catch-up is handled via pull-based `fetch_agent_timeline_request`.
422
438
  const activity = this.clientActivity;
423
439
  if (activity?.deviceType === "mobile") {
424
440
  if (!activity.focusedAgentId) {
@@ -442,6 +458,8 @@ export class Session {
442
458
  agentId: event.agentId,
443
459
  event: serializedEvent,
444
460
  timestamp: new Date().toISOString(),
461
+ ...(typeof event.seq === "number" ? { seq: event.seq } : {}),
462
+ ...(typeof event.epoch === "string" ? { epoch: event.epoch } : {}),
445
463
  };
446
464
  this.emit({
447
465
  type: "agent_stream",
@@ -534,10 +552,12 @@ export class Session {
534
552
  let snapshot;
535
553
  if (handle) {
536
554
  snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, extractTimestamps(record));
555
+ this.sessionLogger.info({ agentId, provider: record.provider }, "Agent resumed from persistence");
537
556
  }
538
557
  else {
539
558
  const config = buildSessionConfig(record);
540
559
  snapshot = await this.agentManager.createAgent(config, agentId, { labels: record.labels });
560
+ this.sessionLogger.info({ agentId, provider: record.provider }, "Agent created from stored config");
541
561
  }
542
562
  await this.agentManager.hydrateTimelineFromProvider(agentId);
543
563
  return this.agentManager.getAgent(agentId) ?? snapshot;
@@ -685,6 +705,9 @@ export class Session {
685
705
  case "archive_agent_request":
686
706
  await this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
687
707
  break;
708
+ case "update_agent_request":
709
+ await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
710
+ break;
688
711
  case "set_voice_mode":
689
712
  await this.handleSetVoiceMode(msg.enabled, msg.agentId, msg.requestId);
690
713
  break;
@@ -695,6 +718,22 @@ export class Session {
695
718
  await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
696
719
  break;
697
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
+ }
698
737
  await this.dictationStreamManager.handleStart(msg.dictationId, msg.format);
699
738
  break;
700
739
  case "dictation_stream_chunk":
@@ -726,8 +765,8 @@ export class Session {
726
765
  case "restart_server_request":
727
766
  await this.handleRestartServerRequest(msg.requestId, msg.reason);
728
767
  break;
729
- case "initialize_agent_request":
730
- await this.handleInitializeAgentRequest(msg.agentId, msg.requestId);
768
+ case "fetch_agent_timeline_request":
769
+ await this.handleFetchAgentTimelineRequest(msg);
731
770
  break;
732
771
  case "set_agent_mode_request":
733
772
  await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId);
@@ -741,15 +780,15 @@ export class Session {
741
780
  case "agent_permission_response":
742
781
  await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response);
743
782
  break;
744
- case "git_diff_request":
745
- await this.handleGitDiffRequest(msg.agentId, msg.requestId);
746
- break;
747
783
  case "checkout_status_request":
748
784
  await this.handleCheckoutStatusRequest(msg);
749
785
  break;
750
786
  case "validate_branch_request":
751
787
  await this.handleValidateBranchRequest(msg);
752
788
  break;
789
+ case "branch_suggestions_request":
790
+ await this.handleBranchSuggestionsRequest(msg);
791
+ break;
753
792
  case "subscribe_checkout_diff_request":
754
793
  await this.handleSubscribeCheckoutDiffRequest(msg);
755
794
  break;
@@ -780,9 +819,6 @@ export class Session {
780
819
  case "paseo_worktree_archive_request":
781
820
  await this.handlePaseoWorktreeArchiveRequest(msg);
782
821
  break;
783
- case "highlighted_diff_request":
784
- await this.handleHighlightedDiffRequest(msg.agentId, msg.requestId);
785
- break;
786
822
  case "file_explorer_request":
787
823
  await this.handleFileExplorerRequest(msg);
788
824
  break;
@@ -795,6 +831,9 @@ export class Session {
795
831
  case "list_provider_models_request":
796
832
  await this.handleListProviderModelsRequest(msg);
797
833
  break;
834
+ case "list_available_providers_request":
835
+ await this.handleListAvailableProvidersRequest(msg);
836
+ break;
798
837
  case "speech_models_list_request":
799
838
  await this.handleSpeechModelsListRequest(msg);
800
839
  break;
@@ -847,6 +886,12 @@ export class Session {
847
886
  case "kill_terminal_request":
848
887
  await this.handleKillTerminalRequest(msg);
849
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;
850
895
  }
851
896
  }
852
897
  catch (error) {
@@ -880,6 +925,58 @@ export class Session {
880
925
  });
881
926
  }
882
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
+ }
883
980
  async handleRestartServerRequest(requestId, reason) {
884
981
  if (restartRequested) {
885
982
  this.sessionLogger.debug("Restart already requested, ignoring duplicate");
@@ -965,12 +1062,123 @@ export class Session {
965
1062
  },
966
1063
  });
967
1064
  }
1065
+ async handleUpdateAgentRequest(agentId, name, labels, requestId) {
1066
+ this.sessionLogger.info({ agentId, requestId, hasName: typeof name === "string", labelCount: labels ? Object.keys(labels).length : 0 }, "session: update_agent_request");
1067
+ const normalizedName = name?.trim();
1068
+ const normalizedLabels = labels && Object.keys(labels).length > 0 ? labels : undefined;
1069
+ if (!normalizedName && !normalizedLabels) {
1070
+ this.emit({
1071
+ type: "update_agent_response",
1072
+ payload: {
1073
+ requestId,
1074
+ agentId,
1075
+ accepted: false,
1076
+ error: "Nothing to update (provide name and/or labels)",
1077
+ },
1078
+ });
1079
+ return;
1080
+ }
1081
+ try {
1082
+ const liveAgent = this.agentManager.getAgent(agentId);
1083
+ if (liveAgent) {
1084
+ if (normalizedName) {
1085
+ await this.agentManager.setTitle(agentId, normalizedName);
1086
+ }
1087
+ if (normalizedLabels) {
1088
+ await this.agentManager.setLabels(agentId, normalizedLabels);
1089
+ }
1090
+ }
1091
+ else {
1092
+ const existing = await this.agentStorage.get(agentId);
1093
+ if (!existing) {
1094
+ throw new Error(`Agent not found: ${agentId}`);
1095
+ }
1096
+ await this.agentStorage.upsert({
1097
+ ...existing,
1098
+ ...(normalizedName ? { title: normalizedName } : {}),
1099
+ ...(normalizedLabels
1100
+ ? { labels: { ...existing.labels, ...normalizedLabels } }
1101
+ : {}),
1102
+ });
1103
+ }
1104
+ this.emit({
1105
+ type: "update_agent_response",
1106
+ payload: { requestId, agentId, accepted: true, error: null },
1107
+ });
1108
+ }
1109
+ catch (error) {
1110
+ this.sessionLogger.error({ err: error, agentId, requestId }, "session: update_agent_request error");
1111
+ this.emit({
1112
+ type: "activity_log",
1113
+ payload: {
1114
+ id: uuidv4(),
1115
+ timestamp: new Date(),
1116
+ type: "error",
1117
+ content: `Failed to update agent: ${error.message}`,
1118
+ },
1119
+ });
1120
+ this.emit({
1121
+ type: "update_agent_response",
1122
+ payload: {
1123
+ requestId,
1124
+ agentId,
1125
+ accepted: false,
1126
+ error: error?.message ? String(error.message) : "Failed to update agent",
1127
+ },
1128
+ });
1129
+ }
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
+ }
968
1172
  /**
969
1173
  * Handle voice mode toggle
970
1174
  */
971
1175
  async handleSetVoiceMode(enabled, agentId, requestId) {
972
1176
  try {
973
1177
  if (enabled) {
1178
+ const unavailable = this.resolveVoiceFeatureUnavailableContext("voice_mode");
1179
+ if (unavailable) {
1180
+ throw new VoiceFeatureUnavailableError(unavailable);
1181
+ }
974
1182
  const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? "", "set_voice_mode");
975
1183
  if (this.isVoiceMode &&
976
1184
  this.voiceModeAgentId &&
@@ -1017,6 +1225,7 @@ export class Session {
1017
1225
  }
1018
1226
  catch (error) {
1019
1227
  const errorMessage = error instanceof Error ? error.message : "Failed to set voice mode";
1228
+ const unavailable = this.getVoiceFeatureUnavailableResponseMetadata(error);
1020
1229
  this.sessionLogger.error({
1021
1230
  err: error,
1022
1231
  enabled,
@@ -1031,6 +1240,7 @@ export class Session {
1031
1240
  agentId: this.voiceModeAgentId,
1032
1241
  accepted: false,
1033
1242
  error: errorMessage,
1243
+ ...unavailable,
1034
1244
  },
1035
1245
  });
1036
1246
  return;
@@ -1318,7 +1528,7 @@ export class Session {
1318
1528
  /**
1319
1529
  * Handle text message to agent (with optional image attachments)
1320
1530
  */
1321
- async handleSendAgentMessage(agentId, text, messageId, images) {
1531
+ async handleSendAgentMessage(agentId, text, messageId, images, runOptions) {
1322
1532
  this.sessionLogger.info({ agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 }, `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ''}`);
1323
1533
  try {
1324
1534
  await this.ensureAgentLoaded(agentId);
@@ -1341,46 +1551,13 @@ export class Session {
1341
1551
  catch (error) {
1342
1552
  this.sessionLogger.error({ err: error, agentId }, `Failed to record user message for agent ${agentId}`);
1343
1553
  }
1344
- this.startAgentStream(agentId, prompt);
1345
- }
1346
- /**
1347
- * Handle on-demand agent initialization request from client
1348
- */
1349
- async handleInitializeAgentRequest(agentId, requestId) {
1350
- this.sessionLogger.info({ agentId }, `Initializing agent ${agentId} on demand`);
1351
- try {
1352
- const snapshot = await this.ensureAgentLoaded(agentId);
1353
- await this.forwardAgentUpdate(snapshot);
1354
- // Send timeline snapshot after hydration (if any)
1355
- const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
1356
- this.emit({
1357
- type: "initialize_agent_request",
1358
- payload: {
1359
- agentId,
1360
- agentStatus: snapshot.lifecycle,
1361
- timelineSize,
1362
- requestId,
1363
- },
1364
- });
1365
- this.sessionLogger.info({ agentId, timelineSize, status: snapshot.lifecycle }, `Agent ${agentId} initialized with ${timelineSize} timeline item(s); status=${snapshot.lifecycle}`);
1366
- }
1367
- catch (error) {
1368
- this.sessionLogger.error({ err: error, agentId }, `Failed to initialize agent ${agentId}`);
1369
- this.emit({
1370
- type: "initialize_agent_request",
1371
- payload: {
1372
- agentId,
1373
- requestId,
1374
- error: error?.message ?? "Failed to initialize agent",
1375
- },
1376
- });
1377
- }
1554
+ this.startAgentStream(agentId, prompt, runOptions);
1378
1555
  }
1379
1556
  /**
1380
1557
  * Handle create agent request
1381
1558
  */
1382
1559
  async handleCreateAgentRequest(msg) {
1383
- const { config, worktreeName, requestId, initialPrompt, git, images, labels } = msg;
1560
+ const { config, worktreeName, requestId, initialPrompt, outputSchema, git, images, labels } = msg;
1384
1561
  this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ""}`);
1385
1562
  try {
1386
1563
  const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
@@ -1398,7 +1575,7 @@ export class Session {
1398
1575
  logger: this.sessionLogger,
1399
1576
  });
1400
1577
  try {
1401
- await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, uuidv4(), images);
1578
+ await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, uuidv4(), images, outputSchema ? { outputSchema } : undefined);
1402
1579
  }
1403
1580
  catch (promptError) {
1404
1581
  this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
@@ -1476,7 +1653,7 @@ export class Session {
1476
1653
  const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
1477
1654
  await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
1478
1655
  await this.forwardAgentUpdate(snapshot);
1479
- const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
1656
+ const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
1480
1657
  if (requestId) {
1481
1658
  const agentPayload = await this.getAgentPayloadById(snapshot.id);
1482
1659
  if (!agentPayload) {
@@ -1535,7 +1712,7 @@ export class Session {
1535
1712
  }
1536
1713
  await this.agentManager.hydrateTimelineFromProvider(agentId);
1537
1714
  await this.forwardAgentUpdate(snapshot);
1538
- const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
1715
+ const timelineSize = this.agentManager.getTimeline(agentId).length;
1539
1716
  if (requestId) {
1540
1717
  this.emit({
1541
1718
  type: "status",
@@ -1752,6 +1929,33 @@ export class Session {
1752
1929
  });
1753
1930
  }
1754
1931
  }
1932
+ async handleListAvailableProvidersRequest(msg) {
1933
+ const fetchedAt = new Date().toISOString();
1934
+ try {
1935
+ const providers = await this.agentManager.listProviderAvailability();
1936
+ this.emit({
1937
+ type: "list_available_providers_response",
1938
+ payload: {
1939
+ providers,
1940
+ error: null,
1941
+ fetchedAt,
1942
+ requestId: msg.requestId,
1943
+ },
1944
+ });
1945
+ }
1946
+ catch (error) {
1947
+ this.sessionLogger.error({ err: error }, "Failed to list provider availability");
1948
+ this.emit({
1949
+ type: "list_available_providers_response",
1950
+ payload: {
1951
+ providers: [],
1952
+ error: error?.message ?? String(error),
1953
+ fetchedAt,
1954
+ requestId: msg.requestId,
1955
+ },
1956
+ });
1957
+ }
1958
+ }
1755
1959
  async handleSpeechModelsListRequest(msg) {
1756
1960
  const modelsDir = this.localSpeechModelsDir;
1757
1961
  const models = await Promise.all(listLocalSpeechModels().map(async (model) => {
@@ -2377,52 +2581,6 @@ export class Session {
2377
2581
  throw error;
2378
2582
  }
2379
2583
  }
2380
- /**
2381
- * Handle git diff request for an agent
2382
- */
2383
- async handleGitDiffRequest(agentId, requestId) {
2384
- this.sessionLogger.debug({ agentId }, `Handling git diff request for agent ${agentId}`);
2385
- try {
2386
- const agents = this.agentManager.listAgents();
2387
- const agent = agents.find((a) => a.id === agentId);
2388
- if (!agent) {
2389
- this.emit({
2390
- type: "git_diff_response",
2391
- payload: {
2392
- agentId,
2393
- diff: "",
2394
- error: `Agent not found: ${agentId}`,
2395
- requestId,
2396
- },
2397
- });
2398
- return;
2399
- }
2400
- const diffResult = await getCheckoutDiff(agent.cwd, { mode: "uncommitted" }, { paseoHome: this.paseoHome });
2401
- const combinedDiff = diffResult.diff;
2402
- this.emit({
2403
- type: "git_diff_response",
2404
- payload: {
2405
- agentId,
2406
- diff: combinedDiff,
2407
- error: null,
2408
- requestId,
2409
- },
2410
- });
2411
- this.sessionLogger.debug({ agentId, diffBytes: combinedDiff.length }, `Git diff for agent ${agentId} completed (${combinedDiff.length} bytes)`);
2412
- }
2413
- catch (error) {
2414
- this.sessionLogger.error({ err: error, agentId }, `Failed to get git diff for agent ${agentId}`);
2415
- this.emit({
2416
- type: "git_diff_response",
2417
- payload: {
2418
- agentId,
2419
- diff: "",
2420
- error: error.message,
2421
- requestId,
2422
- },
2423
- });
2424
- }
2425
- }
2426
2584
  async handleCheckoutStatusRequest(msg) {
2427
2585
  const { cwd, requestId } = msg;
2428
2586
  try {
@@ -2581,6 +2739,31 @@ export class Session {
2581
2739
  });
2582
2740
  }
2583
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
+ }
2584
2767
  normalizeCheckoutDiffCompare(compare) {
2585
2768
  if (compare.mode === "uncommitted") {
2586
2769
  return { mode: "uncommitted" };
@@ -2752,30 +2935,42 @@ export class Session {
2752
2935
  latestPayload: null,
2753
2936
  latestFingerprint: null,
2754
2937
  };
2755
- const watchPaths = new Set([cwd]);
2756
- if (watchRoot) {
2757
- watchPaths.add(watchRoot);
2758
- }
2938
+ const repoWatchPath = watchRoot ?? cwd;
2939
+ const watchPaths = new Set([repoWatchPath]);
2759
2940
  const gitDir = await this.resolveCheckoutGitDir(cwd);
2760
2941
  if (gitDir) {
2761
2942
  watchPaths.add(gitDir);
2762
2943
  }
2763
- let hasWatchRootCoverage = false;
2944
+ let hasRecursiveRepoCoverage = false;
2945
+ const allowRecursiveRepoWatch = process.platform !== "linux";
2764
2946
  for (const watchPath of watchPaths) {
2947
+ const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
2765
2948
  const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
2766
2949
  this.scheduleCheckoutDiffTargetRefresh(target);
2767
2950
  });
2768
2951
  let watcher = null;
2952
+ let watcherIsRecursive = false;
2769
2953
  try {
2770
- watcher = createWatcher(true);
2954
+ if (shouldTryRecursive) {
2955
+ watcher = createWatcher(true);
2956
+ watcherIsRecursive = true;
2957
+ }
2958
+ else {
2959
+ watcher = createWatcher(false);
2960
+ }
2771
2961
  }
2772
2962
  catch (error) {
2773
- try {
2774
- watcher = createWatcher(false);
2775
- 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
+ }
2776
2971
  }
2777
- catch (fallbackError) {
2778
- 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");
2779
2974
  }
2780
2975
  }
2781
2976
  if (!watcher) {
@@ -2785,11 +2980,11 @@ export class Session {
2785
2980
  this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff watcher error");
2786
2981
  });
2787
2982
  target.watchers.push(watcher);
2788
- if (watchRoot && watchPath === watchRoot) {
2789
- hasWatchRootCoverage = true;
2983
+ if (watchPath === repoWatchPath && watcherIsRecursive) {
2984
+ hasRecursiveRepoCoverage = true;
2790
2985
  }
2791
2986
  }
2792
- const missingRepoCoverage = Boolean(watchRoot) && !hasWatchRootCoverage;
2987
+ const missingRepoCoverage = !hasRecursiveRepoCoverage;
2793
2988
  if (target.watchers.length === 0 || missingRepoCoverage) {
2794
2989
  target.fallbackRefreshInterval = setInterval(() => {
2795
2990
  this.scheduleCheckoutDiffTargetRefresh(target);
@@ -2800,7 +2995,7 @@ export class Session {
2800
2995
  intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
2801
2996
  reason: target.watchers.length === 0
2802
2997
  ? "no_watchers"
2803
- : "missing_repo_root_coverage",
2998
+ : "missing_recursive_repo_root_coverage",
2804
2999
  }, "Checkout diff watchers unavailable; using timed refresh fallback");
2805
3000
  }
2806
3001
  this.checkoutDiffTargets.set(targetKey, target);
@@ -3051,12 +3246,13 @@ export class Session {
3051
3246
  async handleCheckoutPrStatusRequest(msg) {
3052
3247
  const { cwd, requestId } = msg;
3053
3248
  try {
3054
- const status = await getPullRequestStatus(cwd);
3249
+ const prStatus = await getPullRequestStatus(cwd);
3055
3250
  this.emit({
3056
3251
  type: "checkout_pr_status_response",
3057
3252
  payload: {
3058
3253
  cwd,
3059
- status,
3254
+ status: prStatus.status,
3255
+ githubFeaturesEnabled: prStatus.githubFeaturesEnabled,
3060
3256
  error: null,
3061
3257
  requestId,
3062
3258
  },
@@ -3068,6 +3264,7 @@ export class Session {
3068
3264
  payload: {
3069
3265
  cwd,
3070
3266
  status: null,
3267
+ githubFeaturesEnabled: true,
3071
3268
  error: this.toCheckoutError(error),
3072
3269
  requestId,
3073
3270
  },
@@ -3223,185 +3420,6 @@ export class Session {
3223
3420
  });
3224
3421
  }
3225
3422
  }
3226
- /**
3227
- * Handle highlighted diff request - returns parsed and syntax-highlighted diff
3228
- */
3229
- async handleHighlightedDiffRequest(agentId, requestId) {
3230
- this.sessionLogger.debug({ agentId }, `Handling highlighted diff request for agent ${agentId}`);
3231
- // Maximum lines changed before we skip showing the diff content
3232
- const MAX_DIFF_LINES = 5000;
3233
- try {
3234
- const agents = this.agentManager.listAgents();
3235
- const agent = agents.find((a) => a.id === agentId);
3236
- if (!agent) {
3237
- this.emit({
3238
- type: "highlighted_diff_response",
3239
- payload: {
3240
- agentId,
3241
- files: [],
3242
- error: `Agent not found: ${agentId}`,
3243
- requestId,
3244
- },
3245
- });
3246
- return;
3247
- }
3248
- // Step 1: Get the list of changed files with their stats (numstat gives additions/deletions per file)
3249
- const { stdout: numstatOutput } = await execAsync("git diff --numstat HEAD", { cwd: agent.cwd });
3250
- // Get file statuses (A=added, D=deleted, M=modified) to detect deleted files
3251
- const { stdout: nameStatusOutput } = await execAsync("git diff --name-status HEAD", { cwd: agent.cwd });
3252
- const deletedFiles = new Set();
3253
- const addedFiles = new Set();
3254
- for (const line of nameStatusOutput.trim().split("\n").filter(Boolean)) {
3255
- const [status, ...pathParts] = line.split("\t");
3256
- const path = pathParts.join("\t");
3257
- if (status === "D") {
3258
- deletedFiles.add(path);
3259
- }
3260
- else if (status === "A") {
3261
- addedFiles.add(path);
3262
- }
3263
- }
3264
- const fileStats = [];
3265
- for (const line of numstatOutput.trim().split("\n").filter(Boolean)) {
3266
- const parts = line.split("\t");
3267
- if (parts.length >= 3) {
3268
- const [addStr, delStr, ...pathParts] = parts;
3269
- const path = pathParts.join("\t"); // Handle paths with tabs
3270
- const isBinary = addStr === "-" && delStr === "-";
3271
- fileStats.push({
3272
- path,
3273
- additions: isBinary ? 0 : parseInt(addStr, 10),
3274
- deletions: isBinary ? 0 : parseInt(delStr, 10),
3275
- isBinary,
3276
- isTracked: true,
3277
- isDeleted: deletedFiles.has(path),
3278
- isNew: addedFiles.has(path),
3279
- });
3280
- }
3281
- }
3282
- // Step 2: Get untracked files
3283
- try {
3284
- const { stdout: untrackedFiles } = await execAsync("git ls-files --others --exclude-standard", { cwd: agent.cwd });
3285
- for (const filePath of untrackedFiles.trim().split("\n").filter(Boolean)) {
3286
- // Use git's numstat with --no-index to detect binary files (cross-platform)
3287
- // Binary files show as "-\t-\tfilepath", text files show line counts
3288
- try {
3289
- const { stdout: numstatLine } = await execAsync(`git diff --numstat --no-index /dev/null "${filePath}" || true`, { cwd: agent.cwd });
3290
- const parts = numstatLine.trim().split("\t");
3291
- const isBinary = parts[0] === "-" && parts[1] === "-";
3292
- const additions = isBinary ? 0 : (parseInt(parts[0], 10) || 0);
3293
- fileStats.push({
3294
- path: filePath,
3295
- additions,
3296
- deletions: 0,
3297
- isBinary,
3298
- isTracked: false,
3299
- isDeleted: false,
3300
- isNew: true,
3301
- });
3302
- }
3303
- catch {
3304
- // If we can't determine, assume text and try to get it
3305
- fileStats.push({
3306
- path: filePath,
3307
- additions: 0,
3308
- deletions: 0,
3309
- isBinary: false,
3310
- isTracked: false,
3311
- isDeleted: false,
3312
- isNew: true,
3313
- });
3314
- }
3315
- }
3316
- }
3317
- catch {
3318
- // Ignore errors getting untracked files
3319
- }
3320
- // Step 3: Fetch diffs per-file, respecting limits
3321
- const allFiles = [];
3322
- for (const stats of fileStats) {
3323
- const totalLines = stats.additions + stats.deletions;
3324
- // Handle binary files
3325
- if (stats.isBinary) {
3326
- allFiles.push({
3327
- path: stats.path,
3328
- isNew: stats.isNew,
3329
- isDeleted: stats.isDeleted,
3330
- additions: 0,
3331
- deletions: 0,
3332
- hunks: [],
3333
- status: "binary",
3334
- });
3335
- continue;
3336
- }
3337
- // Handle files that are too large
3338
- if (totalLines > MAX_DIFF_LINES) {
3339
- allFiles.push({
3340
- path: stats.path,
3341
- isNew: stats.isNew,
3342
- isDeleted: stats.isDeleted,
3343
- additions: stats.additions,
3344
- deletions: stats.deletions,
3345
- hunks: [],
3346
- status: "too_large",
3347
- });
3348
- continue;
3349
- }
3350
- // Fetch the actual diff for this file
3351
- try {
3352
- let fileDiff;
3353
- if (stats.isTracked) {
3354
- const { stdout } = await execAsync(`git diff HEAD -- "${stats.path}"`, { cwd: agent.cwd });
3355
- fileDiff = stdout;
3356
- }
3357
- else {
3358
- const { stdout } = await execAsync(`git diff --no-index /dev/null "${stats.path}" || true`, { cwd: agent.cwd });
3359
- fileDiff = stdout;
3360
- }
3361
- if (fileDiff) {
3362
- const parsedFiles = await parseAndHighlightDiff(fileDiff, agent.cwd);
3363
- for (const file of parsedFiles) {
3364
- allFiles.push({ ...file, status: "ok" });
3365
- }
3366
- }
3367
- }
3368
- catch {
3369
- // If diff fails for this file, add it with empty hunks
3370
- allFiles.push({
3371
- path: stats.path,
3372
- isNew: stats.isNew,
3373
- isDeleted: stats.isDeleted,
3374
- additions: stats.additions,
3375
- deletions: stats.deletions,
3376
- hunks: [],
3377
- status: "ok",
3378
- });
3379
- }
3380
- }
3381
- this.emit({
3382
- type: "highlighted_diff_response",
3383
- payload: {
3384
- agentId,
3385
- files: allFiles,
3386
- error: null,
3387
- requestId,
3388
- },
3389
- });
3390
- this.sessionLogger.debug({ agentId, fileCount: allFiles.length }, `Highlighted diff for agent ${agentId} completed (${allFiles.length} files)`);
3391
- }
3392
- catch (error) {
3393
- this.sessionLogger.error({ err: error, agentId }, `Failed to get highlighted diff for agent ${agentId}`);
3394
- this.emit({
3395
- type: "highlighted_diff_response",
3396
- payload: {
3397
- agentId,
3398
- files: [],
3399
- error: error.message,
3400
- requestId,
3401
- },
3402
- });
3403
- }
3404
- }
3405
3423
  /**
3406
3424
  * Handle read-only file explorer requests scoped to an agent's cwd
3407
3425
  */
@@ -3737,6 +3755,77 @@ export class Session {
3737
3755
  payload: { requestId, agent, error: null },
3738
3756
  });
3739
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
+ }
3740
3829
  async handleSendAgentMessageRequest(msg) {
3741
3830
  const resolved = await this.resolveAgentIdentifier(msg.agentId);
3742
3831
  if (!resolved.ok) {
@@ -3825,7 +3914,7 @@ export class Session {
3825
3914
  if (!resolved.ok) {
3826
3915
  this.emit({
3827
3916
  type: "wait_for_finish_response",
3828
- payload: { requestId, status: "error", final: null, error: resolved.error },
3917
+ payload: { requestId, status: "error", final: null, error: resolved.error, lastMessage: null },
3829
3918
  });
3830
3919
  return;
3831
3920
  }
@@ -3841,6 +3930,7 @@ export class Session {
3841
3930
  status: "error",
3842
3931
  final: null,
3843
3932
  error: `Agent not found: ${agentId}`,
3933
+ lastMessage: null,
3844
3934
  },
3845
3935
  });
3846
3936
  return;
@@ -3853,7 +3943,7 @@ export class Session {
3853
3943
  : "idle";
3854
3944
  this.emit({
3855
3945
  type: "wait_for_finish_response",
3856
- payload: { requestId, status, final, error: null },
3946
+ payload: { requestId, status, final, error: null, lastMessage: null },
3857
3947
  });
3858
3948
  return;
3859
3949
  }
@@ -3877,7 +3967,7 @@ export class Session {
3877
3967
  : "idle";
3878
3968
  this.emit({
3879
3969
  type: "wait_for_finish_response",
3880
- payload: { requestId, status, final, error: null },
3970
+ payload: { requestId, status, final, error: null, lastMessage: result.lastMessage },
3881
3971
  });
3882
3972
  }
3883
3973
  catch (error) {
@@ -3894,6 +3984,7 @@ export class Session {
3894
3984
  status: "error",
3895
3985
  final,
3896
3986
  error: message,
3987
+ lastMessage: null,
3897
3988
  },
3898
3989
  });
3899
3990
  return;
@@ -3904,37 +3995,13 @@ export class Session {
3904
3995
  }
3905
3996
  this.emit({
3906
3997
  type: "wait_for_finish_response",
3907
- payload: { requestId, status: "timeout", final, error: null },
3998
+ payload: { requestId, status: "timeout", final, error: null, lastMessage: null },
3908
3999
  });
3909
4000
  }
3910
4001
  finally {
3911
4002
  clearTimeout(timeoutHandle);
3912
4003
  }
3913
4004
  }
3914
- emitAgentTimelineSnapshot(agent) {
3915
- const timeline = this.agentManager.getTimeline(agent.id);
3916
- const events = timeline.flatMap((item) => {
3917
- const serializedEvent = serializeAgentStreamEvent({
3918
- type: "timeline",
3919
- provider: agent.provider,
3920
- item,
3921
- });
3922
- if (!serializedEvent) {
3923
- return [];
3924
- }
3925
- return [
3926
- {
3927
- event: serializedEvent,
3928
- timestamp: new Date().toISOString(),
3929
- },
3930
- ];
3931
- });
3932
- this.emit({
3933
- type: "agent_stream_snapshot",
3934
- payload: { agentId: agent.id, events },
3935
- });
3936
- return timeline.length;
3937
- }
3938
4005
  /**
3939
4006
  * Handle audio chunk for buffering and transcription
3940
4007
  */
@@ -4386,11 +4453,22 @@ export class Session {
4386
4453
  }
4387
4454
  this.onMessage(msg);
4388
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
+ }
4389
4467
  /**
4390
4468
  * Clean up session resources
4391
4469
  */
4392
4470
  async cleanup() {
4393
- this.sessionLogger.info("Cleaning up");
4471
+ this.sessionLogger.trace("Cleaning up");
4394
4472
  if (this.unsubscribeAgentEvents) {
4395
4473
  this.unsubscribeAgentEvents();
4396
4474
  this.unsubscribeAgentEvents = null;
@@ -4428,6 +4506,7 @@ export class Session {
4428
4506
  unsubscribe();
4429
4507
  }
4430
4508
  this.terminalSubscriptions.clear();
4509
+ this.detachAllTerminalStreams({ emitExit: false });
4431
4510
  for (const target of this.checkoutDiffTargets.values()) {
4432
4511
  this.closeCheckoutDiffWatchTarget(target);
4433
4512
  }
@@ -4601,6 +4680,10 @@ export class Session {
4601
4680
  unsubscribe();
4602
4681
  this.terminalSubscriptions.delete(msg.terminalId);
4603
4682
  }
4683
+ const streamId = this.terminalStreamByTerminalId.get(msg.terminalId);
4684
+ if (typeof streamId === "number") {
4685
+ this.detachTerminalStream(streamId, { emitExit: true });
4686
+ }
4604
4687
  this.terminalManager.killTerminal(msg.terminalId);
4605
4688
  this.emit({
4606
4689
  type: "kill_terminal_response",
@@ -4611,5 +4694,213 @@ export class Session {
4611
4694
  },
4612
4695
  });
4613
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
+ }
4614
4905
  }
4615
4906
  //# sourceMappingURL=session.js.map