@getpaseo/server 0.1.12 → 0.1.14

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 (65) hide show
  1. package/dist/server/client/daemon-client.d.ts +84 -87
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +253 -299
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-manager.d.ts +2 -1
  6. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  7. package/dist/server/server/agent/agent-manager.js +23 -0
  8. package/dist/server/server/agent/agent-manager.js.map +1 -1
  9. package/dist/server/server/agent/agent-sdk-types.d.ts +0 -15
  10. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  11. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  12. package/dist/server/server/agent/providers/claude-agent.js +268 -35
  13. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  14. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  15. package/dist/server/server/agent/providers/codex-app-server-agent.js +85 -36
  16. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  17. package/dist/server/server/bootstrap.d.ts.map +1 -1
  18. package/dist/server/server/bootstrap.js +3 -1
  19. package/dist/server/server/bootstrap.js.map +1 -1
  20. package/dist/server/server/daemon-version.d.ts +5 -0
  21. package/dist/server/server/daemon-version.d.ts.map +1 -0
  22. package/dist/server/server/daemon-version.js +22 -0
  23. package/dist/server/server/daemon-version.js.map +1 -0
  24. package/dist/server/server/dictation/dictation-stream-manager.d.ts +1 -0
  25. package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -1
  26. package/dist/server/server/dictation/dictation-stream-manager.js +32 -1
  27. package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
  28. package/dist/server/server/package-version.d.ts +13 -0
  29. package/dist/server/server/package-version.d.ts.map +1 -0
  30. package/dist/server/server/package-version.js +47 -0
  31. package/dist/server/server/package-version.js.map +1 -0
  32. package/dist/server/server/persisted-config.d.ts +2 -2
  33. package/dist/server/server/pid-lock.d.ts.map +1 -1
  34. package/dist/server/server/pid-lock.js +16 -2
  35. package/dist/server/server/pid-lock.js.map +1 -1
  36. package/dist/server/server/session.d.ts +23 -21
  37. package/dist/server/server/session.d.ts.map +1 -1
  38. package/dist/server/server/session.js +975 -850
  39. package/dist/server/server/session.js.map +1 -1
  40. package/dist/server/server/websocket-server.d.ts +5 -1
  41. package/dist/server/server/websocket-server.d.ts.map +1 -1
  42. package/dist/server/server/websocket-server.js +27 -16
  43. package/dist/server/server/websocket-server.js.map +1 -1
  44. package/dist/server/shared/agent-attention-notification.d.ts +40 -0
  45. package/dist/server/shared/agent-attention-notification.d.ts.map +1 -0
  46. package/dist/server/shared/agent-attention-notification.js +130 -0
  47. package/dist/server/shared/agent-attention-notification.js.map +1 -0
  48. package/dist/server/shared/messages.d.ts +974 -619
  49. package/dist/server/shared/messages.d.ts.map +1 -1
  50. package/dist/server/shared/messages.js +285 -296
  51. package/dist/server/shared/messages.js.map +1 -1
  52. package/dist/server/utils/checkout-git.d.ts +3 -0
  53. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  54. package/dist/server/utils/checkout-git.js +102 -23
  55. package/dist/server/utils/checkout-git.js.map +1 -1
  56. package/dist/server/utils/directory-suggestions.d.ts +15 -0
  57. package/dist/server/utils/directory-suggestions.d.ts.map +1 -1
  58. package/dist/server/utils/directory-suggestions.js +348 -21
  59. package/dist/server/utils/directory-suggestions.js.map +1 -1
  60. package/dist/server/utils/worktree-metadata.d.ts +4 -4
  61. package/dist/server/utils/worktree.d.ts +1 -0
  62. package/dist/server/utils/worktree.d.ts.map +1 -1
  63. package/dist/server/utils/worktree.js +41 -77
  64. package/dist/server/utils/worktree.js.map +1 -1
  65. package/package.json +5 -5
@@ -1,41 +1,41 @@
1
- import { v4 as uuidv4 } from "uuid";
2
- import { watch } from "node:fs";
3
- import { stat } from "fs/promises";
4
- import { exec } from "child_process";
5
- import { promisify } from "util";
6
- import { join, resolve, sep } from "path";
7
- import { homedir } from "node:os";
8
- import { z } from "zod";
9
- import { serializeAgentStreamEvent, } from "./messages.js";
10
- import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from "../shared/binary-mux.js";
11
- import { TTSManager } from "./agent/tts-manager.js";
12
- import { STTManager } from "./agent/stt-manager.js";
13
- import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
14
- import { isPaseoDictationDebugEnabled } from "./agent/recordings-debug.js";
15
- import { DictationStreamManager, } from "./dictation/dictation-stream-manager.js";
16
- import { buildConfigOverrides, buildSessionConfig, extractTimestamps, } from "./persistence-hooks.js";
17
- import { experimental_createMCPClient } from "ai";
18
- import { buildProviderRegistry } from "./agent/provider-registry.js";
19
- import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
20
- import { toAgentPayload } from "./agent/agent-projections.js";
21
- import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
22
- import { projectTimelineRows } from "./agent/timeline-projection.js";
23
- import { DEFAULT_STRUCTURED_GENERATION_PROVIDERS, StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from "./agent/agent-response-loop.js";
24
- import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
25
- import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
26
- import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
27
- import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
28
- import { slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
29
- import { createAgentWorktree, runAsyncWorktreeBootstrap, } from "./worktree-bootstrap.js";
30
- import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from "../utils/checkout-git.js";
31
- import { getProjectIcon } from "../utils/project-icon.js";
32
- import { expandTilde } from "../utils/path.js";
33
- import { searchHomeDirectories } from "../utils/directory-suggestions.js";
34
- import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from "./speech/providers/local/models.js";
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { watch } from 'node:fs';
3
+ import { stat } from 'fs/promises';
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import { join, resolve, sep } from 'path';
7
+ import { homedir } from 'node:os';
8
+ import { z } from 'zod';
9
+ import { serializeAgentStreamEvent, } from './messages.js';
10
+ import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from '../shared/binary-mux.js';
11
+ import { TTSManager } from './agent/tts-manager.js';
12
+ import { STTManager } from './agent/stt-manager.js';
13
+ import { maybePersistTtsDebugAudio } from './agent/tts-debug.js';
14
+ import { isPaseoDictationDebugEnabled } from './agent/recordings-debug.js';
15
+ import { DictationStreamManager, } from './dictation/dictation-stream-manager.js';
16
+ import { buildConfigOverrides, buildSessionConfig, extractTimestamps } from './persistence-hooks.js';
17
+ import { experimental_createMCPClient } from 'ai';
18
+ import { buildProviderRegistry } from './agent/provider-registry.js';
19
+ import { scheduleAgentMetadataGeneration } from './agent/agent-metadata-generator.js';
20
+ import { toAgentPayload } from './agent/agent-projections.js';
21
+ import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from './agent/timeline-append.js';
22
+ import { projectTimelineRows } from './agent/timeline-projection.js';
23
+ import { DEFAULT_STRUCTURED_GENERATION_PROVIDERS, StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from './agent/agent-response-loop.js';
24
+ import { isValidAgentProvider, AGENT_PROVIDER_IDS } from './agent/provider-manifest.js';
25
+ import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from './voice-config.js';
26
+ import { isVoicePermissionAllowed } from './voice-permission-policy.js';
27
+ import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from './file-explorer/service.js';
28
+ import { slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from '../utils/worktree.js';
29
+ import { createAgentWorktree, runAsyncWorktreeBootstrap } from './worktree-bootstrap.js';
30
+ import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from '../utils/checkout-git.js';
31
+ import { getProjectIcon } from '../utils/project-icon.js';
32
+ import { expandTilde } from '../utils/path.js';
33
+ import { searchHomeDirectories, searchWorkspaceEntries } from '../utils/directory-suggestions.js';
34
+ import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from './speech/providers/local/models.js';
35
35
  const execAsync = promisify(exec);
36
36
  const READ_ONLY_GIT_ENV = {
37
37
  ...process.env,
38
- GIT_OPTIONAL_LOCKS: "0",
38
+ GIT_OPTIONAL_LOCKS: '0',
39
39
  };
40
40
  const pendingAgentInitializations = new Map();
41
41
  let restartRequested = false;
@@ -61,11 +61,11 @@ function deriveRemoteProjectKey(remoteUrl) {
61
61
  host = scpLike[1] ?? null;
62
62
  path = scpLike[2] ?? null;
63
63
  }
64
- else if (trimmed.includes("://")) {
64
+ else if (trimmed.includes('://')) {
65
65
  try {
66
66
  const parsed = new URL(trimmed);
67
67
  host = parsed.hostname || null;
68
- path = parsed.pathname ? parsed.pathname.replace(/^\//, "") : null;
68
+ path = parsed.pathname ? parsed.pathname.replace(/^\//, '') : null;
69
69
  }
70
70
  catch {
71
71
  return null;
@@ -74,15 +74,15 @@ function deriveRemoteProjectKey(remoteUrl) {
74
74
  if (!host || !path) {
75
75
  return null;
76
76
  }
77
- let cleanedPath = path.trim().replace(/^\/+/, "").replace(/\/+$/, "");
78
- if (cleanedPath.endsWith(".git")) {
77
+ let cleanedPath = path.trim().replace(/^\/+/, '').replace(/\/+$/, '');
78
+ if (cleanedPath.endsWith('.git')) {
79
79
  cleanedPath = cleanedPath.slice(0, -4);
80
80
  }
81
- if (!cleanedPath.includes("/")) {
81
+ if (!cleanedPath.includes('/')) {
82
82
  return null;
83
83
  }
84
84
  const cleanedHost = host.toLowerCase();
85
- if (cleanedHost === "github.com") {
85
+ if (cleanedHost === 'github.com') {
86
86
  return `remote:github.com/${cleanedPath}`;
87
87
  }
88
88
  return `remote:${cleanedHost}/${cleanedPath}`;
@@ -92,15 +92,15 @@ function deriveProjectGroupingKey(options) {
92
92
  if (remoteKey) {
93
93
  return remoteKey;
94
94
  }
95
- const worktreeMarker = ".paseo/worktrees/";
95
+ const worktreeMarker = '.paseo/worktrees/';
96
96
  const idx = options.cwd.indexOf(worktreeMarker);
97
97
  if (idx !== -1) {
98
- return options.cwd.slice(0, idx).replace(/\/$/, "");
98
+ return options.cwd.slice(0, idx).replace(/\/$/, '');
99
99
  }
100
100
  return options.cwd;
101
101
  }
102
102
  function deriveProjectGroupingName(projectKey) {
103
- const githubRemotePrefix = "remote:github.com/";
103
+ const githubRemotePrefix = 'remote:github.com/';
104
104
  if (projectKey.startsWith(githubRemotePrefix)) {
105
105
  return projectKey.slice(githubRemotePrefix.length) || projectKey;
106
106
  }
@@ -111,7 +111,7 @@ class SessionRequestError extends Error {
111
111
  constructor(code, message) {
112
112
  super(message);
113
113
  this.code = code;
114
- this.name = "SessionRequestError";
114
+ this.name = 'SessionRequestError';
115
115
  }
116
116
  }
117
117
  const PCM_SAMPLE_RATE = 16000;
@@ -121,14 +121,14 @@ const PCM_BYTES_PER_MS = (PCM_SAMPLE_RATE * PCM_CHANNELS * (PCM_BITS_PER_SAMPLE
121
121
  const MIN_STREAMING_SEGMENT_DURATION_MS = 1000;
122
122
  const MIN_STREAMING_SEGMENT_BYTES = Math.round(PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS);
123
123
  const VOICE_MODE_INACTIVITY_FLUSH_MS = 4500;
124
- const VOICE_INTERNAL_DICTATION_ID_PREFIX = "__voice_turn__:";
124
+ const VOICE_INTERNAL_DICTATION_ID_PREFIX = '__voice_turn__:';
125
125
  const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
126
126
  const AgentIdSchema = z.string().uuid();
127
- const VOICE_MCP_SERVER_NAME = "paseo_voice";
127
+ const VOICE_MCP_SERVER_NAME = 'paseo_voice';
128
128
  class VoiceFeatureUnavailableError extends Error {
129
129
  constructor(context) {
130
130
  super(context.message);
131
- this.name = "VoiceFeatureUnavailableError";
131
+ this.name = 'VoiceFeatureUnavailableError';
132
132
  this.reasonCode = context.reasonCode;
133
133
  this.retryable = context.retryable;
134
134
  this.missingModelIds = [...context.missingModelIds];
@@ -139,10 +139,10 @@ function convertPCMToWavBuffer(pcmBuffer, sampleRate, channels, bitsPerSample) {
139
139
  const wavBuffer = Buffer.alloc(headerSize + pcmBuffer.length);
140
140
  const byteRate = (sampleRate * channels * bitsPerSample) / 8;
141
141
  const blockAlign = (channels * bitsPerSample) / 8;
142
- wavBuffer.write("RIFF", 0);
142
+ wavBuffer.write('RIFF', 0);
143
143
  wavBuffer.writeUInt32LE(36 + pcmBuffer.length, 4);
144
- wavBuffer.write("WAVE", 8);
145
- wavBuffer.write("fmt ", 12);
144
+ wavBuffer.write('WAVE', 8);
145
+ wavBuffer.write('fmt ', 12);
146
146
  wavBuffer.writeUInt32LE(16, 16);
147
147
  wavBuffer.writeUInt16LE(1, 20);
148
148
  wavBuffer.writeUInt16LE(channels, 22);
@@ -150,7 +150,7 @@ function convertPCMToWavBuffer(pcmBuffer, sampleRate, channels, bitsPerSample) {
150
150
  wavBuffer.writeUInt32LE(byteRate, 28);
151
151
  wavBuffer.writeUInt16LE(blockAlign, 32);
152
152
  wavBuffer.writeUInt16LE(bitsPerSample, 34);
153
- wavBuffer.write("data", 36);
153
+ wavBuffer.write('data', 36);
154
154
  wavBuffer.writeUInt32LE(pcmBuffer.length, 40);
155
155
  pcmBuffer.copy(wavBuffer, 44);
156
156
  return wavBuffer;
@@ -159,7 +159,7 @@ function coerceAgentProvider(logger, value, agentId) {
159
159
  if (isValidAgentProvider(value)) {
160
160
  return value;
161
161
  }
162
- logger.warn({ value, agentId, defaultProvider: DEFAULT_AGENT_PROVIDER }, `Unknown provider '${value}' for agent ${agentId ?? "unknown"}; defaulting to '${DEFAULT_AGENT_PROVIDER}'`);
162
+ logger.warn({ value, agentId, defaultProvider: DEFAULT_AGENT_PROVIDER }, `Unknown provider '${value}' for agent ${agentId ?? 'unknown'}; defaulting to '${DEFAULT_AGENT_PROVIDER}'`);
163
163
  return DEFAULT_AGENT_PROVIDER;
164
164
  }
165
165
  function toAgentPersistenceHandle(logger, handle) {
@@ -172,7 +172,7 @@ function toAgentPersistenceHandle(logger, handle) {
172
172
  return null;
173
173
  }
174
174
  if (!handle.sessionId) {
175
- logger.warn("Ignoring persistence handle missing sessionId");
175
+ logger.warn('Ignoring persistence handle missing sessionId');
176
176
  return null;
177
177
  }
178
178
  return {
@@ -189,7 +189,7 @@ function toAgentPersistenceHandle(logger, handle) {
189
189
  */
190
190
  export class Session {
191
191
  constructor(options) {
192
- this.processingPhase = "idle";
192
+ this.processingPhase = 'idle';
193
193
  // Voice mode state
194
194
  this.isVoiceMode = false;
195
195
  this.speechInProgress = false;
@@ -246,11 +246,11 @@ export class Session {
246
246
  this.localSpeechModelsDir =
247
247
  configuredModelsDir && configuredModelsDir.length > 0
248
248
  ? configuredModelsDir
249
- : join(this.paseoHome, "models", "local-speech");
249
+ : join(this.paseoHome, 'models', 'local-speech');
250
250
  this.defaultLocalSpeechModelIds =
251
251
  dictation?.localModels?.defaultModelIds && dictation.localModels.defaultModelIds.length > 0
252
252
  ? [...new Set(dictation.localModels.defaultModelIds)]
253
- : ["parakeet-tdt-0.6b-v2-int8", "kokoro-en-v0_19"];
253
+ : ['parakeet-tdt-0.6b-v2-int8', 'kokoro-en-v0_19'];
254
254
  this.registerVoiceSpeakHandler = voiceBridge?.registerVoiceSpeakHandler;
255
255
  this.unregisterVoiceSpeakHandler = voiceBridge?.unregisterVoiceSpeakHandler;
256
256
  this.registerVoiceCallerContext = voiceBridge?.registerVoiceCallerContext;
@@ -261,7 +261,7 @@ export class Session {
261
261
  this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
262
262
  this.abortController = new AbortController();
263
263
  this.sessionLogger = logger.child({
264
- module: "session",
264
+ module: 'session',
265
265
  clientId: this.clientId,
266
266
  sessionId: this.sessionId,
267
267
  });
@@ -279,7 +279,7 @@ export class Session {
279
279
  finalTimeoutMs: dictation?.finalTimeoutMs,
280
280
  });
281
281
  this.voiceStreamManager = new DictationStreamManager({
282
- logger: this.sessionLogger.child({ stream: "voice-internal" }),
282
+ logger: this.sessionLogger.child({ stream: 'voice-internal' }),
283
283
  sessionId: this.sessionId,
284
284
  emit: (msg) => this.handleDictationManagerMessage(msg),
285
285
  stt: stt,
@@ -288,7 +288,7 @@ export class Session {
288
288
  // Initialize agent MCP client asynchronously
289
289
  void this.initializeAgentMcp();
290
290
  this.subscribeToAgentEvents();
291
- this.sessionLogger.trace("Session created");
291
+ this.sessionLogger.trace('Session created');
292
292
  }
293
293
  /**
294
294
  * Get the client's current activity state
@@ -306,16 +306,16 @@ export class Session {
306
306
  * Normalize a user prompt (with optional image metadata) for AgentManager
307
307
  */
308
308
  buildAgentPrompt(text, images) {
309
- const normalized = text?.trim() ?? "";
309
+ const normalized = text?.trim() ?? '';
310
310
  if (!images || images.length === 0) {
311
311
  return normalized;
312
312
  }
313
313
  const blocks = [];
314
314
  if (normalized.length > 0) {
315
- blocks.push({ type: "text", text: normalized });
315
+ blocks.push({ type: 'text', text: normalized });
316
316
  }
317
317
  for (const image of images) {
318
- blocks.push({ type: "image", data: image.data, mimeType: image.mimeType });
318
+ blocks.push({ type: 'image', data: image.data, mimeType: image.mimeType });
319
319
  }
320
320
  return blocks;
321
321
  }
@@ -328,17 +328,17 @@ export class Session {
328
328
  if (!snapshot) {
329
329
  throw new Error(`Agent ${agentId} not found`);
330
330
  }
331
- if (snapshot.lifecycle !== "running" && !snapshot.pendingRun) {
332
- this.sessionLogger.debug({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, "interruptAgentIfRunning: not running, skipping");
331
+ if (snapshot.lifecycle !== 'running' && !snapshot.pendingRun) {
332
+ this.sessionLogger.debug({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, 'interruptAgentIfRunning: not running, skipping');
333
333
  return;
334
334
  }
335
- this.sessionLogger.debug({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, "interruptAgentIfRunning: interrupting");
335
+ this.sessionLogger.debug({ agentId, lifecycle: snapshot.lifecycle, pendingRun: Boolean(snapshot.pendingRun) }, 'interruptAgentIfRunning: interrupting');
336
336
  try {
337
337
  const t0 = Date.now();
338
338
  const cancelled = await this.agentManager.cancelAgentRun(agentId);
339
- this.sessionLogger.debug({ agentId, cancelled, durationMs: Date.now() - t0 }, "interruptAgentIfRunning: cancelAgentRun completed");
339
+ this.sessionLogger.debug({ agentId, cancelled, durationMs: Date.now() - t0 }, 'interruptAgentIfRunning: cancelAgentRun completed');
340
340
  if (!cancelled) {
341
- this.sessionLogger.warn({ agentId }, "interruptAgentIfRunning: reported running but no active run was cancelled");
341
+ this.sessionLogger.warn({ agentId }, 'interruptAgentIfRunning: reported running but no active run was cancelled');
342
342
  }
343
343
  }
344
344
  catch (error) {
@@ -353,7 +353,7 @@ export class Session {
353
353
  if (!snapshot) {
354
354
  return false;
355
355
  }
356
- return snapshot.lifecycle === "running" || Boolean(snapshot.pendingRun);
356
+ return snapshot.lifecycle === 'running' || Boolean(snapshot.pendingRun);
357
357
  }
358
358
  /**
359
359
  * Start streaming an agent run and forward results via the websocket broadcast
@@ -365,8 +365,8 @@ export class Session {
365
365
  iterator = this.agentManager.streamAgent(agentId, prompt, runOptions);
366
366
  }
367
367
  catch (error) {
368
- this.handleAgentRunError(agentId, error, "Failed to start agent run");
369
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
368
+ this.handleAgentRunError(agentId, error, 'Failed to start agent run');
369
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
370
370
  return { ok: false, error: message };
371
371
  }
372
372
  void (async () => {
@@ -376,20 +376,20 @@ export class Session {
376
376
  }
377
377
  }
378
378
  catch (error) {
379
- this.handleAgentRunError(agentId, error, "Agent stream failed");
379
+ this.handleAgentRunError(agentId, error, 'Agent stream failed');
380
380
  }
381
381
  })();
382
382
  return { ok: true };
383
383
  }
384
384
  handleAgentRunError(agentId, error, context) {
385
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
385
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
386
386
  this.sessionLogger.error({ err: error, agentId, context }, `${context} for agent ${agentId}`);
387
387
  this.emit({
388
- type: "activity_log",
388
+ type: 'activity_log',
389
389
  payload: {
390
390
  id: uuidv4(),
391
391
  timestamp: new Date(),
392
- type: "error",
392
+ type: 'error',
393
393
  content: `${context}: ${message}`,
394
394
  },
395
395
  });
@@ -409,7 +409,7 @@ export class Session {
409
409
  this.sessionLogger.trace({ agentToolCount }, `Agent MCP initialized with ${agentToolCount} tools`);
410
410
  }
411
411
  catch (error) {
412
- this.sessionLogger.error({ err: error }, "Failed to initialize Agent MCP");
412
+ this.sessionLogger.error({ err: error }, 'Failed to initialize Agent MCP');
413
413
  }
414
414
  }
415
415
  /**
@@ -420,32 +420,32 @@ export class Session {
420
420
  this.unsubscribeAgentEvents();
421
421
  }
422
422
  this.unsubscribeAgentEvents = this.agentManager.subscribe((event) => {
423
- if (event.type === "agent_state") {
423
+ if (event.type === 'agent_state') {
424
424
  void this.forwardAgentUpdate(event.agent);
425
425
  return;
426
426
  }
427
427
  if (this.isVoiceMode &&
428
428
  this.voiceModeAgentId === event.agentId &&
429
- event.event.type === "permission_requested" &&
429
+ event.event.type === 'permission_requested' &&
430
430
  isVoicePermissionAllowed(event.event.request)) {
431
431
  const requestId = event.event.request.id;
432
432
  void this.agentManager
433
433
  .respondToPermission(event.agentId, requestId, {
434
- behavior: "allow",
434
+ behavior: 'allow',
435
435
  })
436
436
  .catch((error) => {
437
437
  this.sessionLogger.warn({
438
438
  err: error,
439
439
  agentId: event.agentId,
440
440
  requestId,
441
- }, "Failed to auto-allow speak tool permission in voice mode");
441
+ }, 'Failed to auto-allow speak tool permission in voice mode');
442
442
  });
443
443
  }
444
444
  // Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
445
445
  // for the focused agent, with a short grace window while backgrounded.
446
446
  // History catch-up is handled via pull-based `fetch_agent_timeline_request`.
447
447
  const activity = this.clientActivity;
448
- if (activity?.deviceType === "mobile") {
448
+ if (activity?.deviceType === 'mobile') {
449
449
  if (!activity.focusedAgentId) {
450
450
  return;
451
451
  }
@@ -467,25 +467,25 @@ export class Session {
467
467
  agentId: event.agentId,
468
468
  event: serializedEvent,
469
469
  timestamp: new Date().toISOString(),
470
- ...(typeof event.seq === "number" ? { seq: event.seq } : {}),
471
- ...(typeof event.epoch === "string" ? { epoch: event.epoch } : {}),
470
+ ...(typeof event.seq === 'number' ? { seq: event.seq } : {}),
471
+ ...(typeof event.epoch === 'string' ? { epoch: event.epoch } : {}),
472
472
  };
473
473
  this.emit({
474
- type: "agent_stream",
474
+ type: 'agent_stream',
475
475
  payload,
476
476
  });
477
- if (event.event.type === "permission_requested") {
477
+ if (event.event.type === 'permission_requested') {
478
478
  this.emit({
479
- type: "agent_permission_request",
479
+ type: 'agent_permission_request',
480
480
  payload: {
481
481
  agentId: event.agentId,
482
482
  request: event.event.request,
483
483
  },
484
484
  });
485
485
  }
486
- else if (event.event.type === "permission_resolved") {
486
+ else if (event.event.type === 'permission_resolved') {
487
487
  this.emit({
488
- type: "agent_permission_resolved",
488
+ type: 'agent_permission_resolved',
489
489
  payload: {
490
490
  agentId: event.agentId,
491
491
  requestId: event.event.requestId,
@@ -514,9 +514,7 @@ export class Session {
514
514
  };
515
515
  const createdAt = new Date(record.createdAt);
516
516
  const updatedAt = new Date(record.lastActivityAt ?? record.updatedAt);
517
- const lastUserMessageAt = record.lastUserMessageAt
518
- ? new Date(record.lastUserMessageAt)
519
- : null;
517
+ const lastUserMessageAt = record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null;
520
518
  const provider = coerceAgentProvider(this.sessionLogger, record.provider, record.id);
521
519
  return {
522
520
  id: record.id,
@@ -561,12 +559,12 @@ export class Session {
561
559
  let snapshot;
562
560
  if (handle) {
563
561
  snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, extractTimestamps(record));
564
- this.sessionLogger.info({ agentId, provider: record.provider }, "Agent resumed from persistence");
562
+ this.sessionLogger.info({ agentId, provider: record.provider }, 'Agent resumed from persistence');
565
563
  }
566
564
  else {
567
565
  const config = buildSessionConfig(record);
568
566
  snapshot = await this.agentManager.createAgent(config, agentId, { labels: record.labels });
569
- this.sessionLogger.info({ agentId, provider: record.provider }, "Agent created from stored config");
567
+ this.sessionLogger.info({ agentId, provider: record.provider }, 'Agent created from stored config');
570
568
  }
571
569
  await this.agentManager.hydrateTimelineFromProvider(agentId);
572
570
  return this.agentManager.getAgent(agentId) ?? snapshot;
@@ -582,14 +580,74 @@ export class Session {
582
580
  }
583
581
  }
584
582
  }
585
- matchesAgentFilter(agent, filter) {
586
- if (filter?.agentId && agent.id !== filter.agentId) {
583
+ matchesAgentFilter(options) {
584
+ const { agent, project, filter } = options;
585
+ if (filter?.labels) {
586
+ const matchesLabels = Object.entries(filter.labels).every(([key, value]) => agent.labels[key] === value);
587
+ if (!matchesLabels) {
588
+ return false;
589
+ }
590
+ }
591
+ const includeArchived = filter?.includeArchived ?? false;
592
+ if (!includeArchived && agent.archivedAt) {
587
593
  return false;
588
594
  }
589
- if (!filter?.labels) {
590
- return true;
595
+ if (filter?.statuses && filter.statuses.length > 0) {
596
+ const statuses = new Set(filter.statuses);
597
+ if (!statuses.has(agent.status)) {
598
+ return false;
599
+ }
600
+ }
601
+ if (typeof filter?.requiresAttention === 'boolean') {
602
+ const requiresAttention = agent.requiresAttention ?? false;
603
+ if (requiresAttention !== filter.requiresAttention) {
604
+ return false;
605
+ }
606
+ }
607
+ if (filter?.projectKeys && filter.projectKeys.length > 0) {
608
+ const projectKeys = new Set(filter.projectKeys.filter((item) => item.trim().length > 0));
609
+ if (projectKeys.size > 0 && !projectKeys.has(project.projectKey)) {
610
+ return false;
611
+ }
612
+ }
613
+ return true;
614
+ }
615
+ getAgentUpdateTargetId(update) {
616
+ return update.kind === 'remove' ? update.agentId : update.agent.id;
617
+ }
618
+ bufferOrEmitAgentUpdate(subscription, payload) {
619
+ if (subscription.isBootstrapping) {
620
+ subscription.pendingUpdatesByAgentId.set(this.getAgentUpdateTargetId(payload), payload);
621
+ return;
622
+ }
623
+ this.emit({
624
+ type: 'agent_update',
625
+ payload,
626
+ });
627
+ }
628
+ flushBootstrappedAgentUpdates(options) {
629
+ const subscription = this.agentUpdatesSubscription;
630
+ if (!subscription || !subscription.isBootstrapping) {
631
+ return;
632
+ }
633
+ subscription.isBootstrapping = false;
634
+ const pending = Array.from(subscription.pendingUpdatesByAgentId.values());
635
+ subscription.pendingUpdatesByAgentId.clear();
636
+ for (const payload of pending) {
637
+ if (payload.kind === 'upsert') {
638
+ const snapshotUpdatedAt = options?.snapshotUpdatedAtByAgentId?.get(payload.agent.id);
639
+ if (typeof snapshotUpdatedAt === 'number') {
640
+ const updateUpdatedAt = Date.parse(payload.agent.updatedAt);
641
+ if (!Number.isNaN(updateUpdatedAt) && updateUpdatedAt <= snapshotUpdatedAt) {
642
+ continue;
643
+ }
644
+ }
645
+ }
646
+ this.emit({
647
+ type: 'agent_update',
648
+ payload,
649
+ });
591
650
  }
592
- return Object.entries(filter.labels).every(([key, value]) => agent.labels[key] === value);
593
651
  }
594
652
  buildFallbackProjectCheckout(cwd) {
595
653
  return {
@@ -645,43 +703,27 @@ export class Session {
645
703
  return;
646
704
  }
647
705
  const payload = await this.buildAgentPayload(agent);
648
- const matches = this.matchesAgentFilter(payload, subscription.filter);
706
+ const project = await this.buildProjectPlacement(payload.cwd);
707
+ const matches = this.matchesAgentFilter({
708
+ agent: payload,
709
+ project,
710
+ filter: subscription.filter,
711
+ });
649
712
  if (matches) {
650
- const project = await this.buildProjectPlacement(payload.cwd);
651
- this.emit({
652
- type: "agent_update",
653
- payload: { kind: "upsert", agent: payload, project },
713
+ this.bufferOrEmitAgentUpdate(subscription, {
714
+ kind: 'upsert',
715
+ agent: payload,
716
+ project,
654
717
  });
655
718
  return;
656
719
  }
657
- this.emit({
658
- type: "agent_update",
659
- payload: { kind: "remove", agentId: payload.id },
660
- });
661
- }
662
- catch (error) {
663
- this.sessionLogger.error({ err: error }, "Failed to emit agent update");
664
- }
665
- }
666
- async emitCurrentAgentUpdatesForSubscription() {
667
- const subscription = this.agentUpdatesSubscription;
668
- if (!subscription) {
669
- return;
670
- }
671
- try {
672
- const agents = await this.listAgentPayloads({
673
- labels: subscription.filter?.labels,
720
+ this.bufferOrEmitAgentUpdate(subscription, {
721
+ kind: 'remove',
722
+ agentId: payload.id,
674
723
  });
675
- for (const agent of agents) {
676
- const project = await this.buildProjectPlacement(agent.cwd);
677
- this.emit({
678
- type: "agent_update",
679
- payload: { kind: "upsert", agent, project },
680
- });
681
- }
682
724
  }
683
725
  catch (error) {
684
- this.sessionLogger.error({ err: error }, "Failed to emit current agent updates for subscription bootstrap");
726
+ this.sessionLogger.error({ err: error }, 'Failed to emit agent update');
685
727
  }
686
728
  }
687
729
  /**
@@ -690,57 +732,45 @@ export class Session {
690
732
  async handleMessage(msg) {
691
733
  try {
692
734
  switch (msg.type) {
693
- case "voice_audio_chunk":
735
+ case 'voice_audio_chunk':
694
736
  await this.handleAudioChunk(msg);
695
737
  break;
696
- case "abort_request":
738
+ case 'abort_request':
697
739
  await this.handleAbort();
698
740
  break;
699
- case "audio_played":
741
+ case 'audio_played':
700
742
  this.handleAudioPlayed(msg.id);
701
743
  break;
702
- case "fetch_agents_request":
744
+ case 'fetch_agents_request':
703
745
  await this.handleFetchAgents(msg);
704
746
  break;
705
- case "fetch_agent_request":
747
+ case 'fetch_agent_request':
706
748
  await this.handleFetchAgent(msg.agentId, msg.requestId);
707
749
  break;
708
- case "subscribe_agent_updates":
709
- this.agentUpdatesSubscription = {
710
- subscriptionId: msg.subscriptionId,
711
- filter: msg.filter,
712
- };
713
- await this.emitCurrentAgentUpdatesForSubscription();
714
- break;
715
- case "unsubscribe_agent_updates":
716
- if (this.agentUpdatesSubscription?.subscriptionId === msg.subscriptionId) {
717
- this.agentUpdatesSubscription = null;
718
- }
719
- break;
720
- case "delete_agent_request":
750
+ case 'delete_agent_request':
721
751
  await this.handleDeleteAgentRequest(msg.agentId, msg.requestId);
722
752
  break;
723
- case "archive_agent_request":
753
+ case 'archive_agent_request':
724
754
  await this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
725
755
  break;
726
- case "update_agent_request":
756
+ case 'update_agent_request':
727
757
  await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
728
758
  break;
729
- case "set_voice_mode":
759
+ case 'set_voice_mode':
730
760
  await this.handleSetVoiceMode(msg.enabled, msg.agentId, msg.requestId);
731
761
  break;
732
- case "send_agent_message_request":
762
+ case 'send_agent_message_request':
733
763
  await this.handleSendAgentMessageRequest(msg);
734
764
  break;
735
- case "wait_for_finish_request":
765
+ case 'wait_for_finish_request':
736
766
  await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
737
767
  break;
738
- case "dictation_stream_start":
768
+ case 'dictation_stream_start':
739
769
  {
740
- const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation");
770
+ const unavailable = this.resolveVoiceFeatureUnavailableContext('dictation');
741
771
  if (unavailable) {
742
772
  this.emit({
743
- type: "dictation_stream_error",
773
+ type: 'dictation_stream_error',
744
774
  payload: {
745
775
  dictationId: msg.dictationId,
746
776
  error: unavailable.message,
@@ -754,7 +784,7 @@ export class Session {
754
784
  }
755
785
  await this.dictationStreamManager.handleStart(msg.dictationId, msg.format);
756
786
  break;
757
- case "dictation_stream_chunk":
787
+ case 'dictation_stream_chunk':
758
788
  await this.dictationStreamManager.handleChunk({
759
789
  dictationId: msg.dictationId,
760
790
  seq: msg.seq,
@@ -762,115 +792,115 @@ export class Session {
762
792
  format: msg.format,
763
793
  });
764
794
  break;
765
- case "dictation_stream_finish":
795
+ case 'dictation_stream_finish':
766
796
  await this.dictationStreamManager.handleFinish(msg.dictationId, msg.finalSeq);
767
797
  break;
768
- case "dictation_stream_cancel":
798
+ case 'dictation_stream_cancel':
769
799
  this.dictationStreamManager.handleCancel(msg.dictationId);
770
800
  break;
771
- case "create_agent_request":
801
+ case 'create_agent_request':
772
802
  await this.handleCreateAgentRequest(msg);
773
803
  break;
774
- case "resume_agent_request":
804
+ case 'resume_agent_request':
775
805
  await this.handleResumeAgentRequest(msg);
776
806
  break;
777
- case "refresh_agent_request":
807
+ case 'refresh_agent_request':
778
808
  await this.handleRefreshAgentRequest(msg);
779
809
  break;
780
- case "cancel_agent_request":
810
+ case 'cancel_agent_request':
781
811
  await this.handleCancelAgentRequest(msg.agentId);
782
812
  break;
783
- case "restart_server_request":
813
+ case 'restart_server_request':
784
814
  await this.handleRestartServerRequest(msg.requestId, msg.reason);
785
815
  break;
786
- case "fetch_agent_timeline_request":
816
+ case 'fetch_agent_timeline_request':
787
817
  await this.handleFetchAgentTimelineRequest(msg);
788
818
  break;
789
- case "set_agent_mode_request":
819
+ case 'set_agent_mode_request':
790
820
  await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId);
791
821
  break;
792
- case "set_agent_model_request":
822
+ case 'set_agent_model_request':
793
823
  await this.handleSetAgentModelRequest(msg.agentId, msg.modelId, msg.requestId);
794
824
  break;
795
- case "set_agent_thinking_request":
825
+ case 'set_agent_thinking_request':
796
826
  await this.handleSetAgentThinkingRequest(msg.agentId, msg.thinkingOptionId, msg.requestId);
797
827
  break;
798
- case "agent_permission_response":
828
+ case 'agent_permission_response':
799
829
  await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response);
800
830
  break;
801
- case "checkout_status_request":
831
+ case 'checkout_status_request':
802
832
  await this.handleCheckoutStatusRequest(msg);
803
833
  break;
804
- case "validate_branch_request":
834
+ case 'validate_branch_request':
805
835
  await this.handleValidateBranchRequest(msg);
806
836
  break;
807
- case "branch_suggestions_request":
837
+ case 'branch_suggestions_request':
808
838
  await this.handleBranchSuggestionsRequest(msg);
809
839
  break;
810
- case "directory_suggestions_request":
840
+ case 'directory_suggestions_request':
811
841
  await this.handleDirectorySuggestionsRequest(msg);
812
842
  break;
813
- case "subscribe_checkout_diff_request":
843
+ case 'subscribe_checkout_diff_request':
814
844
  await this.handleSubscribeCheckoutDiffRequest(msg);
815
845
  break;
816
- case "unsubscribe_checkout_diff_request":
846
+ case 'unsubscribe_checkout_diff_request':
817
847
  this.handleUnsubscribeCheckoutDiffRequest(msg);
818
848
  break;
819
- case "checkout_commit_request":
849
+ case 'checkout_commit_request':
820
850
  await this.handleCheckoutCommitRequest(msg);
821
851
  break;
822
- case "checkout_merge_request":
852
+ case 'checkout_merge_request':
823
853
  await this.handleCheckoutMergeRequest(msg);
824
854
  break;
825
- case "checkout_merge_from_base_request":
855
+ case 'checkout_merge_from_base_request':
826
856
  await this.handleCheckoutMergeFromBaseRequest(msg);
827
857
  break;
828
- case "checkout_push_request":
858
+ case 'checkout_push_request':
829
859
  await this.handleCheckoutPushRequest(msg);
830
860
  break;
831
- case "checkout_pr_create_request":
861
+ case 'checkout_pr_create_request':
832
862
  await this.handleCheckoutPrCreateRequest(msg);
833
863
  break;
834
- case "checkout_pr_status_request":
864
+ case 'checkout_pr_status_request':
835
865
  await this.handleCheckoutPrStatusRequest(msg);
836
866
  break;
837
- case "paseo_worktree_list_request":
867
+ case 'paseo_worktree_list_request':
838
868
  await this.handlePaseoWorktreeListRequest(msg);
839
869
  break;
840
- case "paseo_worktree_archive_request":
870
+ case 'paseo_worktree_archive_request':
841
871
  await this.handlePaseoWorktreeArchiveRequest(msg);
842
872
  break;
843
- case "file_explorer_request":
873
+ case 'file_explorer_request':
844
874
  await this.handleFileExplorerRequest(msg);
845
875
  break;
846
- case "project_icon_request":
876
+ case 'project_icon_request':
847
877
  await this.handleProjectIconRequest(msg);
848
878
  break;
849
- case "file_download_token_request":
879
+ case 'file_download_token_request':
850
880
  await this.handleFileDownloadTokenRequest(msg);
851
881
  break;
852
- case "list_provider_models_request":
882
+ case 'list_provider_models_request':
853
883
  await this.handleListProviderModelsRequest(msg);
854
884
  break;
855
- case "list_available_providers_request":
885
+ case 'list_available_providers_request':
856
886
  await this.handleListAvailableProvidersRequest(msg);
857
887
  break;
858
- case "speech_models_list_request":
888
+ case 'speech_models_list_request':
859
889
  await this.handleSpeechModelsListRequest(msg);
860
890
  break;
861
- case "speech_models_download_request":
891
+ case 'speech_models_download_request':
862
892
  await this.handleSpeechModelsDownloadRequest(msg);
863
893
  break;
864
- case "clear_agent_attention":
894
+ case 'clear_agent_attention':
865
895
  await this.handleClearAgentAttention(msg.agentId);
866
896
  break;
867
- case "client_heartbeat":
897
+ case 'client_heartbeat':
868
898
  this.handleClientHeartbeat(msg);
869
899
  break;
870
- case "ping": {
900
+ case 'ping': {
871
901
  const now = Date.now();
872
902
  this.emit({
873
- type: "pong",
903
+ type: 'pong',
874
904
  payload: {
875
905
  requestId: msg.requestId,
876
906
  clientSentAt: msg.clientSentAt,
@@ -880,73 +910,70 @@ export class Session {
880
910
  });
881
911
  break;
882
912
  }
883
- case "list_commands_request":
884
- await this.handleListCommandsRequest(msg.agentId, msg.requestId);
885
- break;
886
- case "execute_command_request":
887
- await this.handleExecuteCommandRequest(msg.agentId, msg.commandName, msg.args, msg.requestId);
913
+ case 'list_commands_request':
914
+ await this.handleListCommandsRequest(msg);
888
915
  break;
889
- case "register_push_token":
916
+ case 'register_push_token':
890
917
  this.handleRegisterPushToken(msg.token);
891
918
  break;
892
- case "subscribe_terminals_request":
919
+ case 'subscribe_terminals_request':
893
920
  this.handleSubscribeTerminalsRequest(msg);
894
921
  break;
895
- case "unsubscribe_terminals_request":
922
+ case 'unsubscribe_terminals_request':
896
923
  this.handleUnsubscribeTerminalsRequest(msg);
897
924
  break;
898
- case "list_terminals_request":
925
+ case 'list_terminals_request':
899
926
  await this.handleListTerminalsRequest(msg);
900
927
  break;
901
- case "create_terminal_request":
928
+ case 'create_terminal_request':
902
929
  await this.handleCreateTerminalRequest(msg);
903
930
  break;
904
- case "subscribe_terminal_request":
931
+ case 'subscribe_terminal_request':
905
932
  await this.handleSubscribeTerminalRequest(msg);
906
933
  break;
907
- case "unsubscribe_terminal_request":
934
+ case 'unsubscribe_terminal_request':
908
935
  this.handleUnsubscribeTerminalRequest(msg);
909
936
  break;
910
- case "terminal_input":
937
+ case 'terminal_input':
911
938
  this.handleTerminalInput(msg);
912
939
  break;
913
- case "kill_terminal_request":
940
+ case 'kill_terminal_request':
914
941
  await this.handleKillTerminalRequest(msg);
915
942
  break;
916
- case "attach_terminal_stream_request":
943
+ case 'attach_terminal_stream_request':
917
944
  await this.handleAttachTerminalStreamRequest(msg);
918
945
  break;
919
- case "detach_terminal_stream_request":
946
+ case 'detach_terminal_stream_request':
920
947
  this.handleDetachTerminalStreamRequest(msg);
921
948
  break;
922
949
  }
923
950
  }
924
951
  catch (error) {
925
952
  const err = error instanceof Error ? error : new Error(String(error));
926
- this.sessionLogger.error({ err }, "Error handling message");
953
+ this.sessionLogger.error({ err }, 'Error handling message');
927
954
  const requestId = msg.requestId;
928
- if (typeof requestId === "string") {
955
+ if (typeof requestId === 'string') {
929
956
  try {
930
957
  this.emit({
931
- type: "rpc_error",
958
+ type: 'rpc_error',
932
959
  payload: {
933
960
  requestId,
934
961
  requestType: msg.type,
935
- error: "Request failed",
936
- code: "handler_error",
962
+ error: 'Request failed',
963
+ code: 'handler_error',
937
964
  },
938
965
  });
939
966
  }
940
967
  catch (emitError) {
941
- this.sessionLogger.error({ err: emitError }, "Failed to emit rpc_error");
968
+ this.sessionLogger.error({ err: emitError }, 'Failed to emit rpc_error');
942
969
  }
943
970
  }
944
971
  this.emit({
945
- type: "activity_log",
972
+ type: 'activity_log',
946
973
  payload: {
947
974
  id: uuidv4(),
948
975
  timestamp: new Date(),
949
- type: "error",
976
+ type: 'error',
950
977
  content: `Error: ${err.message}`,
951
978
  },
952
979
  });
@@ -958,7 +985,7 @@ export class Session {
958
985
  this.handleTerminalBinaryFrame(frame);
959
986
  break;
960
987
  default:
961
- this.sessionLogger.warn({ channel: frame.channel, messageType: frame.messageType }, "Unhandled binary mux channel");
988
+ this.sessionLogger.warn({ channel: frame.channel, messageType: frame.messageType }, 'Unhandled binary mux channel');
962
989
  break;
963
990
  }
964
991
  }
@@ -966,7 +993,7 @@ export class Session {
966
993
  if (frame.messageType === TerminalBinaryMessageType.InputUtf8) {
967
994
  const binding = this.terminalStreams.get(frame.streamId);
968
995
  if (!binding) {
969
- this.sessionLogger.warn({ streamId: frame.streamId }, "Terminal stream not found for input");
996
+ this.sessionLogger.warn({ streamId: frame.streamId }, 'Terminal stream not found for input');
970
997
  return;
971
998
  }
972
999
  if (!this.terminalManager) {
@@ -981,11 +1008,11 @@ export class Session {
981
1008
  if (payload.byteLength === 0) {
982
1009
  return;
983
1010
  }
984
- const text = Buffer.from(payload).toString("utf8");
1011
+ const text = Buffer.from(payload).toString('utf8');
985
1012
  if (!text) {
986
1013
  return;
987
1014
  }
988
- session.send({ type: "input", data: text });
1015
+ session.send({ type: 'input', data: text });
989
1016
  return;
990
1017
  }
991
1018
  if (frame.messageType === TerminalBinaryMessageType.Ack) {
@@ -1002,30 +1029,30 @@ export class Session {
1002
1029
  }
1003
1030
  return;
1004
1031
  }
1005
- this.sessionLogger.warn({ streamId: frame.streamId, messageType: frame.messageType }, "Unhandled terminal binary frame");
1032
+ this.sessionLogger.warn({ streamId: frame.streamId, messageType: frame.messageType }, 'Unhandled terminal binary frame');
1006
1033
  }
1007
1034
  async handleRestartServerRequest(requestId, reason) {
1008
1035
  if (restartRequested) {
1009
- this.sessionLogger.debug("Restart already requested, ignoring duplicate");
1036
+ this.sessionLogger.debug('Restart already requested, ignoring duplicate');
1010
1037
  return;
1011
1038
  }
1012
1039
  restartRequested = true;
1013
1040
  const payload = {
1014
- status: "restart_requested",
1041
+ status: 'restart_requested',
1015
1042
  clientId: this.clientId,
1016
1043
  };
1017
1044
  if (reason && reason.trim().length > 0) {
1018
1045
  payload.reason = reason;
1019
1046
  }
1020
1047
  payload.requestId = requestId;
1021
- this.sessionLogger.warn({ reason }, "Restart requested via websocket");
1048
+ this.sessionLogger.warn({ reason }, 'Restart requested via websocket');
1022
1049
  this.emit({
1023
- type: "status",
1050
+ type: 'status',
1024
1051
  payload,
1025
1052
  });
1026
- if (typeof process.send === "function") {
1053
+ if (typeof process.send === 'function') {
1027
1054
  process.send({
1028
- type: "paseo:restart",
1055
+ type: 'paseo:restart',
1029
1056
  ...(reason ? { reason } : {}),
1030
1057
  });
1031
1058
  return;
@@ -1051,56 +1078,75 @@ export class Session {
1051
1078
  this.sessionLogger.error({ err: error, agentId }, `Failed to remove agent ${agentId} from registry`);
1052
1079
  }
1053
1080
  this.emit({
1054
- type: "agent_deleted",
1081
+ type: 'agent_deleted',
1055
1082
  payload: {
1056
1083
  agentId,
1057
1084
  requestId,
1058
1085
  },
1059
1086
  });
1060
1087
  if (this.agentUpdatesSubscription) {
1061
- this.emit({
1062
- type: "agent_update",
1063
- payload: { kind: "remove", agentId },
1088
+ this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
1089
+ kind: 'remove',
1090
+ agentId,
1064
1091
  });
1065
1092
  }
1066
1093
  }
1067
1094
  async handleArchiveAgentRequest(agentId, requestId) {
1068
1095
  this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
1069
1096
  const archivedAt = new Date().toISOString();
1070
- try {
1071
- const existing = await this.agentStorage.get(agentId);
1072
- if (existing) {
1073
- await this.agentStorage.upsert({
1074
- ...existing,
1075
- archivedAt,
1076
- });
1097
+ const existing = await this.agentStorage.get(agentId);
1098
+ let archivedRecord = existing;
1099
+ if (!archivedRecord) {
1100
+ const liveAgent = this.agentManager.getAgent(agentId);
1101
+ if (!liveAgent) {
1102
+ throw new Error(`Agent not found: ${agentId}`);
1103
+ }
1104
+ await this.agentStorage.applySnapshot(liveAgent, {
1105
+ title: liveAgent.config.title ?? null,
1106
+ internal: liveAgent.internal,
1107
+ });
1108
+ archivedRecord = await this.agentStorage.get(agentId);
1109
+ if (!archivedRecord) {
1110
+ throw new Error(`Agent not found in storage after snapshot: ${agentId}`);
1077
1111
  }
1078
- this.agentManager.notifyAgentState(agentId);
1079
- }
1080
- catch (error) {
1081
- this.sessionLogger.error({ err: error, agentId }, `Failed to archive agent ${agentId}`);
1082
1112
  }
1113
+ archivedRecord = {
1114
+ ...archivedRecord,
1115
+ archivedAt,
1116
+ };
1117
+ await this.agentStorage.upsert(archivedRecord);
1118
+ this.agentManager.notifyAgentState(agentId);
1083
1119
  this.emit({
1084
- type: "agent_archived",
1120
+ type: 'agent_archived',
1085
1121
  payload: {
1086
1122
  agentId,
1087
1123
  archivedAt,
1088
1124
  requestId,
1089
1125
  },
1090
1126
  });
1127
+ await this.maybeArchiveWorktreeAfterLastAgentArchived({
1128
+ archivedAgentId: agentId,
1129
+ archivedAgentCwd: archivedRecord.cwd,
1130
+ requestId,
1131
+ });
1091
1132
  }
1092
1133
  async handleUpdateAgentRequest(agentId, name, labels, requestId) {
1093
- this.sessionLogger.info({ agentId, requestId, hasName: typeof name === "string", labelCount: labels ? Object.keys(labels).length : 0 }, "session: update_agent_request");
1134
+ this.sessionLogger.info({
1135
+ agentId,
1136
+ requestId,
1137
+ hasName: typeof name === 'string',
1138
+ labelCount: labels ? Object.keys(labels).length : 0,
1139
+ }, 'session: update_agent_request');
1094
1140
  const normalizedName = name?.trim();
1095
1141
  const normalizedLabels = labels && Object.keys(labels).length > 0 ? labels : undefined;
1096
1142
  if (!normalizedName && !normalizedLabels) {
1097
1143
  this.emit({
1098
- type: "update_agent_response",
1144
+ type: 'update_agent_response',
1099
1145
  payload: {
1100
1146
  requestId,
1101
1147
  agentId,
1102
1148
  accepted: false,
1103
- error: "Nothing to update (provide name and/or labels)",
1149
+ error: 'Nothing to update (provide name and/or labels)',
1104
1150
  },
1105
1151
  });
1106
1152
  return;
@@ -1123,34 +1169,32 @@ export class Session {
1123
1169
  await this.agentStorage.upsert({
1124
1170
  ...existing,
1125
1171
  ...(normalizedName ? { title: normalizedName } : {}),
1126
- ...(normalizedLabels
1127
- ? { labels: { ...existing.labels, ...normalizedLabels } }
1128
- : {}),
1172
+ ...(normalizedLabels ? { labels: { ...existing.labels, ...normalizedLabels } } : {}),
1129
1173
  });
1130
1174
  }
1131
1175
  this.emit({
1132
- type: "update_agent_response",
1176
+ type: 'update_agent_response',
1133
1177
  payload: { requestId, agentId, accepted: true, error: null },
1134
1178
  });
1135
1179
  }
1136
1180
  catch (error) {
1137
- this.sessionLogger.error({ err: error, agentId, requestId }, "session: update_agent_request error");
1181
+ this.sessionLogger.error({ err: error, agentId, requestId }, 'session: update_agent_request error');
1138
1182
  this.emit({
1139
- type: "activity_log",
1183
+ type: 'activity_log',
1140
1184
  payload: {
1141
1185
  id: uuidv4(),
1142
1186
  timestamp: new Date(),
1143
- type: "error",
1187
+ type: 'error',
1144
1188
  content: `Failed to update agent: ${error.message}`,
1145
1189
  },
1146
1190
  });
1147
1191
  this.emit({
1148
- type: "update_agent_response",
1192
+ type: 'update_agent_response',
1149
1193
  payload: {
1150
1194
  requestId,
1151
1195
  agentId,
1152
1196
  accepted: false,
1153
- error: error?.message ? String(error.message) : "Failed to update agent",
1197
+ error: error?.message ? String(error.message) : 'Failed to update agent',
1154
1198
  },
1155
1199
  });
1156
1200
  }
@@ -1164,7 +1208,7 @@ export class Session {
1164
1208
  };
1165
1209
  }
1166
1210
  resolveModeReadinessState(readiness, mode) {
1167
- if (mode === "voice_mode") {
1211
+ if (mode === 'voice_mode') {
1168
1212
  return readiness.realtimeVoice;
1169
1213
  }
1170
1214
  return readiness.dictation;
@@ -1202,11 +1246,11 @@ export class Session {
1202
1246
  async handleSetVoiceMode(enabled, agentId, requestId) {
1203
1247
  try {
1204
1248
  if (enabled) {
1205
- const unavailable = this.resolveVoiceFeatureUnavailableContext("voice_mode");
1249
+ const unavailable = this.resolveVoiceFeatureUnavailableContext('voice_mode');
1206
1250
  if (unavailable) {
1207
1251
  throw new VoiceFeatureUnavailableError(unavailable);
1208
1252
  }
1209
- const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? "", "set_voice_mode");
1253
+ const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? '', 'set_voice_mode');
1210
1254
  if (this.isVoiceMode &&
1211
1255
  this.voiceModeAgentId &&
1212
1256
  this.voiceModeAgentId !== normalizedAgentId) {
@@ -1219,10 +1263,10 @@ export class Session {
1219
1263
  this.isVoiceMode = true;
1220
1264
  this.sessionLogger.info({
1221
1265
  agentId: this.voiceModeAgentId,
1222
- }, "Voice mode enabled for existing agent");
1266
+ }, 'Voice mode enabled for existing agent');
1223
1267
  if (requestId) {
1224
1268
  this.emit({
1225
- type: "set_voice_mode_response",
1269
+ type: 'set_voice_mode_response',
1226
1270
  payload: {
1227
1271
  requestId,
1228
1272
  enabled: true,
@@ -1236,10 +1280,10 @@ export class Session {
1236
1280
  }
1237
1281
  await this.disableVoiceModeForActiveAgent(true);
1238
1282
  this.isVoiceMode = false;
1239
- this.sessionLogger.info("Voice mode disabled");
1283
+ this.sessionLogger.info('Voice mode disabled');
1240
1284
  if (requestId) {
1241
1285
  this.emit({
1242
- type: "set_voice_mode_response",
1286
+ type: 'set_voice_mode_response',
1243
1287
  payload: {
1244
1288
  requestId,
1245
1289
  enabled: false,
@@ -1251,16 +1295,16 @@ export class Session {
1251
1295
  }
1252
1296
  }
1253
1297
  catch (error) {
1254
- const errorMessage = error instanceof Error ? error.message : "Failed to set voice mode";
1298
+ const errorMessage = error instanceof Error ? error.message : 'Failed to set voice mode';
1255
1299
  const unavailable = this.getVoiceFeatureUnavailableResponseMetadata(error);
1256
1300
  this.sessionLogger.error({
1257
1301
  err: error,
1258
1302
  enabled,
1259
1303
  requestedAgentId: agentId ?? null,
1260
- }, "set_voice_mode failed");
1304
+ }, 'set_voice_mode failed');
1261
1305
  if (requestId) {
1262
1306
  this.emit({
1263
- type: "set_voice_mode_response",
1307
+ type: 'set_voice_mode_response',
1264
1308
  payload: {
1265
1309
  requestId,
1266
1310
  enabled: this.isVoiceMode,
@@ -1291,7 +1335,7 @@ export class Session {
1291
1335
  buildVoiceModeMcpServers(existing, socketPath) {
1292
1336
  const mcpStdio = this.voiceAgentMcpStdio;
1293
1337
  if (!mcpStdio) {
1294
- throw new Error("Voice MCP stdio bridge is not configured");
1338
+ throw new Error('Voice MCP stdio bridge is not configured');
1295
1339
  }
1296
1340
  return {
1297
1341
  ...(existing ?? {}),
@@ -1306,7 +1350,7 @@ export class Session {
1306
1350
  async enableVoiceModeForAgent(agentId) {
1307
1351
  const ensureVoiceSocket = this.ensureVoiceMcpSocketForAgent;
1308
1352
  if (!ensureVoiceSocket) {
1309
- throw new Error("Voice MCP socket bridge is not configured");
1353
+ throw new Error('Voice MCP socket bridge is not configured');
1310
1354
  }
1311
1355
  const existing = await this.ensureAgentLoaded(agentId);
1312
1356
  const socketPath = await ensureVoiceSocket(agentId);
@@ -1334,7 +1378,7 @@ export class Session {
1334
1378
  }
1335
1379
  async disableVoiceModeForActiveAgent(restoreAgentConfig) {
1336
1380
  this.clearVoiceModeInactivityTimeout();
1337
- this.cancelActiveVoiceDictationStream("voice mode disabled");
1381
+ this.cancelActiveVoiceDictationStream('voice mode disabled');
1338
1382
  const agentId = this.voiceModeAgentId;
1339
1383
  if (!agentId) {
1340
1384
  this.voiceModeBaseConfig = null;
@@ -1343,7 +1387,7 @@ export class Session {
1343
1387
  this.unregisterVoiceSpeakHandler?.(agentId);
1344
1388
  this.unregisterVoiceCallerContext?.(agentId);
1345
1389
  await this.removeVoiceMcpSocketForAgent?.(agentId).catch((error) => {
1346
- this.sessionLogger.warn({ err: error, agentId }, "Failed to remove voice MCP socket bridge on disable");
1390
+ this.sessionLogger.warn({ err: error, agentId }, 'Failed to remove voice MCP socket bridge on disable');
1347
1391
  });
1348
1392
  if (restoreAgentConfig && this.voiceModeBaseConfig) {
1349
1393
  const baseConfig = this.voiceModeBaseConfig;
@@ -1354,7 +1398,7 @@ export class Session {
1354
1398
  });
1355
1399
  }
1356
1400
  catch (error) {
1357
- this.sessionLogger.warn({ err: error, agentId }, "Failed to restore agent config while disabling voice mode");
1401
+ this.sessionLogger.warn({ err: error, agentId }, 'Failed to restore agent config while disabling voice mode');
1358
1402
  }
1359
1403
  }
1360
1404
  this.voiceModeBaseConfig = null;
@@ -1364,9 +1408,9 @@ export class Session {
1364
1408
  return dictationId.startsWith(VOICE_INTERNAL_DICTATION_ID_PREFIX);
1365
1409
  }
1366
1410
  handleDictationManagerMessage(msg) {
1367
- if (msg.type === "activity_log") {
1411
+ if (msg.type === 'activity_log') {
1368
1412
  const metadata = msg.payload.metadata;
1369
- const dictationId = metadata && typeof metadata.dictationId === "string" ? metadata.dictationId : null;
1413
+ const dictationId = metadata && typeof metadata.dictationId === 'string' ? metadata.dictationId : null;
1370
1414
  if (dictationId && this.isInternalVoiceDictationId(dictationId)) {
1371
1415
  return;
1372
1416
  }
@@ -1374,14 +1418,14 @@ export class Session {
1374
1418
  return;
1375
1419
  }
1376
1420
  const payloadWithDictationId = msg.payload;
1377
- const dictationId = payloadWithDictationId && typeof payloadWithDictationId.dictationId === "string"
1421
+ const dictationId = payloadWithDictationId && typeof payloadWithDictationId.dictationId === 'string'
1378
1422
  ? payloadWithDictationId.dictationId
1379
1423
  : null;
1380
1424
  if (!dictationId || !this.isInternalVoiceDictationId(dictationId)) {
1381
1425
  this.emit(msg);
1382
1426
  return;
1383
1427
  }
1384
- if (msg.type === "dictation_stream_final") {
1428
+ if (msg.type === 'dictation_stream_final') {
1385
1429
  if (dictationId !== this.activeVoiceDictationId || !this.activeVoiceDictationResolve) {
1386
1430
  return;
1387
1431
  }
@@ -1393,7 +1437,7 @@ export class Session {
1393
1437
  });
1394
1438
  return;
1395
1439
  }
1396
- if (msg.type === "dictation_stream_error") {
1440
+ if (msg.type === 'dictation_stream_error') {
1397
1441
  if (dictationId !== this.activeVoiceDictationId || !this.activeVoiceDictationReject) {
1398
1442
  return;
1399
1443
  }
@@ -1417,7 +1461,7 @@ export class Session {
1417
1461
  if (!dictationId) {
1418
1462
  return;
1419
1463
  }
1420
- this.sessionLogger.debug({ dictationId, reason }, "Cancelling active internal voice dictation stream");
1464
+ this.sessionLogger.debug({ dictationId, reason }, 'Cancelling active internal voice dictation stream');
1421
1465
  if (this.activeVoiceDictationReject) {
1422
1466
  this.activeVoiceDictationReject(new Error(`Voice dictation cancelled: ${reason}`));
1423
1467
  }
@@ -1432,7 +1476,7 @@ export class Session {
1432
1476
  return;
1433
1477
  }
1434
1478
  if (this.activeVoiceDictationId) {
1435
- await this.finalizeActiveVoiceDictationStream("voice format changed");
1479
+ await this.finalizeActiveVoiceDictationStream('voice format changed');
1436
1480
  }
1437
1481
  const dictationId = `${VOICE_INTERNAL_DICTATION_ID_PREFIX}${uuidv4()}`;
1438
1482
  let resolve = null;
@@ -1450,14 +1494,14 @@ export class Session {
1450
1494
  this.activeVoiceDictationResultPromise = resultPromise;
1451
1495
  this.activeVoiceDictationResolve = resolve;
1452
1496
  this.activeVoiceDictationReject = reject;
1453
- this.setPhase("transcribing");
1497
+ this.setPhase('transcribing');
1454
1498
  this.emit({
1455
- type: "activity_log",
1499
+ type: 'activity_log',
1456
1500
  payload: {
1457
1501
  id: uuidv4(),
1458
1502
  timestamp: new Date(),
1459
- type: "system",
1460
- content: "Transcribing audio...",
1503
+ type: 'system',
1504
+ content: 'Transcribing audio...',
1461
1505
  },
1462
1506
  });
1463
1507
  const startPromise = this.voiceStreamManager.handleStart(dictationId, format);
@@ -1482,7 +1526,7 @@ export class Session {
1482
1526
  await this.ensureActiveVoiceDictationStream(format);
1483
1527
  const dictationId = this.activeVoiceDictationId;
1484
1528
  if (!dictationId) {
1485
- throw new Error("Voice dictation stream did not initialize");
1529
+ throw new Error('Voice dictation stream did not initialize');
1486
1530
  }
1487
1531
  const seq = this.activeVoiceDictationNextSeq;
1488
1532
  this.activeVoiceDictationNextSeq += 1;
@@ -1513,7 +1557,7 @@ export class Session {
1513
1557
  return;
1514
1558
  }
1515
1559
  this.activeVoiceDictationFinalizePromise = (async () => {
1516
- this.sessionLogger.debug({ dictationId, finalSeq, reason }, "Finalizing internal voice dictation stream");
1560
+ this.sessionLogger.debug({ dictationId, finalSeq, reason }, 'Finalizing internal voice dictation stream');
1517
1561
  await this.voiceStreamManager.handleFinish(dictationId, finalSeq);
1518
1562
  const result = await resultPromise;
1519
1563
  this.resetActiveVoiceDictationState();
@@ -1524,12 +1568,12 @@ export class Session {
1524
1568
  isVoiceMode: this.isVoiceMode,
1525
1569
  transcriptLength: transcriptText.length,
1526
1570
  transcript: transcriptText,
1527
- }, "Transcription result");
1571
+ }, 'Transcription result');
1528
1572
  await this.handleTranscriptionResultPayload({
1529
1573
  text: result.text,
1530
1574
  requestId,
1531
1575
  ...(result.debugRecordingPath
1532
- ? { debugRecordingPath: result.debugRecordingPath, format: "audio/wav" }
1576
+ ? { debugRecordingPath: result.debugRecordingPath, format: 'audio/wav' }
1533
1577
  : {}),
1534
1578
  });
1535
1579
  })();
@@ -1538,14 +1582,14 @@ export class Session {
1538
1582
  }
1539
1583
  catch (error) {
1540
1584
  this.resetActiveVoiceDictationState();
1541
- this.setPhase("idle");
1542
- this.clearSpeechInProgress("transcription error");
1585
+ this.setPhase('idle');
1586
+ this.clearSpeechInProgress('transcription error');
1543
1587
  this.emit({
1544
- type: "activity_log",
1588
+ type: 'activity_log',
1545
1589
  payload: {
1546
1590
  id: uuidv4(),
1547
1591
  timestamp: new Date(),
1548
- type: "error",
1592
+ type: 'error',
1549
1593
  content: `Transcription error: ${error instanceof Error ? error.message : String(error)}`,
1550
1594
  },
1551
1595
  });
@@ -1561,14 +1605,14 @@ export class Session {
1561
1605
  await this.ensureAgentLoaded(agentId);
1562
1606
  }
1563
1607
  catch (error) {
1564
- this.handleAgentRunError(agentId, error, "Failed to initialize agent before sending prompt");
1608
+ this.handleAgentRunError(agentId, error, 'Failed to initialize agent before sending prompt');
1565
1609
  return;
1566
1610
  }
1567
1611
  try {
1568
1612
  await this.interruptAgentIfRunning(agentId);
1569
1613
  }
1570
1614
  catch (error) {
1571
- this.handleAgentRunError(agentId, error, "Failed to interrupt running agent before sending prompt");
1615
+ this.handleAgentRunError(agentId, error, 'Failed to interrupt running agent before sending prompt');
1572
1616
  return;
1573
1617
  }
1574
1618
  const prompt = this.buildAgentPrompt(text, images);
@@ -1588,7 +1632,7 @@ export class Session {
1588
1632
  */
1589
1633
  async handleCreateAgentRequest(msg) {
1590
1634
  const { config, worktreeName, requestId, initialPrompt, outputSchema, git, images, labels } = msg;
1591
- this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ""}`);
1635
+ this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ''}`);
1592
1636
  try {
1593
1637
  const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
1594
1638
  const snapshot = await this.agentManager.createAgent(sessionConfig, undefined, { labels });
@@ -1610,11 +1654,11 @@ export class Session {
1610
1654
  catch (promptError) {
1611
1655
  this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
1612
1656
  this.emit({
1613
- type: "activity_log",
1657
+ type: 'activity_log',
1614
1658
  payload: {
1615
1659
  id: uuidv4(),
1616
1660
  timestamp: new Date(),
1617
- type: "error",
1661
+ type: 'error',
1618
1662
  content: `Initial prompt failed: ${promptError?.message ?? promptError}`,
1619
1663
  },
1620
1664
  });
@@ -1626,9 +1670,9 @@ export class Session {
1626
1670
  throw new Error(`Agent ${snapshot.id} not found after creation`);
1627
1671
  }
1628
1672
  this.emit({
1629
- type: "status",
1673
+ type: 'status',
1630
1674
  payload: {
1631
- status: "agent_created",
1675
+ status: 'agent_created',
1632
1676
  agentId: snapshot.id,
1633
1677
  requestId,
1634
1678
  agent: agentPayload,
@@ -1656,23 +1700,23 @@ export class Session {
1656
1700
  this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1657
1701
  }
1658
1702
  catch (error) {
1659
- this.sessionLogger.error({ err: error }, "Failed to create agent");
1703
+ this.sessionLogger.error({ err: error }, 'Failed to create agent');
1660
1704
  if (requestId) {
1661
1705
  this.emit({
1662
- type: "status",
1706
+ type: 'status',
1663
1707
  payload: {
1664
- status: "agent_create_failed",
1708
+ status: 'agent_create_failed',
1665
1709
  requestId,
1666
1710
  error: error?.message ?? String(error),
1667
1711
  },
1668
1712
  });
1669
1713
  }
1670
1714
  this.emit({
1671
- type: "activity_log",
1715
+ type: 'activity_log',
1672
1716
  payload: {
1673
1717
  id: uuidv4(),
1674
1718
  timestamp: new Date(),
1675
- type: "error",
1719
+ type: 'error',
1676
1720
  content: `Failed to create agent: ${error.message}`,
1677
1721
  },
1678
1722
  });
@@ -1681,14 +1725,14 @@ export class Session {
1681
1725
  async handleResumeAgentRequest(msg) {
1682
1726
  const { handle, overrides, requestId } = msg;
1683
1727
  if (!handle) {
1684
- this.sessionLogger.warn("Resume request missing persistence handle");
1728
+ this.sessionLogger.warn('Resume request missing persistence handle');
1685
1729
  this.emit({
1686
- type: "activity_log",
1730
+ type: 'activity_log',
1687
1731
  payload: {
1688
1732
  id: uuidv4(),
1689
1733
  timestamp: new Date(),
1690
- type: "error",
1691
- content: "Unable to resume agent: missing persistence handle",
1734
+ type: 'error',
1735
+ content: 'Unable to resume agent: missing persistence handle',
1692
1736
  },
1693
1737
  });
1694
1738
  return;
@@ -1705,9 +1749,9 @@ export class Session {
1705
1749
  throw new Error(`Agent ${snapshot.id} not found after resume`);
1706
1750
  }
1707
1751
  this.emit({
1708
- type: "status",
1752
+ type: 'status',
1709
1753
  payload: {
1710
- status: "agent_resumed",
1754
+ status: 'agent_resumed',
1711
1755
  agentId: snapshot.id,
1712
1756
  requestId,
1713
1757
  timelineSize,
@@ -1717,13 +1761,13 @@ export class Session {
1717
1761
  }
1718
1762
  }
1719
1763
  catch (error) {
1720
- this.sessionLogger.error({ err: error }, "Failed to resume agent");
1764
+ this.sessionLogger.error({ err: error }, 'Failed to resume agent');
1721
1765
  this.emit({
1722
- type: "activity_log",
1766
+ type: 'activity_log',
1723
1767
  payload: {
1724
1768
  id: uuidv4(),
1725
1769
  timestamp: new Date(),
1726
- type: "error",
1770
+ type: 'error',
1727
1771
  content: `Failed to resume agent: ${error.message}`,
1728
1772
  },
1729
1773
  });
@@ -1760,9 +1804,9 @@ export class Session {
1760
1804
  const timelineSize = this.agentManager.getTimeline(agentId).length;
1761
1805
  if (requestId) {
1762
1806
  this.emit({
1763
- type: "status",
1807
+ type: 'status',
1764
1808
  payload: {
1765
- status: "agent_refreshed",
1809
+ status: 'agent_refreshed',
1766
1810
  agentId,
1767
1811
  requestId,
1768
1812
  timelineSize,
@@ -1773,11 +1817,11 @@ export class Session {
1773
1817
  catch (error) {
1774
1818
  this.sessionLogger.error({ err: error, agentId }, `Failed to refresh agent ${agentId}`);
1775
1819
  this.emit({
1776
- type: "activity_log",
1820
+ type: 'activity_log',
1777
1821
  payload: {
1778
1822
  id: uuidv4(),
1779
1823
  timestamp: new Date(),
1780
- type: "error",
1824
+ type: 'error',
1781
1825
  content: `Failed to refresh agent: ${error.message}`,
1782
1826
  },
1783
1827
  });
@@ -1789,7 +1833,7 @@ export class Session {
1789
1833
  await this.interruptAgentIfRunning(agentId);
1790
1834
  }
1791
1835
  catch (error) {
1792
- this.handleAgentRunError(agentId, error, "Failed to cancel running agent on request");
1836
+ this.handleAgentRunError(agentId, error, 'Failed to cancel running agent on request');
1793
1837
  }
1794
1838
  }
1795
1839
  async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, _labels) {
@@ -1811,14 +1855,14 @@ export class Session {
1811
1855
  }
1812
1856
  else {
1813
1857
  // Resolve current branch name from HEAD
1814
- const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
1858
+ const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
1815
1859
  cwd,
1816
1860
  env: READ_ONLY_GIT_ENV,
1817
1861
  });
1818
1862
  targetBranch = stdout.trim();
1819
1863
  }
1820
1864
  if (!targetBranch) {
1821
- throw new Error("A branch name is required when creating a worktree.");
1865
+ throw new Error('A branch name is required when creating a worktree.');
1822
1866
  }
1823
1867
  this.sessionLogger.info({ worktreeSlug: normalized.worktreeSlug ?? targetBranch, branch: targetBranch }, `Creating worktree '${normalized.worktreeSlug ?? targetBranch}' for branch ${targetBranch}`);
1824
1868
  const createdWorktree = await createAgentWorktree({
@@ -1856,7 +1900,7 @@ export class Session {
1856
1900
  cwd: msg.cwd ? expandTilde(msg.cwd) : undefined,
1857
1901
  });
1858
1902
  this.emit({
1859
- type: "list_provider_models_response",
1903
+ type: 'list_provider_models_response',
1860
1904
  payload: {
1861
1905
  provider: msg.provider,
1862
1906
  models,
@@ -1869,7 +1913,7 @@ export class Session {
1869
1913
  catch (error) {
1870
1914
  this.sessionLogger.error({ err: error, provider: msg.provider }, `Failed to list models for ${msg.provider}`);
1871
1915
  this.emit({
1872
- type: "list_provider_models_response",
1916
+ type: 'list_provider_models_response',
1873
1917
  payload: {
1874
1918
  provider: msg.provider,
1875
1919
  error: error?.message ?? String(error),
@@ -1884,7 +1928,7 @@ export class Session {
1884
1928
  try {
1885
1929
  const providers = await this.agentManager.listProviderAvailability();
1886
1930
  this.emit({
1887
- type: "list_available_providers_response",
1931
+ type: 'list_available_providers_response',
1888
1932
  payload: {
1889
1933
  providers,
1890
1934
  error: null,
@@ -1894,9 +1938,9 @@ export class Session {
1894
1938
  });
1895
1939
  }
1896
1940
  catch (error) {
1897
- this.sessionLogger.error({ err: error }, "Failed to list provider availability");
1941
+ this.sessionLogger.error({ err: error }, 'Failed to list provider availability');
1898
1942
  this.emit({
1899
- type: "list_available_providers_response",
1943
+ type: 'list_available_providers_response',
1900
1944
  payload: {
1901
1945
  providers: [],
1902
1946
  error: error?.message ?? String(error),
@@ -1936,7 +1980,7 @@ export class Session {
1936
1980
  };
1937
1981
  }));
1938
1982
  this.emit({
1939
- type: "speech_models_list_response",
1983
+ type: 'speech_models_list_response',
1940
1984
  payload: {
1941
1985
  modelsDir,
1942
1986
  models,
@@ -1946,18 +1990,16 @@ export class Session {
1946
1990
  }
1947
1991
  async handleSpeechModelsDownloadRequest(msg) {
1948
1992
  const modelsDir = this.localSpeechModelsDir;
1949
- const modelIdsRaw = msg.modelIds && msg.modelIds.length > 0
1950
- ? msg.modelIds
1951
- : this.defaultLocalSpeechModelIds;
1993
+ const modelIdsRaw = msg.modelIds && msg.modelIds.length > 0 ? msg.modelIds : this.defaultLocalSpeechModelIds;
1952
1994
  const allModelIds = new Set(listLocalSpeechModels().map((m) => m.id));
1953
1995
  const invalid = modelIdsRaw.filter((id) => !allModelIds.has(id));
1954
1996
  if (invalid.length > 0) {
1955
1997
  this.emit({
1956
- type: "speech_models_download_response",
1998
+ type: 'speech_models_download_response',
1957
1999
  payload: {
1958
2000
  modelsDir,
1959
2001
  downloadedModelIds: [],
1960
- error: `Unknown speech model id(s): ${invalid.join(", ")}`,
2002
+ error: `Unknown speech model id(s): ${invalid.join(', ')}`,
1961
2003
  requestId: msg.requestId,
1962
2004
  },
1963
2005
  });
@@ -1971,7 +2013,7 @@ export class Session {
1971
2013
  logger: this.sessionLogger,
1972
2014
  });
1973
2015
  this.emit({
1974
- type: "speech_models_download_response",
2016
+ type: 'speech_models_download_response',
1975
2017
  payload: {
1976
2018
  modelsDir,
1977
2019
  downloadedModelIds: modelIds,
@@ -1981,9 +2023,9 @@ export class Session {
1981
2023
  });
1982
2024
  }
1983
2025
  catch (error) {
1984
- this.sessionLogger.error({ err: error, modelIds }, "Failed to download speech models");
2026
+ this.sessionLogger.error({ err: error, modelIds }, 'Failed to download speech models');
1985
2027
  this.emit({
1986
- type: "speech_models_download_response",
2028
+ type: 'speech_models_download_response',
1987
2029
  payload: {
1988
2030
  modelsDir,
1989
2031
  downloadedModelIds: [],
@@ -2009,9 +2051,7 @@ export class Session {
2009
2051
  const baseBranch = merged.baseBranch?.trim() || undefined;
2010
2052
  const createWorktree = Boolean(merged.createWorktree);
2011
2053
  const createNewBranch = Boolean(merged.createNewBranch);
2012
- const normalizedBranchName = merged.newBranchName
2013
- ? slugify(merged.newBranchName)
2014
- : undefined;
2054
+ const normalizedBranchName = merged.newBranchName ? slugify(merged.newBranchName) : undefined;
2015
2055
  const normalizedWorktreeSlug = merged.worktreeSlug
2016
2056
  ? slugify(merged.worktreeSlug)
2017
2057
  : normalizedBranchName;
@@ -2019,17 +2059,17 @@ export class Session {
2019
2059
  return null;
2020
2060
  }
2021
2061
  if (baseBranch) {
2022
- this.assertSafeGitRef(baseBranch, "base branch");
2062
+ this.assertSafeGitRef(baseBranch, 'base branch');
2023
2063
  }
2024
2064
  if (createWorktree && !baseBranch) {
2025
- throw new Error("Base branch is required when creating a worktree");
2065
+ throw new Error('Base branch is required when creating a worktree');
2026
2066
  }
2027
2067
  if (createNewBranch && !baseBranch) {
2028
- throw new Error("Base branch is required when creating a new branch");
2068
+ throw new Error('Base branch is required when creating a new branch');
2029
2069
  }
2030
2070
  if (createNewBranch) {
2031
2071
  if (!normalizedBranchName) {
2032
- throw new Error("New branch name is required");
2072
+ throw new Error('New branch name is required');
2033
2073
  }
2034
2074
  const validation = validateBranchSlug(normalizedBranchName);
2035
2075
  if (!validation.valid) {
@@ -2051,24 +2091,24 @@ export class Session {
2051
2091
  };
2052
2092
  }
2053
2093
  assertSafeGitRef(ref, label) {
2054
- if (!SAFE_GIT_REF_PATTERN.test(ref) || ref.includes("..") || ref.includes("@{")) {
2094
+ if (!SAFE_GIT_REF_PATTERN.test(ref) || ref.includes('..') || ref.includes('@{')) {
2055
2095
  throw new Error(`Invalid ${label}: ${ref}`);
2056
2096
  }
2057
2097
  }
2058
2098
  toCheckoutError(error) {
2059
2099
  if (error instanceof NotGitRepoError) {
2060
- return { code: "NOT_GIT_REPO", message: error.message };
2100
+ return { code: 'NOT_GIT_REPO', message: error.message };
2061
2101
  }
2062
2102
  if (error instanceof MergeConflictError) {
2063
- return { code: "MERGE_CONFLICT", message: error.message };
2103
+ return { code: 'MERGE_CONFLICT', message: error.message };
2064
2104
  }
2065
2105
  if (error instanceof MergeFromBaseConflictError) {
2066
- return { code: "MERGE_CONFLICT", message: error.message };
2106
+ return { code: 'MERGE_CONFLICT', message: error.message };
2067
2107
  }
2068
2108
  if (error instanceof Error) {
2069
- return { code: "UNKNOWN", message: error.message };
2109
+ return { code: 'UNKNOWN', message: error.message };
2070
2110
  }
2071
- return { code: "UNKNOWN", message: String(error) };
2111
+ return { code: 'UNKNOWN', message: String(error) };
2072
2112
  }
2073
2113
  isPathWithinRoot(rootPath, candidatePath) {
2074
2114
  const resolvedRoot = resolve(rootPath);
@@ -2079,47 +2119,47 @@ export class Session {
2079
2119
  return resolvedCandidate.startsWith(resolvedRoot + sep);
2080
2120
  }
2081
2121
  async generateCommitMessage(cwd) {
2082
- const diff = await getCheckoutDiff(cwd, { mode: "uncommitted", includeStructured: true }, { paseoHome: this.paseoHome });
2122
+ const diff = await getCheckoutDiff(cwd, { mode: 'uncommitted', includeStructured: true }, { paseoHome: this.paseoHome });
2083
2123
  const schema = z.object({
2084
2124
  message: z
2085
2125
  .string()
2086
2126
  .min(1)
2087
2127
  .max(72)
2088
- .describe("Concise git commit message, imperative mood, no trailing period."),
2128
+ .describe('Concise git commit message, imperative mood, no trailing period.'),
2089
2129
  });
2090
2130
  const fileList = diff.structured && diff.structured.length > 0
2091
2131
  ? [
2092
- "Files changed:",
2132
+ 'Files changed:',
2093
2133
  ...diff.structured.map((file) => {
2094
- const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
2095
- const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
2134
+ const changeType = file.isNew ? 'A' : file.isDeleted ? 'D' : 'M';
2135
+ const status = file.status && file.status !== 'ok' ? ` [${file.status}]` : '';
2096
2136
  return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
2097
2137
  }),
2098
- ].join("\n")
2099
- : "Files changed: (unknown)";
2138
+ ].join('\n')
2139
+ : 'Files changed: (unknown)';
2100
2140
  const maxPatchChars = 120000;
2101
2141
  const patch = diff.diff.length > maxPatchChars
2102
2142
  ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
2103
2143
  : diff.diff;
2104
2144
  const prompt = [
2105
- "Write a concise git commit message for the changes below.",
2145
+ 'Write a concise git commit message for the changes below.',
2106
2146
  "Return JSON only with a single field 'message'.",
2107
- "",
2147
+ '',
2108
2148
  fileList,
2109
- "",
2110
- patch.length > 0 ? patch : "(No diff available)",
2111
- ].join("\n");
2149
+ '',
2150
+ patch.length > 0 ? patch : '(No diff available)',
2151
+ ].join('\n');
2112
2152
  try {
2113
2153
  const result = await generateStructuredAgentResponseWithFallback({
2114
2154
  manager: this.agentManager,
2115
2155
  cwd,
2116
2156
  prompt,
2117
2157
  schema,
2118
- schemaName: "CommitMessage",
2158
+ schemaName: 'CommitMessage',
2119
2159
  maxRetries: 2,
2120
2160
  providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2121
2161
  agentConfigOverrides: {
2122
- title: "Commit generator",
2162
+ title: 'Commit generator',
2123
2163
  internal: true,
2124
2164
  },
2125
2165
  });
@@ -2128,14 +2168,14 @@ export class Session {
2128
2168
  catch (error) {
2129
2169
  if (error instanceof StructuredAgentResponseError ||
2130
2170
  error instanceof StructuredAgentFallbackError) {
2131
- return "Update files";
2171
+ return 'Update files';
2132
2172
  }
2133
2173
  throw error;
2134
2174
  }
2135
2175
  }
2136
2176
  async generatePullRequestText(cwd, baseRef) {
2137
2177
  const diff = await getCheckoutDiff(cwd, {
2138
- mode: "base",
2178
+ mode: 'base',
2139
2179
  baseRef,
2140
2180
  includeStructured: true,
2141
2181
  }, { paseoHome: this.paseoHome });
@@ -2145,37 +2185,37 @@ export class Session {
2145
2185
  });
2146
2186
  const fileList = diff.structured && diff.structured.length > 0
2147
2187
  ? [
2148
- "Files changed:",
2188
+ 'Files changed:',
2149
2189
  ...diff.structured.map((file) => {
2150
- const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
2151
- const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
2190
+ const changeType = file.isNew ? 'A' : file.isDeleted ? 'D' : 'M';
2191
+ const status = file.status && file.status !== 'ok' ? ` [${file.status}]` : '';
2152
2192
  return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
2153
2193
  }),
2154
- ].join("\n")
2155
- : "Files changed: (unknown)";
2194
+ ].join('\n')
2195
+ : 'Files changed: (unknown)';
2156
2196
  const maxPatchChars = 200000;
2157
2197
  const patch = diff.diff.length > maxPatchChars
2158
2198
  ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
2159
2199
  : diff.diff;
2160
2200
  const prompt = [
2161
- "Write a pull request title and body for the changes below.",
2201
+ 'Write a pull request title and body for the changes below.',
2162
2202
  "Return JSON only with fields 'title' and 'body'.",
2163
- "",
2203
+ '',
2164
2204
  fileList,
2165
- "",
2166
- patch.length > 0 ? patch : "(No diff available)",
2167
- ].join("\n");
2205
+ '',
2206
+ patch.length > 0 ? patch : '(No diff available)',
2207
+ ].join('\n');
2168
2208
  try {
2169
2209
  return await generateStructuredAgentResponseWithFallback({
2170
2210
  manager: this.agentManager,
2171
2211
  cwd,
2172
2212
  prompt,
2173
2213
  schema,
2174
- schemaName: "PullRequest",
2214
+ schemaName: 'PullRequest',
2175
2215
  maxRetries: 2,
2176
2216
  providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2177
2217
  agentConfigOverrides: {
2178
- title: "PR generator",
2218
+ title: 'PR generator',
2179
2219
  internal: true,
2180
2220
  },
2181
2221
  });
@@ -2184,8 +2224,8 @@ export class Session {
2184
2224
  if (error instanceof StructuredAgentResponseError ||
2185
2225
  error instanceof StructuredAgentFallbackError) {
2186
2226
  return {
2187
- title: "Update changes",
2188
- body: "Automated PR generated by Paseo.",
2227
+ title: 'Update changes',
2228
+ body: 'Automated PR generated by Paseo.',
2189
2229
  };
2190
2230
  }
2191
2231
  throw error;
@@ -2194,12 +2234,12 @@ export class Session {
2194
2234
  async ensureCleanWorkingTree(cwd) {
2195
2235
  const dirty = await this.isWorkingTreeDirty(cwd);
2196
2236
  if (dirty) {
2197
- throw new Error("Working directory has uncommitted changes. Commit or stash before switching branches.");
2237
+ throw new Error('Working directory has uncommitted changes. Commit or stash before switching branches.');
2198
2238
  }
2199
2239
  }
2200
2240
  async isWorkingTreeDirty(cwd) {
2201
2241
  try {
2202
- const { stdout } = await execAsync("git status --porcelain", {
2242
+ const { stdout } = await execAsync('git status --porcelain', {
2203
2243
  cwd,
2204
2244
  env: READ_ONLY_GIT_ENV,
2205
2245
  });
@@ -2210,14 +2250,14 @@ export class Session {
2210
2250
  }
2211
2251
  }
2212
2252
  async checkoutExistingBranch(cwd, branch) {
2213
- this.assertSafeGitRef(branch, "branch");
2253
+ this.assertSafeGitRef(branch, 'branch');
2214
2254
  try {
2215
2255
  await execAsync(`git rev-parse --verify ${branch}`, { cwd });
2216
2256
  }
2217
2257
  catch (error) {
2218
2258
  throw new Error(`Branch not found: ${branch}`);
2219
2259
  }
2220
- const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
2260
+ const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
2221
2261
  cwd,
2222
2262
  });
2223
2263
  const current = stdout.trim();
@@ -2229,7 +2269,7 @@ export class Session {
2229
2269
  }
2230
2270
  async createBranchFromBase(params) {
2231
2271
  const { cwd, baseBranch, newBranchName } = params;
2232
- this.assertSafeGitRef(baseBranch, "base branch");
2272
+ this.assertSafeGitRef(baseBranch, 'base branch');
2233
2273
  try {
2234
2274
  await execAsync(`git rev-parse --verify ${baseBranch}`, { cwd });
2235
2275
  }
@@ -2260,99 +2300,97 @@ export class Session {
2260
2300
  * Handle set agent mode request
2261
2301
  */
2262
2302
  async handleSetAgentModeRequest(agentId, modeId, requestId) {
2263
- this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request");
2303
+ this.sessionLogger.info({ agentId, modeId, requestId }, 'session: set_agent_mode_request');
2264
2304
  try {
2265
2305
  await this.agentManager.setAgentMode(agentId, modeId);
2266
- this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request success");
2306
+ this.sessionLogger.info({ agentId, modeId, requestId }, 'session: set_agent_mode_request success');
2267
2307
  this.emit({
2268
- type: "set_agent_mode_response",
2308
+ type: 'set_agent_mode_response',
2269
2309
  payload: { requestId, agentId, accepted: true, error: null },
2270
2310
  });
2271
2311
  }
2272
2312
  catch (error) {
2273
- this.sessionLogger.error({ err: error, agentId, modeId, requestId }, "session: set_agent_mode_request error");
2313
+ this.sessionLogger.error({ err: error, agentId, modeId, requestId }, 'session: set_agent_mode_request error');
2274
2314
  this.emit({
2275
- type: "activity_log",
2315
+ type: 'activity_log',
2276
2316
  payload: {
2277
2317
  id: uuidv4(),
2278
2318
  timestamp: new Date(),
2279
- type: "error",
2319
+ type: 'error',
2280
2320
  content: `Failed to set agent mode: ${error.message}`,
2281
2321
  },
2282
2322
  });
2283
2323
  this.emit({
2284
- type: "set_agent_mode_response",
2324
+ type: 'set_agent_mode_response',
2285
2325
  payload: {
2286
2326
  requestId,
2287
2327
  agentId,
2288
2328
  accepted: false,
2289
- error: error?.message ? String(error.message) : "Failed to set agent mode",
2329
+ error: error?.message ? String(error.message) : 'Failed to set agent mode',
2290
2330
  },
2291
2331
  });
2292
2332
  }
2293
2333
  }
2294
2334
  async handleSetAgentModelRequest(agentId, modelId, requestId) {
2295
- this.sessionLogger.info({ agentId, modelId, requestId }, "session: set_agent_model_request");
2335
+ this.sessionLogger.info({ agentId, modelId, requestId }, 'session: set_agent_model_request');
2296
2336
  try {
2297
2337
  await this.agentManager.setAgentModel(agentId, modelId);
2298
- this.sessionLogger.info({ agentId, modelId, requestId }, "session: set_agent_model_request success");
2338
+ this.sessionLogger.info({ agentId, modelId, requestId }, 'session: set_agent_model_request success');
2299
2339
  this.emit({
2300
- type: "set_agent_model_response",
2340
+ type: 'set_agent_model_response',
2301
2341
  payload: { requestId, agentId, accepted: true, error: null },
2302
2342
  });
2303
2343
  }
2304
2344
  catch (error) {
2305
- this.sessionLogger.error({ err: error, agentId, modelId, requestId }, "session: set_agent_model_request error");
2345
+ this.sessionLogger.error({ err: error, agentId, modelId, requestId }, 'session: set_agent_model_request error');
2306
2346
  this.emit({
2307
- type: "activity_log",
2347
+ type: 'activity_log',
2308
2348
  payload: {
2309
2349
  id: uuidv4(),
2310
2350
  timestamp: new Date(),
2311
- type: "error",
2351
+ type: 'error',
2312
2352
  content: `Failed to set agent model: ${error.message}`,
2313
2353
  },
2314
2354
  });
2315
2355
  this.emit({
2316
- type: "set_agent_model_response",
2356
+ type: 'set_agent_model_response',
2317
2357
  payload: {
2318
2358
  requestId,
2319
2359
  agentId,
2320
2360
  accepted: false,
2321
- error: error?.message ? String(error.message) : "Failed to set agent model",
2361
+ error: error?.message ? String(error.message) : 'Failed to set agent model',
2322
2362
  },
2323
2363
  });
2324
2364
  }
2325
2365
  }
2326
2366
  async handleSetAgentThinkingRequest(agentId, thinkingOptionId, requestId) {
2327
- this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request");
2367
+ this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request');
2328
2368
  try {
2329
2369
  await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
2330
- this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request success");
2370
+ this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request success');
2331
2371
  this.emit({
2332
- type: "set_agent_thinking_response",
2372
+ type: 'set_agent_thinking_response',
2333
2373
  payload: { requestId, agentId, accepted: true, error: null },
2334
2374
  });
2335
2375
  }
2336
2376
  catch (error) {
2337
- this.sessionLogger.error({ err: error, agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request error");
2377
+ this.sessionLogger.error({ err: error, agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request error');
2338
2378
  this.emit({
2339
- type: "activity_log",
2379
+ type: 'activity_log',
2340
2380
  payload: {
2341
2381
  id: uuidv4(),
2342
2382
  timestamp: new Date(),
2343
- type: "error",
2383
+ type: 'error',
2344
2384
  content: `Failed to set agent thinking option: ${error.message}`,
2345
2385
  },
2346
2386
  });
2347
2387
  this.emit({
2348
- type: "set_agent_thinking_response",
2388
+ type: 'set_agent_thinking_response',
2349
2389
  payload: {
2350
2390
  requestId,
2351
2391
  agentId,
2352
2392
  accepted: false,
2353
- error: error?.message
2354
- ? String(error.message)
2355
- : "Failed to set agent thinking option",
2393
+ error: error?.message ? String(error.message) : 'Failed to set agent thinking option',
2356
2394
  },
2357
2395
  });
2358
2396
  }
@@ -2362,12 +2400,12 @@ export class Session {
2362
2400
  */
2363
2401
  async handleClearAgentAttention(agentId) {
2364
2402
  const agentIds = Array.isArray(agentId) ? agentId : [agentId];
2365
- this.sessionLogger.debug({ agentIds }, `Clearing attention for ${agentIds.length} agent(s): ${agentIds.join(", ")}`);
2403
+ this.sessionLogger.debug({ agentIds }, `Clearing attention for ${agentIds.length} agent(s): ${agentIds.join(', ')}`);
2366
2404
  try {
2367
2405
  await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id)));
2368
2406
  }
2369
2407
  catch (error) {
2370
- this.sessionLogger.error({ err: error, agentIds }, "Failed to clear agent attention");
2408
+ this.sessionLogger.error({ err: error, agentIds }, 'Failed to clear agent attention');
2371
2409
  // Don't throw - this is not critical
2372
2410
  }
2373
2411
  }
@@ -2391,116 +2429,69 @@ export class Session {
2391
2429
  */
2392
2430
  handleRegisterPushToken(token) {
2393
2431
  this.pushTokenStore.addToken(token);
2394
- this.sessionLogger.info("Registered push token");
2432
+ this.sessionLogger.info('Registered push token');
2395
2433
  }
2396
2434
  /**
2397
2435
  * Handle list commands request for an agent
2398
2436
  */
2399
- async handleListCommandsRequest(agentId, requestId) {
2400
- this.sessionLogger.debug({ agentId }, `Handling list commands request for agent ${agentId}`);
2437
+ async handleListCommandsRequest(msg) {
2438
+ const { agentId, requestId, draftConfig } = msg;
2439
+ this.sessionLogger.debug({ agentId, draftConfig }, `Handling list commands request for agent ${agentId}`);
2401
2440
  try {
2402
2441
  const agents = this.agentManager.listAgents();
2403
2442
  const agent = agents.find((a) => a.id === agentId);
2404
- if (!agent) {
2443
+ if (agent?.session?.listCommands) {
2444
+ const commands = await agent.session.listCommands();
2405
2445
  this.emit({
2406
- type: "list_commands_response",
2446
+ type: 'list_commands_response',
2407
2447
  payload: {
2408
2448
  agentId,
2409
- commands: [],
2410
- error: `Agent not found: ${agentId}`,
2449
+ commands,
2450
+ error: null,
2411
2451
  requestId,
2412
2452
  },
2413
2453
  });
2414
2454
  return;
2415
2455
  }
2416
- const session = agent.session;
2417
- if (!session || !session.listCommands) {
2456
+ if (!agent && draftConfig) {
2457
+ const sessionConfig = {
2458
+ provider: draftConfig.provider,
2459
+ cwd: expandTilde(draftConfig.cwd),
2460
+ ...(draftConfig.modeId ? { modeId: draftConfig.modeId } : {}),
2461
+ ...(draftConfig.model ? { model: draftConfig.model } : {}),
2462
+ ...(draftConfig.thinkingOptionId
2463
+ ? { thinkingOptionId: draftConfig.thinkingOptionId }
2464
+ : {}),
2465
+ };
2466
+ const commands = await this.agentManager.listDraftCommands(sessionConfig);
2418
2467
  this.emit({
2419
- type: "list_commands_response",
2468
+ type: 'list_commands_response',
2420
2469
  payload: {
2421
2470
  agentId,
2422
- commands: [],
2423
- error: `Agent does not support listing commands`,
2471
+ commands,
2472
+ error: null,
2424
2473
  requestId,
2425
2474
  },
2426
2475
  });
2427
2476
  return;
2428
2477
  }
2429
- const commands = await session.listCommands();
2430
- this.emit({
2431
- type: "list_commands_response",
2432
- payload: {
2433
- agentId,
2434
- commands,
2435
- error: null,
2436
- requestId,
2437
- },
2438
- });
2439
- }
2440
- catch (error) {
2441
- this.sessionLogger.error({ err: error, agentId }, "Failed to list commands");
2442
2478
  this.emit({
2443
- type: "list_commands_response",
2479
+ type: 'list_commands_response',
2444
2480
  payload: {
2445
2481
  agentId,
2446
2482
  commands: [],
2447
- error: error.message,
2448
- requestId,
2449
- },
2450
- });
2451
- }
2452
- }
2453
- /**
2454
- * Handle execute command request for an agent
2455
- */
2456
- async handleExecuteCommandRequest(agentId, commandName, args, requestId) {
2457
- this.sessionLogger.debug({ agentId, commandName }, `Handling execute command request for agent ${agentId}`);
2458
- try {
2459
- const agents = this.agentManager.listAgents();
2460
- const agent = agents.find((a) => a.id === agentId);
2461
- if (!agent) {
2462
- this.emit({
2463
- type: "execute_command_response",
2464
- payload: {
2465
- agentId,
2466
- result: null,
2467
- error: `Agent not found: ${agentId}`,
2468
- requestId,
2469
- },
2470
- });
2471
- return;
2472
- }
2473
- const session = agent.session;
2474
- if (!session || !session.executeCommand) {
2475
- this.emit({
2476
- type: "execute_command_response",
2477
- payload: {
2478
- agentId,
2479
- result: null,
2480
- error: `Agent does not support executing commands`,
2481
- requestId,
2482
- },
2483
- });
2484
- return;
2485
- }
2486
- const result = await session.executeCommand(commandName, args);
2487
- this.emit({
2488
- type: "execute_command_response",
2489
- payload: {
2490
- agentId,
2491
- result,
2492
- error: null,
2483
+ error: agent ? `Agent does not support listing commands` : `Agent not found: ${agentId}`,
2493
2484
  requestId,
2494
2485
  },
2495
2486
  });
2496
2487
  }
2497
2488
  catch (error) {
2498
- this.sessionLogger.error({ err: error, agentId, commandName }, "Failed to execute command");
2489
+ this.sessionLogger.error({ err: error, agentId, draftConfig }, 'Failed to list commands');
2499
2490
  this.emit({
2500
- type: "execute_command_response",
2491
+ type: 'list_commands_response',
2501
2492
  payload: {
2502
2493
  agentId,
2503
- result: null,
2494
+ commands: [],
2504
2495
  error: error.message,
2505
2496
  requestId,
2506
2497
  },
@@ -2517,13 +2508,13 @@ export class Session {
2517
2508
  this.sessionLogger.debug({ agentId }, `Permission response forwarded to agent ${agentId}`);
2518
2509
  }
2519
2510
  catch (error) {
2520
- this.sessionLogger.error({ err: error, agentId, requestId }, "Failed to respond to permission");
2511
+ this.sessionLogger.error({ err: error, agentId, requestId }, 'Failed to respond to permission');
2521
2512
  this.emit({
2522
- type: "activity_log",
2513
+ type: 'activity_log',
2523
2514
  payload: {
2524
2515
  id: uuidv4(),
2525
2516
  timestamp: new Date(),
2526
- type: "error",
2517
+ type: 'error',
2527
2518
  content: `Failed to respond to permission: ${error.message}`,
2528
2519
  },
2529
2520
  });
@@ -2537,7 +2528,7 @@ export class Session {
2537
2528
  const status = await getCheckoutStatus(resolvedCwd, { paseoHome: this.paseoHome });
2538
2529
  if (!status.isGit) {
2539
2530
  this.emit({
2540
- type: "checkout_status_response",
2531
+ type: 'checkout_status_response',
2541
2532
  payload: {
2542
2533
  cwd,
2543
2534
  isGit: false,
@@ -2547,6 +2538,7 @@ export class Session {
2547
2538
  baseRef: null,
2548
2539
  aheadBehind: null,
2549
2540
  aheadOfOrigin: null,
2541
+ behindOfOrigin: null,
2550
2542
  hasRemote: false,
2551
2543
  remoteUrl: null,
2552
2544
  isPaseoOwnedWorktree: false,
@@ -2558,7 +2550,7 @@ export class Session {
2558
2550
  }
2559
2551
  if (status.isPaseoOwnedWorktree) {
2560
2552
  this.emit({
2561
- type: "checkout_status_response",
2553
+ type: 'checkout_status_response',
2562
2554
  payload: {
2563
2555
  cwd,
2564
2556
  isGit: true,
@@ -2569,6 +2561,7 @@ export class Session {
2569
2561
  baseRef: status.baseRef,
2570
2562
  aheadBehind: status.aheadBehind ?? null,
2571
2563
  aheadOfOrigin: status.aheadOfOrigin ?? null,
2564
+ behindOfOrigin: status.behindOfOrigin ?? null,
2572
2565
  hasRemote: status.hasRemote,
2573
2566
  remoteUrl: status.remoteUrl,
2574
2567
  isPaseoOwnedWorktree: true,
@@ -2579,7 +2572,7 @@ export class Session {
2579
2572
  return;
2580
2573
  }
2581
2574
  this.emit({
2582
- type: "checkout_status_response",
2575
+ type: 'checkout_status_response',
2583
2576
  payload: {
2584
2577
  cwd,
2585
2578
  isGit: true,
@@ -2589,6 +2582,7 @@ export class Session {
2589
2582
  baseRef: status.baseRef ?? null,
2590
2583
  aheadBehind: status.aheadBehind ?? null,
2591
2584
  aheadOfOrigin: status.aheadOfOrigin ?? null,
2585
+ behindOfOrigin: status.behindOfOrigin ?? null,
2592
2586
  hasRemote: status.hasRemote,
2593
2587
  remoteUrl: status.remoteUrl,
2594
2588
  isPaseoOwnedWorktree: false,
@@ -2599,7 +2593,7 @@ export class Session {
2599
2593
  }
2600
2594
  catch (error) {
2601
2595
  this.emit({
2602
- type: "checkout_status_response",
2596
+ type: 'checkout_status_response',
2603
2597
  payload: {
2604
2598
  cwd,
2605
2599
  isGit: false,
@@ -2609,6 +2603,7 @@ export class Session {
2609
2603
  baseRef: null,
2610
2604
  aheadBehind: null,
2611
2605
  aheadOfOrigin: null,
2606
+ behindOfOrigin: null,
2612
2607
  hasRemote: false,
2613
2608
  remoteUrl: null,
2614
2609
  isPaseoOwnedWorktree: false,
@@ -2629,7 +2624,7 @@ export class Session {
2629
2624
  env: READ_ONLY_GIT_ENV,
2630
2625
  });
2631
2626
  this.emit({
2632
- type: "validate_branch_response",
2627
+ type: 'validate_branch_response',
2633
2628
  payload: {
2634
2629
  exists: true,
2635
2630
  resolvedRef: branchName,
@@ -2650,7 +2645,7 @@ export class Session {
2650
2645
  env: READ_ONLY_GIT_ENV,
2651
2646
  });
2652
2647
  this.emit({
2653
- type: "validate_branch_response",
2648
+ type: 'validate_branch_response',
2654
2649
  payload: {
2655
2650
  exists: true,
2656
2651
  resolvedRef: `origin/${branchName}`,
@@ -2666,7 +2661,7 @@ export class Session {
2666
2661
  }
2667
2662
  // Branch not found anywhere
2668
2663
  this.emit({
2669
- type: "validate_branch_response",
2664
+ type: 'validate_branch_response',
2670
2665
  payload: {
2671
2666
  exists: false,
2672
2667
  resolvedRef: null,
@@ -2678,7 +2673,7 @@ export class Session {
2678
2673
  }
2679
2674
  catch (error) {
2680
2675
  this.emit({
2681
- type: "validate_branch_response",
2676
+ type: 'validate_branch_response',
2682
2677
  payload: {
2683
2678
  exists: false,
2684
2679
  resolvedRef: null,
@@ -2695,7 +2690,7 @@ export class Session {
2695
2690
  const resolvedCwd = expandTilde(cwd);
2696
2691
  const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
2697
2692
  this.emit({
2698
- type: "branch_suggestions_response",
2693
+ type: 'branch_suggestions_response',
2699
2694
  payload: {
2700
2695
  branches,
2701
2696
  error: null,
@@ -2705,7 +2700,7 @@ export class Session {
2705
2700
  }
2706
2701
  catch (error) {
2707
2702
  this.emit({
2708
- type: "branch_suggestions_response",
2703
+ type: 'branch_suggestions_response',
2709
2704
  payload: {
2710
2705
  branches: [],
2711
2706
  error: error instanceof Error ? error.message : String(error),
@@ -2715,17 +2710,30 @@ export class Session {
2715
2710
  }
2716
2711
  }
2717
2712
  async handleDirectorySuggestionsRequest(msg) {
2718
- const { query, limit, requestId } = msg;
2713
+ const { query, limit, requestId, cwd, includeFiles, includeDirectories } = msg;
2719
2714
  try {
2720
- const directories = await searchHomeDirectories({
2721
- homeDir: process.env.HOME ?? homedir(),
2722
- query,
2723
- limit,
2724
- });
2715
+ const workspaceCwd = cwd?.trim();
2716
+ const entries = workspaceCwd
2717
+ ? await searchWorkspaceEntries({
2718
+ cwd: expandTilde(workspaceCwd),
2719
+ query,
2720
+ limit,
2721
+ includeFiles,
2722
+ includeDirectories,
2723
+ })
2724
+ : (await searchHomeDirectories({
2725
+ homeDir: process.env.HOME ?? homedir(),
2726
+ query,
2727
+ limit,
2728
+ })).map((path) => ({ path, kind: 'directory' }));
2729
+ const directories = entries
2730
+ .filter((entry) => entry.kind === 'directory')
2731
+ .map((entry) => entry.path);
2725
2732
  this.emit({
2726
- type: "directory_suggestions_response",
2733
+ type: 'directory_suggestions_response',
2727
2734
  payload: {
2728
2735
  directories,
2736
+ entries,
2729
2737
  error: null,
2730
2738
  requestId,
2731
2739
  },
@@ -2733,9 +2741,10 @@ export class Session {
2733
2741
  }
2734
2742
  catch (error) {
2735
2743
  this.emit({
2736
- type: "directory_suggestions_response",
2744
+ type: 'directory_suggestions_response',
2737
2745
  payload: {
2738
2746
  directories: [],
2747
+ entries: [],
2739
2748
  error: error instanceof Error ? error.message : String(error),
2740
2749
  requestId,
2741
2750
  },
@@ -2743,19 +2752,17 @@ export class Session {
2743
2752
  }
2744
2753
  }
2745
2754
  normalizeCheckoutDiffCompare(compare) {
2746
- if (compare.mode === "uncommitted") {
2747
- return { mode: "uncommitted" };
2755
+ if (compare.mode === 'uncommitted') {
2756
+ return { mode: 'uncommitted' };
2748
2757
  }
2749
2758
  const trimmedBaseRef = compare.baseRef?.trim();
2750
- return trimmedBaseRef
2751
- ? { mode: "base", baseRef: trimmedBaseRef }
2752
- : { mode: "base" };
2759
+ return trimmedBaseRef ? { mode: 'base', baseRef: trimmedBaseRef } : { mode: 'base' };
2753
2760
  }
2754
2761
  buildCheckoutDiffTargetKey(cwd, compare) {
2755
2762
  return JSON.stringify([
2756
2763
  cwd,
2757
2764
  compare.mode,
2758
- compare.mode === "base" ? (compare.baseRef ?? "") : "",
2765
+ compare.mode === 'base' ? (compare.baseRef ?? '') : '',
2759
2766
  ]);
2760
2767
  }
2761
2768
  closeCheckoutDiffWatchTarget(target) {
@@ -2790,7 +2797,7 @@ export class Session {
2790
2797
  }
2791
2798
  async resolveCheckoutGitDir(cwd) {
2792
2799
  try {
2793
- const { stdout } = await execAsync("git rev-parse --absolute-git-dir", {
2800
+ const { stdout } = await execAsync('git rev-parse --absolute-git-dir', {
2794
2801
  cwd,
2795
2802
  env: READ_ONLY_GIT_ENV,
2796
2803
  });
@@ -2803,7 +2810,7 @@ export class Session {
2803
2810
  }
2804
2811
  async resolveCheckoutWatchRoot(cwd) {
2805
2812
  try {
2806
- const { stdout } = await execAsync("git rev-parse --path-format=absolute --show-toplevel", {
2813
+ const { stdout } = await execAsync('git rev-parse --path-format=absolute --show-toplevel', {
2807
2814
  cwd,
2808
2815
  env: READ_ONLY_GIT_ENV,
2809
2816
  });
@@ -2829,7 +2836,7 @@ export class Session {
2829
2836
  }
2830
2837
  for (const subscriptionId of target.subscriptions) {
2831
2838
  this.emit({
2832
- type: "checkout_diff_update",
2839
+ type: 'checkout_diff_update',
2833
2840
  payload: {
2834
2841
  subscriptionId,
2835
2842
  ...snapshot,
@@ -2876,7 +2883,9 @@ export class Session {
2876
2883
  target.refreshPromise = (async () => {
2877
2884
  do {
2878
2885
  target.refreshQueued = false;
2879
- const snapshot = await this.computeCheckoutDiffSnapshot(target.cwd, target.compare, { diffCwd: target.diffCwd });
2886
+ const snapshot = await this.computeCheckoutDiffSnapshot(target.cwd, target.compare, {
2887
+ diffCwd: target.diffCwd,
2888
+ });
2880
2889
  target.latestPayload = snapshot;
2881
2890
  const fingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
2882
2891
  if (fingerprint !== target.latestFingerprint) {
@@ -2920,7 +2929,7 @@ export class Session {
2920
2929
  watchPaths.add(gitDir);
2921
2930
  }
2922
2931
  let hasRecursiveRepoCoverage = false;
2923
- const allowRecursiveRepoWatch = process.platform !== "linux";
2932
+ const allowRecursiveRepoWatch = process.platform !== 'linux';
2924
2933
  for (const watchPath of watchPaths) {
2925
2934
  const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
2926
2935
  const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
@@ -2941,21 +2950,21 @@ export class Session {
2941
2950
  if (shouldTryRecursive) {
2942
2951
  try {
2943
2952
  watcher = createWatcher(false);
2944
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff recursive watch unavailable; using non-recursive fallback");
2953
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff recursive watch unavailable; using non-recursive fallback');
2945
2954
  }
2946
2955
  catch (fallbackError) {
2947
- this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
2956
+ this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
2948
2957
  }
2949
2958
  }
2950
2959
  else {
2951
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
2960
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
2952
2961
  }
2953
2962
  }
2954
2963
  if (!watcher) {
2955
2964
  continue;
2956
2965
  }
2957
- watcher.on("error", (error) => {
2958
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff watcher error");
2966
+ watcher.on('error', (error) => {
2967
+ this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff watcher error');
2959
2968
  });
2960
2969
  target.watchers.push(watcher);
2961
2970
  if (watchPath === repoWatchPath && watcherIsRecursive) {
@@ -2971,10 +2980,8 @@ export class Session {
2971
2980
  cwd,
2972
2981
  compare,
2973
2982
  intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
2974
- reason: target.watchers.length === 0
2975
- ? "no_watchers"
2976
- : "missing_recursive_repo_root_coverage",
2977
- }, "Checkout diff watchers unavailable; using timed refresh fallback");
2983
+ reason: target.watchers.length === 0 ? 'no_watchers' : 'missing_recursive_repo_root_coverage',
2984
+ }, 'Checkout diff watchers unavailable; using timed refresh fallback');
2978
2985
  }
2979
2986
  this.checkoutDiffTargets.set(targetKey, target);
2980
2987
  return target;
@@ -2995,7 +3002,7 @@ export class Session {
2995
3002
  target.latestPayload = snapshot;
2996
3003
  target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
2997
3004
  this.emit({
2998
- type: "subscribe_checkout_diff_response",
3005
+ type: 'subscribe_checkout_diff_response',
2999
3006
  payload: {
3000
3007
  subscriptionId: msg.subscriptionId,
3001
3008
  ...snapshot,
@@ -3018,12 +3025,12 @@ export class Session {
3018
3025
  async handleCheckoutCommitRequest(msg) {
3019
3026
  const { cwd, requestId } = msg;
3020
3027
  try {
3021
- let message = msg.message?.trim() ?? "";
3028
+ let message = msg.message?.trim() ?? '';
3022
3029
  if (!message) {
3023
3030
  message = await this.generateCommitMessage(cwd);
3024
3031
  }
3025
3032
  if (!message) {
3026
- throw new Error("Commit message is required");
3033
+ throw new Error('Commit message is required');
3027
3034
  }
3028
3035
  await commitChanges(cwd, {
3029
3036
  message,
@@ -3031,7 +3038,7 @@ export class Session {
3031
3038
  });
3032
3039
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3033
3040
  this.emit({
3034
- type: "checkout_commit_response",
3041
+ type: 'checkout_commit_response',
3035
3042
  payload: {
3036
3043
  cwd,
3037
3044
  success: true,
@@ -3042,7 +3049,7 @@ export class Session {
3042
3049
  }
3043
3050
  catch (error) {
3044
3051
  this.emit({
3045
- type: "checkout_commit_response",
3052
+ type: 'checkout_commit_response',
3046
3053
  payload: {
3047
3054
  cwd,
3048
3055
  success: false,
@@ -3058,13 +3065,13 @@ export class Session {
3058
3065
  const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
3059
3066
  if (!status.isGit) {
3060
3067
  try {
3061
- await execAsync("git rev-parse --is-inside-work-tree", {
3068
+ await execAsync('git rev-parse --is-inside-work-tree', {
3062
3069
  cwd,
3063
3070
  env: READ_ONLY_GIT_ENV,
3064
3071
  });
3065
3072
  }
3066
3073
  catch (error) {
3067
- const details = typeof error?.stderr === "string"
3074
+ const details = typeof error?.stderr === 'string'
3068
3075
  ? String(error.stderr).trim()
3069
3076
  : error instanceof Error
3070
3077
  ? error.message
@@ -3073,28 +3080,28 @@ export class Session {
3073
3080
  }
3074
3081
  }
3075
3082
  if (msg.requireCleanTarget) {
3076
- const { stdout } = await execAsync("git status --porcelain", {
3083
+ const { stdout } = await execAsync('git status --porcelain', {
3077
3084
  cwd,
3078
3085
  env: READ_ONLY_GIT_ENV,
3079
3086
  });
3080
3087
  if (stdout.trim().length > 0) {
3081
- throw new Error("Working directory has uncommitted changes.");
3088
+ throw new Error('Working directory has uncommitted changes.');
3082
3089
  }
3083
3090
  }
3084
3091
  let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
3085
3092
  if (!baseRef) {
3086
- throw new Error("Base branch is required for merge");
3093
+ throw new Error('Base branch is required for merge');
3087
3094
  }
3088
- if (baseRef.startsWith("origin/")) {
3089
- baseRef = baseRef.slice("origin/".length);
3095
+ if (baseRef.startsWith('origin/')) {
3096
+ baseRef = baseRef.slice('origin/'.length);
3090
3097
  }
3091
3098
  await mergeToBase(cwd, {
3092
3099
  baseRef,
3093
- mode: msg.strategy === "squash" ? "squash" : "merge",
3100
+ mode: msg.strategy === 'squash' ? 'squash' : 'merge',
3094
3101
  }, { paseoHome: this.paseoHome });
3095
3102
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3096
3103
  this.emit({
3097
- type: "checkout_merge_response",
3104
+ type: 'checkout_merge_response',
3098
3105
  payload: {
3099
3106
  cwd,
3100
3107
  success: true,
@@ -3105,7 +3112,7 @@ export class Session {
3105
3112
  }
3106
3113
  catch (error) {
3107
3114
  this.emit({
3108
- type: "checkout_merge_response",
3115
+ type: 'checkout_merge_response',
3109
3116
  payload: {
3110
3117
  cwd,
3111
3118
  success: false,
@@ -3119,12 +3126,12 @@ export class Session {
3119
3126
  const { cwd, requestId } = msg;
3120
3127
  try {
3121
3128
  if (msg.requireCleanTarget ?? true) {
3122
- const { stdout } = await execAsync("git status --porcelain", {
3129
+ const { stdout } = await execAsync('git status --porcelain', {
3123
3130
  cwd,
3124
3131
  env: READ_ONLY_GIT_ENV,
3125
3132
  });
3126
3133
  if (stdout.trim().length > 0) {
3127
- throw new Error("Working directory has uncommitted changes.");
3134
+ throw new Error('Working directory has uncommitted changes.');
3128
3135
  }
3129
3136
  }
3130
3137
  await mergeFromBase(cwd, {
@@ -3133,7 +3140,7 @@ export class Session {
3133
3140
  });
3134
3141
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3135
3142
  this.emit({
3136
- type: "checkout_merge_from_base_response",
3143
+ type: 'checkout_merge_from_base_response',
3137
3144
  payload: {
3138
3145
  cwd,
3139
3146
  success: true,
@@ -3144,7 +3151,7 @@ export class Session {
3144
3151
  }
3145
3152
  catch (error) {
3146
3153
  this.emit({
3147
- type: "checkout_merge_from_base_response",
3154
+ type: 'checkout_merge_from_base_response',
3148
3155
  payload: {
3149
3156
  cwd,
3150
3157
  success: false,
@@ -3159,7 +3166,7 @@ export class Session {
3159
3166
  try {
3160
3167
  await pushCurrentBranch(cwd);
3161
3168
  this.emit({
3162
- type: "checkout_push_response",
3169
+ type: 'checkout_push_response',
3163
3170
  payload: {
3164
3171
  cwd,
3165
3172
  success: true,
@@ -3170,7 +3177,7 @@ export class Session {
3170
3177
  }
3171
3178
  catch (error) {
3172
3179
  this.emit({
3173
- type: "checkout_push_response",
3180
+ type: 'checkout_push_response',
3174
3181
  payload: {
3175
3182
  cwd,
3176
3183
  success: false,
@@ -3183,8 +3190,8 @@ export class Session {
3183
3190
  async handleCheckoutPrCreateRequest(msg) {
3184
3191
  const { cwd, requestId } = msg;
3185
3192
  try {
3186
- let title = msg.title?.trim() ?? "";
3187
- let body = msg.body?.trim() ?? "";
3193
+ let title = msg.title?.trim() ?? '';
3194
+ let body = msg.body?.trim() ?? '';
3188
3195
  if (!title || !body) {
3189
3196
  const generated = await this.generatePullRequestText(cwd, msg.baseRef);
3190
3197
  if (!title)
@@ -3198,7 +3205,7 @@ export class Session {
3198
3205
  base: msg.baseRef,
3199
3206
  });
3200
3207
  this.emit({
3201
- type: "checkout_pr_create_response",
3208
+ type: 'checkout_pr_create_response',
3202
3209
  payload: {
3203
3210
  cwd,
3204
3211
  url: result.url ?? null,
@@ -3210,7 +3217,7 @@ export class Session {
3210
3217
  }
3211
3218
  catch (error) {
3212
3219
  this.emit({
3213
- type: "checkout_pr_create_response",
3220
+ type: 'checkout_pr_create_response',
3214
3221
  payload: {
3215
3222
  cwd,
3216
3223
  url: null,
@@ -3226,7 +3233,7 @@ export class Session {
3226
3233
  try {
3227
3234
  const prStatus = await getPullRequestStatus(cwd);
3228
3235
  this.emit({
3229
- type: "checkout_pr_status_response",
3236
+ type: 'checkout_pr_status_response',
3230
3237
  payload: {
3231
3238
  cwd,
3232
3239
  status: prStatus.status,
@@ -3238,7 +3245,7 @@ export class Session {
3238
3245
  }
3239
3246
  catch (error) {
3240
3247
  this.emit({
3241
- type: "checkout_pr_status_response",
3248
+ type: 'checkout_pr_status_response',
3242
3249
  payload: {
3243
3250
  cwd,
3244
3251
  status: null,
@@ -3254,10 +3261,10 @@ export class Session {
3254
3261
  const cwd = msg.repoRoot ?? msg.cwd;
3255
3262
  if (!cwd) {
3256
3263
  this.emit({
3257
- type: "paseo_worktree_list_response",
3264
+ type: 'paseo_worktree_list_response',
3258
3265
  payload: {
3259
3266
  worktrees: [],
3260
- error: { code: "UNKNOWN", message: "cwd or repoRoot is required" },
3267
+ error: { code: 'UNKNOWN', message: 'cwd or repoRoot is required' },
3261
3268
  requestId,
3262
3269
  },
3263
3270
  });
@@ -3266,7 +3273,7 @@ export class Session {
3266
3273
  try {
3267
3274
  const worktrees = await listPaseoWorktrees({ cwd, paseoHome: this.paseoHome });
3268
3275
  this.emit({
3269
- type: "paseo_worktree_list_response",
3276
+ type: 'paseo_worktree_list_response',
3270
3277
  payload: {
3271
3278
  worktrees: worktrees.map((entry) => ({
3272
3279
  worktreePath: entry.path,
@@ -3280,7 +3287,7 @@ export class Session {
3280
3287
  }
3281
3288
  catch (error) {
3282
3289
  this.emit({
3283
- type: "paseo_worktree_list_response",
3290
+ type: 'paseo_worktree_list_response',
3284
3291
  payload: {
3285
3292
  worktrees: [],
3286
3293
  error: this.toCheckoutError(error),
@@ -3289,6 +3296,115 @@ export class Session {
3289
3296
  });
3290
3297
  }
3291
3298
  }
3299
+ async maybeArchiveWorktreeAfterLastAgentArchived(options) {
3300
+ try {
3301
+ const ownership = await isPaseoOwnedWorktreeCwd(options.archivedAgentCwd, {
3302
+ paseoHome: this.paseoHome,
3303
+ });
3304
+ if (!ownership.allowed) {
3305
+ return;
3306
+ }
3307
+ const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(options.archivedAgentCwd, {
3308
+ paseoHome: this.paseoHome,
3309
+ });
3310
+ if (!resolvedWorktree) {
3311
+ return;
3312
+ }
3313
+ const records = await this.agentStorage.list();
3314
+ const recordsById = new Map(records.map((record) => [record.id, record]));
3315
+ const targetPath = resolvedWorktree.worktreePath;
3316
+ const hasRemainingNonArchivedRecord = records.some((record) => {
3317
+ if (record.id === options.archivedAgentId || record.archivedAt) {
3318
+ return false;
3319
+ }
3320
+ return this.isPathWithinRoot(targetPath, record.cwd);
3321
+ });
3322
+ if (hasRemainingNonArchivedRecord) {
3323
+ return;
3324
+ }
3325
+ const hasUnknownLiveAgent = this.agentManager.listAgents().some((agent) => {
3326
+ if (agent.id === options.archivedAgentId) {
3327
+ return false;
3328
+ }
3329
+ if (!this.isPathWithinRoot(targetPath, agent.cwd)) {
3330
+ return false;
3331
+ }
3332
+ return !recordsById.has(agent.id);
3333
+ });
3334
+ if (hasUnknownLiveAgent) {
3335
+ return;
3336
+ }
3337
+ const repoRoot = ownership.repoRoot;
3338
+ if (!repoRoot) {
3339
+ this.sessionLogger.warn({ agentId: options.archivedAgentId, worktreePath: targetPath }, 'Unable to resolve repo root for auto-archive after agent archive');
3340
+ return;
3341
+ }
3342
+ await this.archivePaseoWorktree({
3343
+ targetPath,
3344
+ repoRoot,
3345
+ requestId: options.requestId,
3346
+ });
3347
+ }
3348
+ catch (error) {
3349
+ this.sessionLogger.warn({ err: error, agentId: options.archivedAgentId, cwd: options.archivedAgentCwd }, 'Failed to auto-archive worktree after agent archive');
3350
+ }
3351
+ }
3352
+ async archivePaseoWorktree(options) {
3353
+ let targetPath = options.targetPath;
3354
+ const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, {
3355
+ paseoHome: this.paseoHome,
3356
+ });
3357
+ if (resolvedWorktree) {
3358
+ targetPath = resolvedWorktree.worktreePath;
3359
+ }
3360
+ const removedAgents = new Set();
3361
+ const agents = this.agentManager.listAgents();
3362
+ for (const agent of agents) {
3363
+ if (this.isPathWithinRoot(targetPath, agent.cwd)) {
3364
+ removedAgents.add(agent.id);
3365
+ try {
3366
+ await this.agentManager.closeAgent(agent.id);
3367
+ }
3368
+ catch {
3369
+ // ignore cleanup errors
3370
+ }
3371
+ try {
3372
+ await this.agentStorage.remove(agent.id);
3373
+ }
3374
+ catch {
3375
+ // ignore cleanup errors
3376
+ }
3377
+ }
3378
+ }
3379
+ const registryRecords = await this.agentStorage.list();
3380
+ for (const record of registryRecords) {
3381
+ if (this.isPathWithinRoot(targetPath, record.cwd)) {
3382
+ removedAgents.add(record.id);
3383
+ try {
3384
+ await this.agentStorage.remove(record.id);
3385
+ }
3386
+ catch {
3387
+ // ignore cleanup errors
3388
+ }
3389
+ }
3390
+ }
3391
+ await this.killTerminalsUnderPath(targetPath);
3392
+ await deletePaseoWorktree({
3393
+ cwd: options.repoRoot,
3394
+ worktreePath: targetPath,
3395
+ paseoHome: this.paseoHome,
3396
+ });
3397
+ for (const agentId of removedAgents) {
3398
+ this.emit({
3399
+ type: 'agent_deleted',
3400
+ payload: {
3401
+ agentId,
3402
+ requestId: options.requestId,
3403
+ },
3404
+ });
3405
+ }
3406
+ return Array.from(removedAgents);
3407
+ }
3292
3408
  async handlePaseoWorktreeArchiveRequest(msg) {
3293
3409
  const { requestId } = msg;
3294
3410
  let targetPath = msg.worktreePath;
@@ -3296,7 +3412,7 @@ export class Session {
3296
3412
  try {
3297
3413
  if (!targetPath) {
3298
3414
  if (!repoRoot || !msg.branchName) {
3299
- throw new Error("worktreePath or repoRoot+branchName is required");
3415
+ throw new Error('worktreePath or repoRoot+branchName is required');
3300
3416
  }
3301
3417
  const worktrees = await listPaseoWorktrees({ cwd: repoRoot, paseoHome: this.paseoHome });
3302
3418
  const match = worktrees.find((entry) => entry.branchName === msg.branchName);
@@ -3305,16 +3421,18 @@ export class Session {
3305
3421
  }
3306
3422
  targetPath = match.path;
3307
3423
  }
3308
- const ownership = await isPaseoOwnedWorktreeCwd(targetPath, { paseoHome: this.paseoHome });
3424
+ const ownership = await isPaseoOwnedWorktreeCwd(targetPath, {
3425
+ paseoHome: this.paseoHome,
3426
+ });
3309
3427
  if (!ownership.allowed) {
3310
3428
  this.emit({
3311
- type: "paseo_worktree_archive_response",
3429
+ type: 'paseo_worktree_archive_response',
3312
3430
  payload: {
3313
3431
  success: false,
3314
3432
  removedAgents: [],
3315
3433
  error: {
3316
- code: "NOT_ALLOWED",
3317
- message: "Worktree is not a Paseo-owned worktree",
3434
+ code: 'NOT_ALLOWED',
3435
+ message: 'Worktree is not a Paseo-owned worktree',
3318
3436
  },
3319
3437
  requestId,
3320
3438
  },
@@ -3323,64 +3441,18 @@ export class Session {
3323
3441
  }
3324
3442
  repoRoot = ownership.repoRoot ?? repoRoot ?? null;
3325
3443
  if (!repoRoot) {
3326
- throw new Error("Unable to resolve repo root for worktree");
3327
- }
3328
- const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, {
3329
- paseoHome: this.paseoHome,
3330
- });
3331
- if (resolvedWorktree) {
3332
- targetPath = resolvedWorktree.worktreePath;
3333
- }
3334
- const removedAgents = new Set();
3335
- const agents = this.agentManager.listAgents();
3336
- for (const agent of agents) {
3337
- if (this.isPathWithinRoot(targetPath, agent.cwd)) {
3338
- removedAgents.add(agent.id);
3339
- try {
3340
- await this.agentManager.closeAgent(agent.id);
3341
- }
3342
- catch {
3343
- // ignore cleanup errors
3344
- }
3345
- try {
3346
- await this.agentStorage.remove(agent.id);
3347
- }
3348
- catch {
3349
- // ignore cleanup errors
3350
- }
3351
- }
3444
+ throw new Error('Unable to resolve repo root for worktree');
3352
3445
  }
3353
- const registryRecords = await this.agentStorage.list();
3354
- for (const record of registryRecords) {
3355
- if (this.isPathWithinRoot(targetPath, record.cwd)) {
3356
- removedAgents.add(record.id);
3357
- try {
3358
- await this.agentStorage.remove(record.id);
3359
- }
3360
- catch {
3361
- // ignore cleanup errors
3362
- }
3363
- }
3364
- }
3365
- await deletePaseoWorktree({
3366
- cwd: repoRoot,
3367
- worktreePath: targetPath,
3368
- paseoHome: this.paseoHome,
3446
+ const removedAgents = await this.archivePaseoWorktree({
3447
+ targetPath,
3448
+ repoRoot,
3449
+ requestId,
3369
3450
  });
3370
- for (const agentId of removedAgents) {
3371
- this.emit({
3372
- type: "agent_deleted",
3373
- payload: {
3374
- agentId,
3375
- requestId,
3376
- },
3377
- });
3378
- }
3379
3451
  this.emit({
3380
- type: "paseo_worktree_archive_response",
3452
+ type: 'paseo_worktree_archive_response',
3381
3453
  payload: {
3382
3454
  success: true,
3383
- removedAgents: Array.from(removedAgents),
3455
+ removedAgents,
3384
3456
  error: null,
3385
3457
  requestId,
3386
3458
  },
@@ -3388,7 +3460,7 @@ export class Session {
3388
3460
  }
3389
3461
  catch (error) {
3390
3462
  this.emit({
3391
- type: "paseo_worktree_archive_response",
3463
+ type: 'paseo_worktree_archive_response',
3392
3464
  payload: {
3393
3465
  success: false,
3394
3466
  removedAgents: [],
@@ -3402,14 +3474,14 @@ export class Session {
3402
3474
  * Handle read-only file explorer requests scoped to an agent's cwd
3403
3475
  */
3404
3476
  async handleFileExplorerRequest(request) {
3405
- const { agentId, path: requestedPath = ".", mode, requestId } = request;
3477
+ const { agentId, path: requestedPath = '.', mode, requestId } = request;
3406
3478
  this.sessionLogger.debug({ agentId, mode, path: requestedPath }, `Handling file explorer request for agent ${agentId} (${mode} ${requestedPath})`);
3407
3479
  try {
3408
3480
  const agents = this.agentManager.listAgents();
3409
3481
  const agent = agents.find((a) => a.id === agentId);
3410
3482
  if (!agent) {
3411
3483
  this.emit({
3412
- type: "file_explorer_response",
3484
+ type: 'file_explorer_response',
3413
3485
  payload: {
3414
3486
  agentId,
3415
3487
  path: requestedPath,
@@ -3422,13 +3494,13 @@ export class Session {
3422
3494
  });
3423
3495
  return;
3424
3496
  }
3425
- if (mode === "list") {
3497
+ if (mode === 'list') {
3426
3498
  const directory = await listDirectoryEntries({
3427
3499
  root: agent.cwd,
3428
3500
  relativePath: requestedPath,
3429
3501
  });
3430
3502
  this.emit({
3431
- type: "file_explorer_response",
3503
+ type: 'file_explorer_response',
3432
3504
  payload: {
3433
3505
  agentId,
3434
3506
  path: directory.path,
@@ -3446,7 +3518,7 @@ export class Session {
3446
3518
  relativePath: requestedPath,
3447
3519
  });
3448
3520
  this.emit({
3449
- type: "file_explorer_response",
3521
+ type: 'file_explorer_response',
3450
3522
  payload: {
3451
3523
  agentId,
3452
3524
  path: file.path,
@@ -3462,7 +3534,7 @@ export class Session {
3462
3534
  catch (error) {
3463
3535
  this.sessionLogger.error({ err: error, agentId, path: requestedPath }, `Failed to fulfill file explorer request for agent ${agentId}`);
3464
3536
  this.emit({
3465
- type: "file_explorer_response",
3537
+ type: 'file_explorer_response',
3466
3538
  payload: {
3467
3539
  agentId,
3468
3540
  path: requestedPath,
@@ -3483,7 +3555,7 @@ export class Session {
3483
3555
  try {
3484
3556
  const icon = await getProjectIcon(cwd);
3485
3557
  this.emit({
3486
- type: "project_icon_response",
3558
+ type: 'project_icon_response',
3487
3559
  payload: {
3488
3560
  cwd,
3489
3561
  icon,
@@ -3494,7 +3566,7 @@ export class Session {
3494
3566
  }
3495
3567
  catch (error) {
3496
3568
  this.emit({
3497
- type: "project_icon_response",
3569
+ type: 'project_icon_response',
3498
3570
  payload: {
3499
3571
  cwd,
3500
3572
  icon: null,
@@ -3515,7 +3587,7 @@ export class Session {
3515
3587
  const agent = agents.find((a) => a.id === agentId);
3516
3588
  if (!agent) {
3517
3589
  this.emit({
3518
- type: "file_download_token_response",
3590
+ type: 'file_download_token_response',
3519
3591
  payload: {
3520
3592
  agentId,
3521
3593
  path: requestedPath,
@@ -3542,7 +3614,7 @@ export class Session {
3542
3614
  size: info.size,
3543
3615
  });
3544
3616
  this.emit({
3545
- type: "file_download_token_response",
3617
+ type: 'file_download_token_response',
3546
3618
  payload: {
3547
3619
  agentId,
3548
3620
  path: info.path,
@@ -3558,7 +3630,7 @@ export class Session {
3558
3630
  catch (error) {
3559
3631
  this.sessionLogger.error({ err: error, agentId, path: requestedPath }, `Failed to issue download token for agent ${agentId}`);
3560
3632
  this.emit({
3561
- type: "file_download_token_response",
3633
+ type: 'file_download_token_response',
3562
3634
  payload: {
3563
3635
  agentId,
3564
3636
  path: requestedPath,
@@ -3597,7 +3669,7 @@ export class Session {
3597
3669
  async resolveAgentIdentifier(identifier) {
3598
3670
  const trimmed = identifier.trim();
3599
3671
  if (!trimmed) {
3600
- return { ok: false, error: "Agent identifier cannot be empty" };
3672
+ return { ok: false, error: 'Agent identifier cannot be empty' };
3601
3673
  }
3602
3674
  const stored = await this.agentStorage.list();
3603
3675
  const storedRecords = stored.filter((record) => !record.internal);
@@ -3621,7 +3693,7 @@ export class Session {
3621
3693
  error: `Agent identifier "${trimmed}" is ambiguous (${prefixMatches
3622
3694
  .slice(0, 5)
3623
3695
  .map((id) => id.slice(0, 8))
3624
- .join(", ")}${prefixMatches.length > 5 ? ", …" : ""})`,
3696
+ .join(', ')}${prefixMatches.length > 5 ? ', …' : ''})`,
3625
3697
  };
3626
3698
  }
3627
3699
  const titleMatches = storedRecords.filter((record) => record.title === trimmed);
@@ -3634,7 +3706,7 @@ export class Session {
3634
3706
  error: `Agent title "${trimmed}" is ambiguous (${titleMatches
3635
3707
  .slice(0, 5)
3636
3708
  .map((r) => r.id.slice(0, 8))
3637
- .join(", ")}${titleMatches.length > 5 ? ", …" : ""})`,
3709
+ .join(', ')}${titleMatches.length > 5 ? ', …' : ''})`,
3638
3710
  };
3639
3711
  }
3640
3712
  return { ok: false, error: `Agent not found: ${trimmed}` };
@@ -3651,9 +3723,7 @@ export class Session {
3651
3723
  return this.buildStoredAgentPayload(record);
3652
3724
  }
3653
3725
  normalizeFetchAgentsSort(sort) {
3654
- const fallback = [
3655
- { key: "updated_at", direction: "desc" },
3656
- ];
3726
+ const fallback = [{ key: 'updated_at', direction: 'desc' }];
3657
3727
  if (!sort || sort.length === 0) {
3658
3728
  return fallback;
3659
3729
  }
@@ -3671,30 +3741,30 @@ export class Session {
3671
3741
  getStatusPriority(agent) {
3672
3742
  const requiresAttention = agent.requiresAttention ?? false;
3673
3743
  const attentionReason = agent.attentionReason ?? null;
3674
- if (requiresAttention && attentionReason === "permission") {
3744
+ if (requiresAttention && attentionReason === 'permission') {
3675
3745
  return 0;
3676
3746
  }
3677
- if (agent.status === "error" || attentionReason === "error") {
3747
+ if (agent.status === 'error' || attentionReason === 'error') {
3678
3748
  return 1;
3679
3749
  }
3680
- if (agent.status === "running") {
3750
+ if (agent.status === 'running') {
3681
3751
  return 2;
3682
3752
  }
3683
- if (agent.status === "initializing") {
3753
+ if (agent.status === 'initializing') {
3684
3754
  return 3;
3685
3755
  }
3686
3756
  return 4;
3687
3757
  }
3688
3758
  getFetchAgentsSortValue(entry, key) {
3689
3759
  switch (key) {
3690
- case "status_priority":
3760
+ case 'status_priority':
3691
3761
  return this.getStatusPriority(entry.agent);
3692
- case "created_at":
3762
+ case 'created_at':
3693
3763
  return Date.parse(entry.agent.createdAt);
3694
- case "updated_at":
3764
+ case 'updated_at':
3695
3765
  return Date.parse(entry.agent.updatedAt);
3696
- case "title":
3697
- return entry.agent.title?.toLocaleLowerCase() ?? "";
3766
+ case 'title':
3767
+ return entry.agent.title?.toLocaleLowerCase() ?? '';
3698
3768
  }
3699
3769
  }
3700
3770
  compareSortValues(left, right) {
@@ -3707,7 +3777,7 @@ export class Session {
3707
3777
  if (right === null) {
3708
3778
  return 1;
3709
3779
  }
3710
- if (typeof left === "number" && typeof right === "number") {
3780
+ if (typeof left === 'number' && typeof right === 'number') {
3711
3781
  return left < right ? -1 : 1;
3712
3782
  }
3713
3783
  return String(left).localeCompare(String(right));
@@ -3720,7 +3790,7 @@ export class Session {
3720
3790
  if (base === 0) {
3721
3791
  continue;
3722
3792
  }
3723
- return spec.direction === "asc" ? base : -base;
3793
+ return spec.direction === 'asc' ? base : -base;
3724
3794
  }
3725
3795
  return left.agent.id.localeCompare(right.agent.id);
3726
3796
  }
@@ -3733,49 +3803,48 @@ export class Session {
3733
3803
  sort,
3734
3804
  values,
3735
3805
  id: entry.agent.id,
3736
- }), "utf8").toString("base64url");
3806
+ }), 'utf8').toString('base64url');
3737
3807
  }
3738
3808
  decodeFetchAgentsCursor(cursor, sort) {
3739
3809
  let parsed;
3740
3810
  try {
3741
- parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
3811
+ parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
3742
3812
  }
3743
3813
  catch {
3744
- throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3814
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3745
3815
  }
3746
- if (!parsed || typeof parsed !== "object") {
3747
- throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3816
+ if (!parsed || typeof parsed !== 'object') {
3817
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3748
3818
  }
3749
3819
  const payload = parsed;
3750
- if (!Array.isArray(payload.sort) || typeof payload.id !== "string") {
3751
- throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3820
+ if (!Array.isArray(payload.sort) || typeof payload.id !== 'string') {
3821
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3752
3822
  }
3753
- if (!payload.values || typeof payload.values !== "object") {
3754
- throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3823
+ if (!payload.values || typeof payload.values !== 'object') {
3824
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3755
3825
  }
3756
3826
  const cursorSort = [];
3757
3827
  for (const item of payload.sort) {
3758
3828
  if (!item ||
3759
- typeof item !== "object" ||
3760
- typeof item.key !== "string" ||
3761
- typeof item.direction !== "string") {
3762
- throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3829
+ typeof item !== 'object' ||
3830
+ typeof item.key !== 'string' ||
3831
+ typeof item.direction !== 'string') {
3832
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3763
3833
  }
3764
3834
  const key = item.key;
3765
3835
  const direction = item.direction;
3766
- if ((key !== "status_priority" &&
3767
- key !== "created_at" &&
3768
- key !== "updated_at" &&
3769
- key !== "title") ||
3770
- (direction !== "asc" && direction !== "desc")) {
3771
- throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3836
+ if ((key !== 'status_priority' &&
3837
+ key !== 'created_at' &&
3838
+ key !== 'updated_at' &&
3839
+ key !== 'title') ||
3840
+ (direction !== 'asc' && direction !== 'desc')) {
3841
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3772
3842
  }
3773
3843
  cursorSort.push({ key, direction });
3774
3844
  }
3775
3845
  if (cursorSort.length !== sort.length ||
3776
- cursorSort.some((entry, index) => entry.key !== sort[index]?.key ||
3777
- entry.direction !== sort[index]?.direction)) {
3778
- throw new SessionRequestError("invalid_cursor", "fetch_agents cursor does not match current sort");
3846
+ cursorSort.some((entry, index) => entry.key !== sort[index]?.key || entry.direction !== sort[index]?.direction)) {
3847
+ throw new SessionRequestError('invalid_cursor', 'fetch_agents cursor does not match current sort');
3779
3848
  }
3780
3849
  return {
3781
3850
  sort: cursorSort,
@@ -3786,32 +3855,21 @@ export class Session {
3786
3855
  compareEntryWithCursor(entry, cursor, sort) {
3787
3856
  for (const spec of sort) {
3788
3857
  const leftValue = this.getFetchAgentsSortValue(entry, spec.key);
3789
- const rightValue = cursor.values[spec.key] !== undefined ? cursor.values[spec.key] ?? null : null;
3858
+ const rightValue = cursor.values[spec.key] !== undefined ? (cursor.values[spec.key] ?? null) : null;
3790
3859
  const base = this.compareSortValues(leftValue, rightValue);
3791
3860
  if (base === 0) {
3792
3861
  continue;
3793
3862
  }
3794
- return spec.direction === "asc" ? base : -base;
3863
+ return spec.direction === 'asc' ? base : -base;
3795
3864
  }
3796
3865
  return entry.agent.id.localeCompare(cursor.id);
3797
3866
  }
3798
3867
  async listFetchAgentsEntries(request) {
3799
3868
  const filter = request.filter;
3800
3869
  const sort = this.normalizeFetchAgentsSort(request.sort);
3801
- const includeArchived = filter?.includeArchived ?? false;
3802
- let agents = await this.listAgentPayloads({
3870
+ const agents = await this.listAgentPayloads({
3803
3871
  labels: filter?.labels,
3804
3872
  });
3805
- if (!includeArchived) {
3806
- agents = agents.filter((agent) => !agent.archivedAt);
3807
- }
3808
- if (filter?.statuses && filter.statuses.length > 0) {
3809
- const statuses = new Set(filter.statuses);
3810
- agents = agents.filter((agent) => statuses.has(agent.status));
3811
- }
3812
- if (typeof filter?.requiresAttention === "boolean") {
3813
- agents = agents.filter((agent) => (agent.requiresAttention ?? false) === filter.requiresAttention);
3814
- }
3815
3873
  const placementByCwd = new Map();
3816
3874
  const getPlacement = (cwd) => {
3817
3875
  const existing = placementByCwd.get(cwd);
@@ -3826,10 +3884,11 @@ export class Session {
3826
3884
  agent,
3827
3885
  project: await getPlacement(agent.cwd),
3828
3886
  })));
3829
- if (filter?.projectKeys && filter.projectKeys.length > 0) {
3830
- const projectKeys = new Set(filter.projectKeys.filter((item) => item.trim().length > 0));
3831
- entries = entries.filter((entry) => projectKeys.has(entry.project.projectKey));
3832
- }
3887
+ entries = entries.filter((entry) => this.matchesAgentFilter({
3888
+ agent: entry.agent,
3889
+ project: entry.project,
3890
+ filter,
3891
+ }));
3833
3892
  entries.sort((left, right) => this.compareFetchAgentsEntries(left, right, sort));
3834
3893
  const cursorToken = request.page?.cursor;
3835
3894
  if (cursorToken) {
@@ -3852,22 +3911,50 @@ export class Session {
3852
3911
  };
3853
3912
  }
3854
3913
  async handleFetchAgents(request) {
3914
+ const requestedSubscriptionId = request.subscribe?.subscriptionId?.trim();
3915
+ const subscriptionId = request.subscribe
3916
+ ? requestedSubscriptionId && requestedSubscriptionId.length > 0
3917
+ ? requestedSubscriptionId
3918
+ : uuidv4()
3919
+ : null;
3855
3920
  try {
3921
+ if (subscriptionId) {
3922
+ this.agentUpdatesSubscription = {
3923
+ subscriptionId,
3924
+ filter: request.filter,
3925
+ isBootstrapping: true,
3926
+ pendingUpdatesByAgentId: new Map(),
3927
+ };
3928
+ }
3856
3929
  const payload = await this.listFetchAgentsEntries(request);
3930
+ const snapshotUpdatedAtByAgentId = new Map();
3931
+ for (const entry of payload.entries) {
3932
+ const parsedUpdatedAt = Date.parse(entry.agent.updatedAt);
3933
+ if (!Number.isNaN(parsedUpdatedAt)) {
3934
+ snapshotUpdatedAtByAgentId.set(entry.agent.id, parsedUpdatedAt);
3935
+ }
3936
+ }
3857
3937
  this.emit({
3858
- type: "fetch_agents_response",
3938
+ type: 'fetch_agents_response',
3859
3939
  payload: {
3860
3940
  requestId: request.requestId,
3941
+ ...(subscriptionId ? { subscriptionId } : {}),
3861
3942
  ...payload,
3862
3943
  },
3863
3944
  });
3945
+ if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
3946
+ this.flushBootstrappedAgentUpdates({ snapshotUpdatedAtByAgentId });
3947
+ }
3864
3948
  }
3865
3949
  catch (error) {
3866
- const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
3867
- const message = error instanceof Error ? error.message : "Failed to fetch agents";
3868
- this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
3950
+ if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
3951
+ this.agentUpdatesSubscription = null;
3952
+ }
3953
+ const code = error instanceof SessionRequestError ? error.code : 'fetch_agents_failed';
3954
+ const message = error instanceof Error ? error.message : 'Failed to fetch agents';
3955
+ this.sessionLogger.error({ err: error }, 'Failed to handle fetch_agents_request');
3869
3956
  this.emit({
3870
- type: "rpc_error",
3957
+ type: 'rpc_error',
3871
3958
  payload: {
3872
3959
  requestId: request.requestId,
3873
3960
  requestType: request.type,
@@ -3881,7 +3968,7 @@ export class Session {
3881
3968
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
3882
3969
  if (!resolved.ok) {
3883
3970
  this.emit({
3884
- type: "fetch_agent_response",
3971
+ type: 'fetch_agent_response',
3885
3972
  payload: { requestId, agent: null, error: resolved.error },
3886
3973
  });
3887
3974
  return;
@@ -3889,20 +3976,20 @@ export class Session {
3889
3976
  const agent = await this.getAgentPayloadById(resolved.agentId);
3890
3977
  if (!agent) {
3891
3978
  this.emit({
3892
- type: "fetch_agent_response",
3979
+ type: 'fetch_agent_response',
3893
3980
  payload: { requestId, agent: null, error: `Agent not found: ${resolved.agentId}` },
3894
3981
  });
3895
3982
  return;
3896
3983
  }
3897
3984
  this.emit({
3898
- type: "fetch_agent_response",
3985
+ type: 'fetch_agent_response',
3899
3986
  payload: { requestId, agent, error: null },
3900
3987
  });
3901
3988
  }
3902
3989
  async handleFetchAgentTimelineRequest(msg) {
3903
- const direction = msg.direction ?? (msg.cursor ? "after" : "tail");
3904
- const projection = msg.projection ?? "projected";
3905
- const limit = msg.limit ?? (direction === "after" ? 0 : undefined);
3990
+ const direction = msg.direction ?? (msg.cursor ? 'after' : 'tail');
3991
+ const projection = msg.projection ?? 'projected';
3992
+ const limit = msg.limit ?? (direction === 'after' ? 0 : undefined);
3906
3993
  const cursor = msg.cursor
3907
3994
  ? {
3908
3995
  epoch: msg.cursor.epoch,
@@ -3919,14 +4006,10 @@ export class Session {
3919
4006
  const projected = projectTimelineRows(timeline.rows, snapshot.provider, projection);
3920
4007
  const firstRow = timeline.rows[0];
3921
4008
  const lastRow = timeline.rows[timeline.rows.length - 1];
3922
- const startCursor = firstRow
3923
- ? { epoch: timeline.epoch, seq: firstRow.seq }
3924
- : null;
3925
- const endCursor = lastRow
3926
- ? { epoch: timeline.epoch, seq: lastRow.seq }
3927
- : null;
4009
+ const startCursor = firstRow ? { epoch: timeline.epoch, seq: firstRow.seq } : null;
4010
+ const endCursor = lastRow ? { epoch: timeline.epoch, seq: lastRow.seq } : null;
3928
4011
  this.emit({
3929
- type: "fetch_agent_timeline_response",
4012
+ type: 'fetch_agent_timeline_response',
3930
4013
  payload: {
3931
4014
  requestId: msg.requestId,
3932
4015
  agentId: msg.agentId,
@@ -3947,15 +4030,15 @@ export class Session {
3947
4030
  });
3948
4031
  }
3949
4032
  catch (error) {
3950
- this.sessionLogger.error({ err: error, agentId: msg.agentId }, "Failed to handle fetch_agent_timeline_request");
4033
+ this.sessionLogger.error({ err: error, agentId: msg.agentId }, 'Failed to handle fetch_agent_timeline_request');
3951
4034
  this.emit({
3952
- type: "fetch_agent_timeline_response",
4035
+ type: 'fetch_agent_timeline_response',
3953
4036
  payload: {
3954
4037
  requestId: msg.requestId,
3955
4038
  agentId: msg.agentId,
3956
4039
  direction,
3957
4040
  projection,
3958
- epoch: "",
4041
+ epoch: '',
3959
4042
  reset: false,
3960
4043
  staleCursor: false,
3961
4044
  gap: false,
@@ -3974,7 +4057,7 @@ export class Session {
3974
4057
  const resolved = await this.resolveAgentIdentifier(msg.agentId);
3975
4058
  if (!resolved.ok) {
3976
4059
  this.emit({
3977
- type: "send_agent_message_response",
4060
+ type: 'send_agent_message_response',
3978
4061
  payload: {
3979
4062
  requestId: msg.requestId,
3980
4063
  agentId: msg.agentId,
@@ -3995,13 +4078,13 @@ export class Session {
3995
4078
  });
3996
4079
  }
3997
4080
  catch (error) {
3998
- this.sessionLogger.error({ err: error, agentId }, "Failed to record user message for send_agent_message_request");
4081
+ this.sessionLogger.error({ err: error, agentId }, 'Failed to record user message for send_agent_message_request');
3999
4082
  }
4000
4083
  const prompt = this.buildAgentPrompt(msg.text, msg.images);
4001
4084
  const started = this.startAgentStream(agentId, prompt);
4002
4085
  if (!started.ok) {
4003
4086
  this.emit({
4004
- type: "send_agent_message_response",
4087
+ type: 'send_agent_message_response',
4005
4088
  payload: {
4006
4089
  requestId: msg.requestId,
4007
4090
  agentId,
@@ -4013,14 +4096,18 @@ export class Session {
4013
4096
  }
4014
4097
  const startAbort = new AbortController();
4015
4098
  const startTimeoutMs = 15000;
4016
- const startTimeout = setTimeout(() => startAbort.abort("timeout"), startTimeoutMs);
4099
+ const startTimeout = setTimeout(() => startAbort.abort('timeout'), startTimeoutMs);
4017
4100
  try {
4018
4101
  await this.agentManager.waitForAgentRunStart(agentId, { signal: startAbort.signal });
4019
4102
  }
4020
4103
  catch (error) {
4021
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
4104
+ const message = error instanceof Error
4105
+ ? error.message
4106
+ : typeof error === 'string'
4107
+ ? error
4108
+ : 'Unknown error';
4022
4109
  this.emit({
4023
- type: "send_agent_message_response",
4110
+ type: 'send_agent_message_response',
4024
4111
  payload: {
4025
4112
  requestId: msg.requestId,
4026
4113
  agentId,
@@ -4034,7 +4121,7 @@ export class Session {
4034
4121
  clearTimeout(startTimeout);
4035
4122
  }
4036
4123
  this.emit({
4037
- type: "send_agent_message_response",
4124
+ type: 'send_agent_message_response',
4038
4125
  payload: {
4039
4126
  requestId: msg.requestId,
4040
4127
  agentId,
@@ -4044,9 +4131,9 @@ export class Session {
4044
4131
  });
4045
4132
  }
4046
4133
  catch (error) {
4047
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
4134
+ const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
4048
4135
  this.emit({
4049
- type: "send_agent_message_response",
4136
+ type: 'send_agent_message_response',
4050
4137
  payload: {
4051
4138
  requestId: msg.requestId,
4052
4139
  agentId: resolved.agentId,
@@ -4060,8 +4147,14 @@ export class Session {
4060
4147
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
4061
4148
  if (!resolved.ok) {
4062
4149
  this.emit({
4063
- type: "wait_for_finish_response",
4064
- payload: { requestId, status: "error", final: null, error: resolved.error, lastMessage: null },
4150
+ type: 'wait_for_finish_response',
4151
+ payload: {
4152
+ requestId,
4153
+ status: 'error',
4154
+ final: null,
4155
+ error: resolved.error,
4156
+ lastMessage: null,
4157
+ },
4065
4158
  });
4066
4159
  return;
4067
4160
  }
@@ -4071,10 +4164,10 @@ export class Session {
4071
4164
  const record = await this.agentStorage.get(agentId);
4072
4165
  if (!record || record.internal) {
4073
4166
  this.emit({
4074
- type: "wait_for_finish_response",
4167
+ type: 'wait_for_finish_response',
4075
4168
  payload: {
4076
4169
  requestId,
4077
- status: "error",
4170
+ status: 'error',
4078
4171
  final: null,
4079
4172
  error: `Agent not found: ${agentId}`,
4080
4173
  lastMessage: null,
@@ -4083,13 +4176,13 @@ export class Session {
4083
4176
  return;
4084
4177
  }
4085
4178
  const final = this.buildStoredAgentPayload(record);
4086
- const status = record.attentionReason === "permission"
4087
- ? "permission"
4088
- : record.lastStatus === "error"
4089
- ? "error"
4090
- : "idle";
4179
+ const status = record.attentionReason === 'permission'
4180
+ ? 'permission'
4181
+ : record.lastStatus === 'error'
4182
+ ? 'error'
4183
+ : 'idle';
4091
4184
  this.emit({
4092
- type: "wait_for_finish_response",
4185
+ type: 'wait_for_finish_response',
4093
4186
  payload: { requestId, status, final, error: null, lastMessage: null },
4094
4187
  });
4095
4188
  return;
@@ -4097,7 +4190,7 @@ export class Session {
4097
4190
  const abortController = new AbortController();
4098
4191
  const effectiveTimeoutMs = timeoutMs ?? 600000; // 10 minutes default
4099
4192
  const timeoutHandle = setTimeout(() => {
4100
- abortController.abort("timeout");
4193
+ abortController.abort('timeout');
4101
4194
  }, effectiveTimeoutMs);
4102
4195
  try {
4103
4196
  const result = await this.agentManager.waitForAgentEvent(agentId, {
@@ -4107,28 +4200,28 @@ export class Session {
4107
4200
  if (!final) {
4108
4201
  throw new Error(`Agent ${agentId} disappeared while waiting`);
4109
4202
  }
4110
- const status = result.permission
4111
- ? "permission"
4112
- : result.status === "error"
4113
- ? "error"
4114
- : "idle";
4203
+ const status = result.permission ? 'permission' : result.status === 'error' ? 'error' : 'idle';
4115
4204
  this.emit({
4116
- type: "wait_for_finish_response",
4205
+ type: 'wait_for_finish_response',
4117
4206
  payload: { requestId, status, final, error: null, lastMessage: result.lastMessage },
4118
4207
  });
4119
4208
  }
4120
4209
  catch (error) {
4121
4210
  const isAbort = error instanceof Error &&
4122
- (error.name === "AbortError" || error.message.toLowerCase().includes("aborted"));
4211
+ (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted'));
4123
4212
  if (!isAbort) {
4124
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
4125
- this.sessionLogger.error({ err: error, agentId }, "wait_for_finish_request failed");
4213
+ const message = error instanceof Error
4214
+ ? error.message
4215
+ : typeof error === 'string'
4216
+ ? error
4217
+ : 'Unknown error';
4218
+ this.sessionLogger.error({ err: error, agentId }, 'wait_for_finish_request failed');
4126
4219
  const final = await this.getAgentPayloadById(agentId);
4127
4220
  this.emit({
4128
- type: "wait_for_finish_response",
4221
+ type: 'wait_for_finish_response',
4129
4222
  payload: {
4130
4223
  requestId,
4131
- status: "error",
4224
+ status: 'error',
4132
4225
  final,
4133
4226
  error: message,
4134
4227
  lastMessage: null,
@@ -4141,8 +4234,8 @@ export class Session {
4141
4234
  throw new Error(`Agent ${agentId} disappeared while waiting`);
4142
4235
  }
4143
4236
  this.emit({
4144
- type: "wait_for_finish_response",
4145
- payload: { requestId, status: "timeout", final, error: null, lastMessage: null },
4237
+ type: 'wait_for_finish_response',
4238
+ payload: { requestId, status: 'timeout', final, error: null, lastMessage: null },
4146
4239
  });
4147
4240
  }
4148
4241
  finally {
@@ -4154,24 +4247,24 @@ export class Session {
4154
4247
  */
4155
4248
  async handleAudioChunk(msg) {
4156
4249
  if (!this.isVoiceMode) {
4157
- this.sessionLogger.warn("Received voice_audio_chunk while voice mode is disabled; transcript will be emitted but voice assistant turn is skipped");
4250
+ this.sessionLogger.warn('Received voice_audio_chunk while voice mode is disabled; transcript will be emitted but voice assistant turn is skipped');
4158
4251
  }
4159
4252
  await this.handleVoiceSpeechStart();
4160
- const chunkFormat = msg.format || "audio/wav";
4253
+ const chunkFormat = msg.format || 'audio/wav';
4161
4254
  if (this.isVoiceMode) {
4162
4255
  await this.appendToActiveVoiceDictationStream(msg.audio, chunkFormat);
4163
4256
  if (!msg.isLast) {
4164
4257
  this.setVoiceModeInactivityTimeout();
4165
- this.sessionLogger.debug("Voice mode: streaming chunk, waiting for speech end");
4258
+ this.sessionLogger.debug('Voice mode: streaming chunk, waiting for speech end');
4166
4259
  return;
4167
4260
  }
4168
4261
  this.clearVoiceModeInactivityTimeout();
4169
- this.sessionLogger.debug("Voice mode: speech ended, finalizing streaming transcription");
4170
- await this.finalizeActiveVoiceDictationStream("speech ended");
4262
+ this.sessionLogger.debug('Voice mode: speech ended, finalizing streaming transcription');
4263
+ await this.finalizeActiveVoiceDictationStream('speech ended');
4171
4264
  return;
4172
4265
  }
4173
- const chunkBuffer = Buffer.from(msg.audio, "base64");
4174
- const isPCMChunk = chunkFormat.toLowerCase().includes("pcm");
4266
+ const chunkBuffer = Buffer.from(msg.audio, 'base64');
4267
+ const isPCMChunk = chunkFormat.toLowerCase().includes('pcm');
4175
4268
  if (!this.audioBuffer) {
4176
4269
  this.audioBuffer = {
4177
4270
  chunks: [],
@@ -4182,7 +4275,10 @@ export class Session {
4182
4275
  }
4183
4276
  // If the format changes mid-stream, flush what we have first
4184
4277
  if (this.audioBuffer.isPCM !== isPCMChunk) {
4185
- this.sessionLogger.debug({ oldFormat: this.audioBuffer.isPCM ? "pcm" : this.audioBuffer.format, newFormat: chunkFormat }, `Audio format changed mid-stream, flushing current buffer`);
4278
+ this.sessionLogger.debug({
4279
+ oldFormat: this.audioBuffer.isPCM ? 'pcm' : this.audioBuffer.format,
4280
+ newFormat: chunkFormat,
4281
+ }, `Audio format changed mid-stream, flushing current buffer`);
4186
4282
  const finalized = this.finalizeBufferedAudio();
4187
4283
  if (finalized) {
4188
4284
  await this.processCompletedAudio(finalized.audio, finalized.format);
@@ -4215,7 +4311,10 @@ export class Session {
4215
4311
  return;
4216
4312
  }
4217
4313
  if (!msg.isLast && reachedStreamingThreshold) {
4218
- this.sessionLogger.debug({ minDuration: MIN_STREAMING_SEGMENT_DURATION_MS, pcmBytes: bufferedState?.totalPCMBytes ?? 0 }, `Minimum chunk duration reached (~${MIN_STREAMING_SEGMENT_DURATION_MS}ms, ${bufferedState?.totalPCMBytes ?? 0} PCM bytes) – triggering STT`);
4314
+ this.sessionLogger.debug({
4315
+ minDuration: MIN_STREAMING_SEGMENT_DURATION_MS,
4316
+ pcmBytes: bufferedState?.totalPCMBytes ?? 0,
4317
+ }, `Minimum chunk duration reached (~${MIN_STREAMING_SEGMENT_DURATION_MS}ms, ${bufferedState?.totalPCMBytes ?? 0} PCM bytes) – triggering STT`);
4219
4318
  }
4220
4319
  else {
4221
4320
  this.sessionLogger.debug({ audioBytes: finalized.audio.length, chunks: bufferedState?.chunks.length ?? 0 }, `Complete audio segment (${finalized.audio.length} bytes, ${bufferedState?.chunks.length ?? 0} chunk(s))`);
@@ -4233,7 +4332,7 @@ export class Session {
4233
4332
  const wavBuffer = convertPCMToWavBuffer(pcmBuffer, PCM_SAMPLE_RATE, PCM_CHANNELS, PCM_BITS_PER_SAMPLE);
4234
4333
  return {
4235
4334
  audio: wavBuffer,
4236
- format: "audio/wav",
4335
+ format: 'audio/wav',
4237
4336
  };
4238
4337
  }
4239
4338
  return {
@@ -4242,8 +4341,7 @@ export class Session {
4242
4341
  };
4243
4342
  }
4244
4343
  async processCompletedAudio(audio, format) {
4245
- const shouldBuffer = this.processingPhase === "transcribing" &&
4246
- this.pendingAudioSegments.length === 0;
4344
+ const shouldBuffer = this.processingPhase === 'transcribing' && this.pendingAudioSegments.length === 0;
4247
4345
  if (shouldBuffer) {
4248
4346
  this.sessionLogger.debug({ phase: this.processingPhase }, `Buffering audio segment (phase: ${this.processingPhase})`);
4249
4347
  this.pendingAudioSegments.push({
@@ -4273,21 +4371,21 @@ export class Session {
4273
4371
  * Process audio through STT and then LLM
4274
4372
  */
4275
4373
  async processAudio(audio, format) {
4276
- this.setPhase("transcribing");
4374
+ this.setPhase('transcribing');
4277
4375
  this.emit({
4278
- type: "activity_log",
4376
+ type: 'activity_log',
4279
4377
  payload: {
4280
4378
  id: uuidv4(),
4281
4379
  timestamp: new Date(),
4282
- type: "system",
4283
- content: "Transcribing audio...",
4380
+ type: 'system',
4381
+ content: 'Transcribing audio...',
4284
4382
  },
4285
4383
  });
4286
4384
  try {
4287
4385
  const requestId = uuidv4();
4288
4386
  const result = await this.sttManager.transcribe(audio, format, {
4289
4387
  requestId,
4290
- label: this.isVoiceMode ? "voice" : "buffered",
4388
+ label: this.isVoiceMode ? 'voice' : 'buffered',
4291
4389
  });
4292
4390
  const transcriptText = result.text.trim();
4293
4391
  this.sessionLogger.info({
@@ -4295,7 +4393,7 @@ export class Session {
4295
4393
  isVoiceMode: this.isVoiceMode,
4296
4394
  transcriptLength: transcriptText.length,
4297
4395
  transcript: transcriptText,
4298
- }, "Transcription result");
4396
+ }, 'Transcription result');
4299
4397
  await this.handleTranscriptionResultPayload({
4300
4398
  text: result.text,
4301
4399
  language: result.language,
@@ -4309,14 +4407,14 @@ export class Session {
4309
4407
  });
4310
4408
  }
4311
4409
  catch (error) {
4312
- this.setPhase("idle");
4313
- this.clearSpeechInProgress("transcription error");
4410
+ this.setPhase('idle');
4411
+ this.clearSpeechInProgress('transcription error');
4314
4412
  this.emit({
4315
- type: "activity_log",
4413
+ type: 'activity_log',
4316
4414
  payload: {
4317
4415
  id: uuidv4(),
4318
4416
  timestamp: new Date(),
4319
- type: "error",
4417
+ type: 'error',
4320
4418
  content: `Transcription error: ${error.message}`,
4321
4419
  },
4322
4420
  });
@@ -4326,34 +4424,36 @@ export class Session {
4326
4424
  async handleTranscriptionResultPayload(result) {
4327
4425
  const transcriptText = result.text.trim();
4328
4426
  this.emit({
4329
- type: "transcription_result",
4427
+ type: 'transcription_result',
4330
4428
  payload: {
4331
4429
  text: result.text,
4332
4430
  ...(result.language ? { language: result.language } : {}),
4333
4431
  ...(result.duration !== undefined ? { duration: result.duration } : {}),
4334
4432
  requestId: result.requestId,
4335
4433
  ...(result.avgLogprob !== undefined ? { avgLogprob: result.avgLogprob } : {}),
4336
- ...(result.isLowConfidence !== undefined ? { isLowConfidence: result.isLowConfidence } : {}),
4434
+ ...(result.isLowConfidence !== undefined
4435
+ ? { isLowConfidence: result.isLowConfidence }
4436
+ : {}),
4337
4437
  ...(result.byteLength !== undefined ? { byteLength: result.byteLength } : {}),
4338
4438
  ...(result.format ? { format: result.format } : {}),
4339
4439
  ...(result.debugRecordingPath ? { debugRecordingPath: result.debugRecordingPath } : {}),
4340
4440
  },
4341
4441
  });
4342
4442
  if (!transcriptText) {
4343
- this.sessionLogger.debug("Empty transcription (false positive), not aborting");
4344
- this.setPhase("idle");
4345
- this.clearSpeechInProgress("empty transcription");
4443
+ this.sessionLogger.debug('Empty transcription (false positive), not aborting');
4444
+ this.setPhase('idle');
4445
+ this.clearSpeechInProgress('empty transcription');
4346
4446
  return;
4347
4447
  }
4348
4448
  // Has content - abort any in-progress stream now
4349
4449
  this.createAbortController();
4350
4450
  if (result.debugRecordingPath) {
4351
4451
  this.emit({
4352
- type: "activity_log",
4452
+ type: 'activity_log',
4353
4453
  payload: {
4354
4454
  id: uuidv4(),
4355
4455
  timestamp: new Date(),
4356
- type: "system",
4456
+ type: 'system',
4357
4457
  content: `Saved input audio: ${result.debugRecordingPath}`,
4358
4458
  metadata: {
4359
4459
  recordingPath: result.debugRecordingPath,
@@ -4364,11 +4464,11 @@ export class Session {
4364
4464
  });
4365
4465
  }
4366
4466
  this.emit({
4367
- type: "activity_log",
4467
+ type: 'activity_log',
4368
4468
  payload: {
4369
4469
  id: uuidv4(),
4370
4470
  timestamp: new Date(),
4371
- type: "transcript",
4471
+ type: 'transcript',
4372
4472
  content: result.text,
4373
4473
  metadata: {
4374
4474
  ...(result.language ? { language: result.language } : {}),
@@ -4376,15 +4476,15 @@ export class Session {
4376
4476
  },
4377
4477
  },
4378
4478
  });
4379
- this.clearSpeechInProgress("transcription complete");
4380
- this.setPhase("idle");
4479
+ this.clearSpeechInProgress('transcription complete');
4480
+ this.setPhase('idle');
4381
4481
  if (!this.isVoiceMode) {
4382
- this.sessionLogger.debug({ requestId: result.requestId }, "Skipping voice agent processing because voice mode is disabled");
4482
+ this.sessionLogger.debug({ requestId: result.requestId }, 'Skipping voice agent processing because voice mode is disabled');
4383
4483
  return;
4384
4484
  }
4385
4485
  const agentId = this.voiceModeAgentId;
4386
4486
  if (!agentId) {
4387
- this.sessionLogger.warn({ requestId: result.requestId }, "Skipping voice agent processing because no agent is currently voice-enabled");
4487
+ this.sessionLogger.warn({ requestId: result.requestId }, 'Skipping voice agent processing because no agent is currently voice-enabled');
4388
4488
  return;
4389
4489
  }
4390
4490
  // Route voice utterances through the same send path as regular text input:
@@ -4397,22 +4497,22 @@ export class Session {
4397
4497
  agentId,
4398
4498
  textLength: text.length,
4399
4499
  preview: text.slice(0, 160),
4400
- }, "Voice speak tool call received by session handler");
4500
+ }, 'Voice speak tool call received by session handler');
4401
4501
  const abortSignal = signal ?? this.abortController.signal;
4402
4502
  await this.ttsManager.generateAndWaitForPlayback(text, (msg) => this.emit(msg), abortSignal, true);
4403
- this.sessionLogger.info({ agentId, textLength: text.length }, "Voice speak tool call finished playback");
4503
+ this.sessionLogger.info({ agentId, textLength: text.length }, 'Voice speak tool call finished playback');
4404
4504
  this.emit({
4405
- type: "activity_log",
4505
+ type: 'activity_log',
4406
4506
  payload: {
4407
4507
  id: uuidv4(),
4408
4508
  timestamp: new Date(),
4409
- type: "assistant",
4509
+ type: 'assistant',
4410
4510
  content: text,
4411
4511
  },
4412
4512
  });
4413
4513
  });
4414
4514
  this.registerVoiceCallerContext?.(agentId, {
4415
- childAgentDefaultLabels: { ui: "true" },
4515
+ childAgentDefaultLabels: { ui: 'true' },
4416
4516
  allowCustomCwd: false,
4417
4517
  enableVoiceTools: true,
4418
4518
  });
@@ -4423,24 +4523,24 @@ export class Session {
4423
4523
  async handleAbort() {
4424
4524
  this.sessionLogger.info({ phase: this.processingPhase }, `Abort request, phase: ${this.processingPhase}`);
4425
4525
  this.abortController.abort();
4426
- this.ttsManager.cancelPendingPlaybacks("abort request");
4526
+ this.ttsManager.cancelPendingPlaybacks('abort request');
4427
4527
  // Voice abort should always interrupt active agent output immediately.
4428
4528
  if (this.isVoiceMode && this.voiceModeAgentId) {
4429
4529
  try {
4430
4530
  await this.interruptAgentIfRunning(this.voiceModeAgentId);
4431
4531
  }
4432
4532
  catch (error) {
4433
- this.sessionLogger.warn({ err: error, agentId: this.voiceModeAgentId }, "Failed to interrupt active voice-mode agent on abort");
4533
+ this.sessionLogger.warn({ err: error, agentId: this.voiceModeAgentId }, 'Failed to interrupt active voice-mode agent on abort');
4434
4534
  }
4435
4535
  }
4436
- if (this.processingPhase === "transcribing") {
4536
+ if (this.processingPhase === 'transcribing') {
4437
4537
  // Still in STT phase - we'll buffer the next audio
4438
- this.sessionLogger.debug("Will buffer next audio (currently transcribing)");
4538
+ this.sessionLogger.debug('Will buffer next audio (currently transcribing)');
4439
4539
  // Phase stays as 'transcribing', handleAudioChunk will handle buffering
4440
4540
  return;
4441
4541
  }
4442
4542
  // Reset phase to idle and clear pending non-voice buffers.
4443
- this.setPhase("idle");
4543
+ this.setPhase('idle');
4444
4544
  this.pendingAudioSegments = [];
4445
4545
  this.clearBufferTimeout();
4446
4546
  }
@@ -4461,24 +4561,22 @@ export class Session {
4461
4561
  const phaseBeforeAbort = this.processingPhase;
4462
4562
  const hadActiveStream = this.hasActiveAgentRun(this.voiceModeAgentId);
4463
4563
  this.speechInProgress = true;
4464
- this.sessionLogger.debug("Voice speech detected – aborting playback and active agent run");
4564
+ this.sessionLogger.debug('Voice speech detected – aborting playback and active agent run');
4465
4565
  if (this.pendingAudioSegments.length > 0) {
4466
4566
  this.sessionLogger.debug({ segmentCount: this.pendingAudioSegments.length }, `Dropping ${this.pendingAudioSegments.length} buffered audio segment(s) due to voice speech`);
4467
4567
  this.pendingAudioSegments = [];
4468
4568
  }
4469
4569
  if (this.audioBuffer) {
4470
- this.sessionLogger.debug({ chunks: this.audioBuffer.chunks.length, pcmBytes: this.audioBuffer.totalPCMBytes }, `Clearing partial audio buffer (${this.audioBuffer.chunks.length} chunk(s)${this.audioBuffer.isPCM
4471
- ? `, ${this.audioBuffer.totalPCMBytes} PCM bytes`
4472
- : ""})`);
4570
+ this.sessionLogger.debug({ chunks: this.audioBuffer.chunks.length, pcmBytes: this.audioBuffer.totalPCMBytes }, `Clearing partial audio buffer (${this.audioBuffer.chunks.length} chunk(s)${this.audioBuffer.isPCM ? `, ${this.audioBuffer.totalPCMBytes} PCM bytes` : ''})`);
4473
4571
  this.audioBuffer = null;
4474
4572
  }
4475
- this.cancelActiveVoiceDictationStream("new speech turn started");
4573
+ this.cancelActiveVoiceDictationStream('new speech turn started');
4476
4574
  this.clearVoiceModeInactivityTimeout();
4477
4575
  this.clearBufferTimeout();
4478
4576
  this.abortController.abort();
4479
4577
  await this.handleAbort();
4480
4578
  const latencyMs = Date.now() - chunkReceivedAt;
4481
- this.sessionLogger.debug({ latencyMs, phaseBeforeAbort, hadActiveStream }, "[Telemetry] barge_in.llm_abort_latency");
4579
+ this.sessionLogger.debug({ latencyMs, phaseBeforeAbort, hadActiveStream }, '[Telemetry] barge_in.llm_abort_latency');
4482
4580
  }
4483
4581
  /**
4484
4582
  * Clear speech-in-progress flag once the user turn has completed
@@ -4512,7 +4610,7 @@ export class Session {
4512
4610
  setBufferTimeout() {
4513
4611
  this.clearBufferTimeout();
4514
4612
  this.bufferTimeout = setTimeout(async () => {
4515
- this.sessionLogger.debug("Buffer timeout reached, processing pending segments");
4613
+ this.sessionLogger.debug('Buffer timeout reached, processing pending segments');
4516
4614
  if (this.pendingAudioSegments.length > 0) {
4517
4615
  const segments = [...this.pendingAudioSegments];
4518
4616
  this.pendingAudioSegments = [];
@@ -4536,9 +4634,9 @@ export class Session {
4536
4634
  timeoutMs: VOICE_MODE_INACTIVITY_FLUSH_MS,
4537
4635
  dictationId: this.activeVoiceDictationId,
4538
4636
  nextSeq: this.activeVoiceDictationNextSeq,
4539
- }, "Voice mode inactivity timeout reached without isLast; finalizing active voice dictation stream");
4540
- void this.finalizeActiveVoiceDictationStream("inactivity timeout").catch((error) => {
4541
- this.sessionLogger.error({ err: error }, "Failed to finalize voice dictation stream after inactivity timeout");
4637
+ }, 'Voice mode inactivity timeout reached without isLast; finalizing active voice dictation stream');
4638
+ void this.finalizeActiveVoiceDictationStream('inactivity timeout').catch((error) => {
4639
+ this.sessionLogger.error({ err: error }, 'Failed to finalize voice dictation stream after inactivity timeout');
4542
4640
  });
4543
4641
  }, VOICE_MODE_INACTIVITY_FLUSH_MS);
4544
4642
  }
@@ -4561,15 +4659,15 @@ export class Session {
4561
4659
  * Emit a message to the client
4562
4660
  */
4563
4661
  emit(msg) {
4564
- if (msg.type === "audio_output" &&
4662
+ if (msg.type === 'audio_output' &&
4565
4663
  (process.env.TTS_DEBUG_AUDIO_DIR || isPaseoDictationDebugEnabled()) &&
4566
4664
  msg.payload.groupId &&
4567
- typeof msg.payload.audio === "string") {
4665
+ typeof msg.payload.audio === 'string') {
4568
4666
  const groupId = msg.payload.groupId;
4569
4667
  const existing = this.ttsDebugStreams.get(groupId) ??
4570
4668
  { format: msg.payload.format, chunks: [] };
4571
4669
  try {
4572
- existing.chunks.push(Buffer.from(msg.payload.audio, "base64"));
4670
+ existing.chunks.push(Buffer.from(msg.payload.audio, 'base64'));
4573
4671
  existing.format = msg.payload.format;
4574
4672
  this.ttsDebugStreams.set(groupId, existing);
4575
4673
  }
@@ -4584,11 +4682,11 @@ export class Session {
4584
4682
  const recordingPath = await maybePersistTtsDebugAudio(Buffer.concat(final.chunks), { sessionId: this.sessionId, groupId, format: final.format }, this.sessionLogger);
4585
4683
  if (recordingPath) {
4586
4684
  this.onMessage({
4587
- type: "activity_log",
4685
+ type: 'activity_log',
4588
4686
  payload: {
4589
4687
  id: uuidv4(),
4590
4688
  timestamp: new Date(),
4591
- type: "system",
4689
+ type: 'system',
4592
4690
  content: `Saved TTS audio: ${recordingPath}`,
4593
4691
  metadata: { recordingPath, format: final.format, groupId },
4594
4692
  },
@@ -4608,14 +4706,14 @@ export class Session {
4608
4706
  this.onBinaryMessage(frame);
4609
4707
  }
4610
4708
  catch (error) {
4611
- this.sessionLogger.error({ err: error }, "Failed to emit binary frame");
4709
+ this.sessionLogger.error({ err: error }, 'Failed to emit binary frame');
4612
4710
  }
4613
4711
  }
4614
4712
  /**
4615
4713
  * Clean up session resources
4616
4714
  */
4617
4715
  async cleanup() {
4618
- this.sessionLogger.trace("Cleaning up");
4716
+ this.sessionLogger.trace('Cleaning up');
4619
4717
  if (this.unsubscribeAgentEvents) {
4620
4718
  this.unsubscribeAgentEvents();
4621
4719
  this.unsubscribeAgentEvents = null;
@@ -4626,7 +4724,7 @@ export class Session {
4626
4724
  this.clearVoiceModeInactivityTimeout();
4627
4725
  this.clearBufferTimeout();
4628
4726
  // Clear buffers
4629
- this.cancelActiveVoiceDictationStream("session cleanup");
4727
+ this.cancelActiveVoiceDictationStream('session cleanup');
4630
4728
  this.pendingAudioSegments = [];
4631
4729
  this.audioBuffer = null;
4632
4730
  // Cleanup managers
@@ -4638,10 +4736,10 @@ export class Session {
4638
4736
  if (this.agentMcpClient) {
4639
4737
  try {
4640
4738
  await this.agentMcpClient.close();
4641
- this.sessionLogger.debug("Agent MCP client closed");
4739
+ this.sessionLogger.debug('Agent MCP client closed');
4642
4740
  }
4643
4741
  catch (error) {
4644
- this.sessionLogger.error({ err: error }, "Failed to close Agent MCP client");
4742
+ this.sessionLogger.error({ err: error }, 'Failed to close Agent MCP client');
4645
4743
  }
4646
4744
  this.agentMcpClient = null;
4647
4745
  this.agentTools = null;
@@ -4693,18 +4791,18 @@ export class Session {
4693
4791
  unsubscribe();
4694
4792
  }
4695
4793
  catch (error) {
4696
- this.sessionLogger.warn({ err: error, terminalId }, "Failed to unsubscribe terminal after process exit");
4794
+ this.sessionLogger.warn({ err: error, terminalId }, 'Failed to unsubscribe terminal after process exit');
4697
4795
  }
4698
4796
  this.terminalSubscriptions.delete(terminalId);
4699
4797
  }
4700
4798
  const streamId = this.terminalStreamByTerminalId.get(terminalId);
4701
- if (typeof streamId === "number") {
4799
+ if (typeof streamId === 'number') {
4702
4800
  this.detachTerminalStream(streamId, { emitExit: true });
4703
4801
  }
4704
4802
  }
4705
4803
  emitTerminalsChangedSnapshot(input) {
4706
4804
  this.emit({
4707
- type: "terminals_changed",
4805
+ type: 'terminals_changed',
4708
4806
  payload: {
4709
4807
  cwd: input.cwd,
4710
4808
  terminals: input.terminals,
@@ -4734,9 +4832,7 @@ export class Session {
4734
4832
  if (!this.terminalManager || !this.subscribedTerminalDirectories.has(cwd)) {
4735
4833
  return;
4736
4834
  }
4737
- const hadDirectoryBeforeSubscribe = this.terminalManager
4738
- .listDirectories()
4739
- .includes(cwd);
4835
+ const hadDirectoryBeforeSubscribe = this.terminalManager.listDirectories().includes(cwd);
4740
4836
  try {
4741
4837
  const terminals = await this.terminalManager.getTerminals(cwd);
4742
4838
  for (const terminal of terminals) {
@@ -4759,13 +4855,13 @@ export class Session {
4759
4855
  });
4760
4856
  }
4761
4857
  catch (error) {
4762
- this.sessionLogger.warn({ err: error, cwd }, "Failed to emit initial terminal snapshot");
4858
+ this.sessionLogger.warn({ err: error, cwd }, 'Failed to emit initial terminal snapshot');
4763
4859
  }
4764
4860
  }
4765
4861
  async handleListTerminalsRequest(msg) {
4766
4862
  if (!this.terminalManager) {
4767
4863
  this.emit({
4768
- type: "list_terminals_response",
4864
+ type: 'list_terminals_response',
4769
4865
  payload: {
4770
4866
  cwd: msg.cwd,
4771
4867
  terminals: [],
@@ -4780,7 +4876,7 @@ export class Session {
4780
4876
  this.ensureTerminalExitSubscription(terminal);
4781
4877
  }
4782
4878
  this.emit({
4783
- type: "list_terminals_response",
4879
+ type: 'list_terminals_response',
4784
4880
  payload: {
4785
4881
  cwd: msg.cwd,
4786
4882
  terminals: terminals.map((t) => ({ id: t.id, name: t.name })),
@@ -4789,9 +4885,9 @@ export class Session {
4789
4885
  });
4790
4886
  }
4791
4887
  catch (error) {
4792
- this.sessionLogger.error({ err: error, cwd: msg.cwd }, "Failed to list terminals");
4888
+ this.sessionLogger.error({ err: error, cwd: msg.cwd }, 'Failed to list terminals');
4793
4889
  this.emit({
4794
- type: "list_terminals_response",
4890
+ type: 'list_terminals_response',
4795
4891
  payload: {
4796
4892
  cwd: msg.cwd,
4797
4893
  terminals: [],
@@ -4803,10 +4899,10 @@ export class Session {
4803
4899
  async handleCreateTerminalRequest(msg) {
4804
4900
  if (!this.terminalManager) {
4805
4901
  this.emit({
4806
- type: "create_terminal_response",
4902
+ type: 'create_terminal_response',
4807
4903
  payload: {
4808
4904
  terminal: null,
4809
- error: "Terminal manager not available",
4905
+ error: 'Terminal manager not available',
4810
4906
  requestId: msg.requestId,
4811
4907
  },
4812
4908
  });
@@ -4819,7 +4915,7 @@ export class Session {
4819
4915
  });
4820
4916
  this.ensureTerminalExitSubscription(session);
4821
4917
  this.emit({
4822
- type: "create_terminal_response",
4918
+ type: 'create_terminal_response',
4823
4919
  payload: {
4824
4920
  terminal: { id: session.id, name: session.name, cwd: session.cwd },
4825
4921
  error: null,
@@ -4828,9 +4924,9 @@ export class Session {
4828
4924
  });
4829
4925
  }
4830
4926
  catch (error) {
4831
- this.sessionLogger.error({ err: error, cwd: msg.cwd }, "Failed to create terminal");
4927
+ this.sessionLogger.error({ err: error, cwd: msg.cwd }, 'Failed to create terminal');
4832
4928
  this.emit({
4833
- type: "create_terminal_response",
4929
+ type: 'create_terminal_response',
4834
4930
  payload: {
4835
4931
  terminal: null,
4836
4932
  error: error.message,
@@ -4842,11 +4938,11 @@ export class Session {
4842
4938
  async handleSubscribeTerminalRequest(msg) {
4843
4939
  if (!this.terminalManager) {
4844
4940
  this.emit({
4845
- type: "subscribe_terminal_response",
4941
+ type: 'subscribe_terminal_response',
4846
4942
  payload: {
4847
4943
  terminalId: msg.terminalId,
4848
4944
  state: null,
4849
- error: "Terminal manager not available",
4945
+ error: 'Terminal manager not available',
4850
4946
  requestId: msg.requestId,
4851
4947
  },
4852
4948
  });
@@ -4855,11 +4951,11 @@ export class Session {
4855
4951
  const session = this.terminalManager.getTerminal(msg.terminalId);
4856
4952
  if (!session) {
4857
4953
  this.emit({
4858
- type: "subscribe_terminal_response",
4954
+ type: 'subscribe_terminal_response',
4859
4955
  payload: {
4860
4956
  terminalId: msg.terminalId,
4861
4957
  state: null,
4862
- error: "Terminal not found",
4958
+ error: 'Terminal not found',
4863
4959
  requestId: msg.requestId,
4864
4960
  },
4865
4961
  });
@@ -4873,9 +4969,9 @@ export class Session {
4873
4969
  }
4874
4970
  // Subscribe to terminal updates
4875
4971
  const unsubscribe = session.subscribe((serverMsg) => {
4876
- if (serverMsg.type === "full") {
4972
+ if (serverMsg.type === 'full') {
4877
4973
  this.emit({
4878
- type: "terminal_output",
4974
+ type: 'terminal_output',
4879
4975
  payload: {
4880
4976
  terminalId: msg.terminalId,
4881
4977
  state: serverMsg.state,
@@ -4886,7 +4982,7 @@ export class Session {
4886
4982
  this.terminalSubscriptions.set(msg.terminalId, unsubscribe);
4887
4983
  // Send initial state
4888
4984
  this.emit({
4889
- type: "subscribe_terminal_response",
4985
+ type: 'subscribe_terminal_response',
4890
4986
  payload: {
4891
4987
  terminalId: msg.terminalId,
4892
4988
  state: session.getState(),
@@ -4908,16 +5004,55 @@ export class Session {
4908
5004
  }
4909
5005
  const session = this.terminalManager.getTerminal(msg.terminalId);
4910
5006
  if (!session) {
4911
- this.sessionLogger.warn({ terminalId: msg.terminalId }, "Terminal not found for input");
5007
+ this.sessionLogger.warn({ terminalId: msg.terminalId }, 'Terminal not found for input');
4912
5008
  return;
4913
5009
  }
4914
5010
  this.ensureTerminalExitSubscription(session);
4915
5011
  session.send(msg.message);
4916
5012
  }
5013
+ killTrackedTerminal(terminalId, options) {
5014
+ const unsubscribe = this.terminalSubscriptions.get(terminalId);
5015
+ if (unsubscribe) {
5016
+ unsubscribe();
5017
+ this.terminalSubscriptions.delete(terminalId);
5018
+ }
5019
+ const streamId = this.terminalStreamByTerminalId.get(terminalId);
5020
+ if (typeof streamId === 'number') {
5021
+ this.detachTerminalStream(streamId, { emitExit: options?.emitExit ?? true });
5022
+ }
5023
+ this.terminalManager?.killTerminal(terminalId);
5024
+ }
5025
+ async killTerminalsUnderPath(rootPath) {
5026
+ if (!this.terminalManager) {
5027
+ return;
5028
+ }
5029
+ const cleanupErrors = [];
5030
+ const terminalDirectories = [...this.terminalManager.listDirectories()];
5031
+ for (const terminalCwd of terminalDirectories) {
5032
+ if (!this.isPathWithinRoot(rootPath, terminalCwd)) {
5033
+ continue;
5034
+ }
5035
+ try {
5036
+ const terminals = await this.terminalManager.getTerminals(terminalCwd);
5037
+ for (const terminal of [...terminals]) {
5038
+ this.killTrackedTerminal(terminal.id, { emitExit: true });
5039
+ }
5040
+ }
5041
+ catch (error) {
5042
+ const message = error instanceof Error ? error.message : String(error);
5043
+ cleanupErrors.push({ cwd: terminalCwd, message });
5044
+ this.sessionLogger.warn({ err: error, cwd: terminalCwd }, 'Failed to clean up worktree terminals during archive');
5045
+ }
5046
+ }
5047
+ if (cleanupErrors.length > 0) {
5048
+ const details = cleanupErrors.map((entry) => `${entry.cwd}: ${entry.message}`).join('; ');
5049
+ throw new Error(`Failed to clean up worktree terminals during archive (${details})`);
5050
+ }
5051
+ }
4917
5052
  async handleKillTerminalRequest(msg) {
4918
5053
  if (!this.terminalManager) {
4919
5054
  this.emit({
4920
- type: "kill_terminal_response",
5055
+ type: 'kill_terminal_response',
4921
5056
  payload: {
4922
5057
  terminalId: msg.terminalId,
4923
5058
  success: false,
@@ -4926,19 +5061,9 @@ export class Session {
4926
5061
  });
4927
5062
  return;
4928
5063
  }
4929
- // Unsubscribe first
4930
- const unsubscribe = this.terminalSubscriptions.get(msg.terminalId);
4931
- if (unsubscribe) {
4932
- unsubscribe();
4933
- this.terminalSubscriptions.delete(msg.terminalId);
4934
- }
4935
- const streamId = this.terminalStreamByTerminalId.get(msg.terminalId);
4936
- if (typeof streamId === "number") {
4937
- this.detachTerminalStream(streamId, { emitExit: true });
4938
- }
4939
- this.terminalManager.killTerminal(msg.terminalId);
5064
+ this.killTrackedTerminal(msg.terminalId, { emitExit: true });
4940
5065
  this.emit({
4941
- type: "kill_terminal_response",
5066
+ type: 'kill_terminal_response',
4942
5067
  payload: {
4943
5068
  terminalId: msg.terminalId,
4944
5069
  success: true,
@@ -4949,7 +5074,7 @@ export class Session {
4949
5074
  async handleAttachTerminalStreamRequest(msg) {
4950
5075
  if (!this.terminalManager || !this.onBinaryMessage) {
4951
5076
  this.emit({
4952
- type: "attach_terminal_stream_response",
5077
+ type: 'attach_terminal_stream_response',
4953
5078
  payload: {
4954
5079
  terminalId: msg.terminalId,
4955
5080
  streamId: null,
@@ -4957,7 +5082,7 @@ export class Session {
4957
5082
  currentOffset: 0,
4958
5083
  earliestAvailableOffset: 0,
4959
5084
  reset: true,
4960
- error: "Terminal streaming not available",
5085
+ error: 'Terminal streaming not available',
4961
5086
  requestId: msg.requestId,
4962
5087
  },
4963
5088
  });
@@ -4966,7 +5091,7 @@ export class Session {
4966
5091
  const session = this.terminalManager.getTerminal(msg.terminalId);
4967
5092
  if (!session) {
4968
5093
  this.emit({
4969
- type: "attach_terminal_stream_response",
5094
+ type: 'attach_terminal_stream_response',
4970
5095
  payload: {
4971
5096
  terminalId: msg.terminalId,
4972
5097
  streamId: null,
@@ -4974,7 +5099,7 @@ export class Session {
4974
5099
  currentOffset: 0,
4975
5100
  earliestAvailableOffset: 0,
4976
5101
  reset: true,
4977
- error: "Terminal not found",
5102
+ error: 'Terminal not found',
4978
5103
  requestId: msg.requestId,
4979
5104
  },
4980
5105
  });
@@ -4983,13 +5108,13 @@ export class Session {
4983
5108
  if (msg.rows || msg.cols) {
4984
5109
  const state = session.getState();
4985
5110
  session.send({
4986
- type: "resize",
5111
+ type: 'resize',
4987
5112
  rows: msg.rows ?? state.rows,
4988
5113
  cols: msg.cols ?? state.cols,
4989
5114
  });
4990
5115
  }
4991
5116
  const existingStreamId = this.terminalStreamByTerminalId.get(msg.terminalId);
4992
- if (typeof existingStreamId === "number") {
5117
+ if (typeof existingStreamId === 'number') {
4993
5118
  this.detachTerminalStream(existingStreamId, { emitExit: false });
4994
5119
  }
4995
5120
  const streamId = this.allocateTerminalStreamId();
@@ -5031,7 +5156,7 @@ export class Session {
5031
5156
  }
5032
5157
  this.flushPendingTerminalStreamChunks(streamId, binding);
5033
5158
  this.emit({
5034
- type: "attach_terminal_stream_response",
5159
+ type: 'attach_terminal_stream_response',
5035
5160
  payload: {
5036
5161
  terminalId: msg.terminalId,
5037
5162
  streamId,
@@ -5051,7 +5176,7 @@ export class Session {
5051
5176
  return chunk.startOffset < binding.lastAckOffset + TERMINAL_STREAM_WINDOW_BYTES;
5052
5177
  }
5053
5178
  emitTerminalStreamChunk(streamId, binding, chunk) {
5054
- const payload = new Uint8Array(Buffer.from(chunk.data, "utf8"));
5179
+ const payload = new Uint8Array(Buffer.from(chunk.data, 'utf8'));
5055
5180
  this.emitBinary({
5056
5181
  channel: BinaryMuxChannel.Terminal,
5057
5182
  messageType: TerminalBinaryMessageType.OutputUtf8,
@@ -5072,7 +5197,7 @@ export class Session {
5072
5197
  pendingChunks: binding.pendingChunks.length,
5073
5198
  pendingBytes: binding.pendingBytes,
5074
5199
  chunkBytes,
5075
- }, "Terminal stream pending buffer overflow; closing stream");
5200
+ }, 'Terminal stream pending buffer overflow; closing stream');
5076
5201
  this.detachTerminalStream(streamId, { emitExit: true });
5077
5202
  return;
5078
5203
  }
@@ -5099,7 +5224,7 @@ export class Session {
5099
5224
  handleDetachTerminalStreamRequest(msg) {
5100
5225
  const success = this.detachTerminalStream(msg.streamId, { emitExit: false });
5101
5226
  this.emit({
5102
- type: "detach_terminal_stream_response",
5227
+ type: 'detach_terminal_stream_response',
5103
5228
  payload: {
5104
5229
  streamId: msg.streamId,
5105
5230
  success,
@@ -5121,7 +5246,7 @@ export class Session {
5121
5246
  binding.unsubscribe();
5122
5247
  }
5123
5248
  catch (error) {
5124
- this.sessionLogger.warn({ err: error, streamId }, "Failed to unsubscribe terminal stream");
5249
+ this.sessionLogger.warn({ err: error, streamId }, 'Failed to unsubscribe terminal stream');
5125
5250
  }
5126
5251
  this.terminalStreams.delete(streamId);
5127
5252
  if (this.terminalStreamByTerminalId.get(binding.terminalId) === streamId) {
@@ -5129,7 +5254,7 @@ export class Session {
5129
5254
  }
5130
5255
  if (options?.emitExit) {
5131
5256
  this.emit({
5132
- type: "terminal_stream_exit",
5257
+ type: 'terminal_stream_exit',
5133
5258
  payload: {
5134
5259
  streamId,
5135
5260
  terminalId: binding.terminalId,
@@ -5152,7 +5277,7 @@ export class Session {
5152
5277
  }
5153
5278
  attempts += 1;
5154
5279
  }
5155
- throw new Error("Unable to allocate terminal stream id");
5280
+ throw new Error('Unable to allocate terminal stream id');
5156
5281
  }
5157
5282
  }
5158
5283
  //# sourceMappingURL=session.js.map