@getpaseo/server 0.1.13 → 0.1.15

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 -77
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +252 -265
  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 +263 -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 +20 -20
  37. package/dist/server/server/session.d.ts.map +1 -1
  38. package/dist/server/server/session.js +871 -798
  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 +886 -410
  49. package/dist/server/shared/messages.d.ts.map +1 -1
  50. package/dist/server/shared/messages.js +272 -267
  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 +2 -2
@@ -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;
@@ -600,7 +598,7 @@ export class Session {
600
598
  return false;
601
599
  }
602
600
  }
603
- if (typeof filter?.requiresAttention === "boolean") {
601
+ if (typeof filter?.requiresAttention === 'boolean') {
604
602
  const requiresAttention = agent.requiresAttention ?? false;
605
603
  if (requiresAttention !== filter.requiresAttention) {
606
604
  return false;
@@ -615,7 +613,7 @@ export class Session {
615
613
  return true;
616
614
  }
617
615
  getAgentUpdateTargetId(update) {
618
- return update.kind === "remove" ? update.agentId : update.agent.id;
616
+ return update.kind === 'remove' ? update.agentId : update.agent.id;
619
617
  }
620
618
  bufferOrEmitAgentUpdate(subscription, payload) {
621
619
  if (subscription.isBootstrapping) {
@@ -623,7 +621,7 @@ export class Session {
623
621
  return;
624
622
  }
625
623
  this.emit({
626
- type: "agent_update",
624
+ type: 'agent_update',
627
625
  payload,
628
626
  });
629
627
  }
@@ -636,9 +634,9 @@ export class Session {
636
634
  const pending = Array.from(subscription.pendingUpdatesByAgentId.values());
637
635
  subscription.pendingUpdatesByAgentId.clear();
638
636
  for (const payload of pending) {
639
- if (payload.kind === "upsert") {
637
+ if (payload.kind === 'upsert') {
640
638
  const snapshotUpdatedAt = options?.snapshotUpdatedAtByAgentId?.get(payload.agent.id);
641
- if (typeof snapshotUpdatedAt === "number") {
639
+ if (typeof snapshotUpdatedAt === 'number') {
642
640
  const updateUpdatedAt = Date.parse(payload.agent.updatedAt);
643
641
  if (!Number.isNaN(updateUpdatedAt) && updateUpdatedAt <= snapshotUpdatedAt) {
644
642
  continue;
@@ -646,7 +644,7 @@ export class Session {
646
644
  }
647
645
  }
648
646
  this.emit({
649
- type: "agent_update",
647
+ type: 'agent_update',
650
648
  payload,
651
649
  });
652
650
  }
@@ -713,19 +711,19 @@ export class Session {
713
711
  });
714
712
  if (matches) {
715
713
  this.bufferOrEmitAgentUpdate(subscription, {
716
- kind: "upsert",
714
+ kind: 'upsert',
717
715
  agent: payload,
718
716
  project,
719
717
  });
720
718
  return;
721
719
  }
722
720
  this.bufferOrEmitAgentUpdate(subscription, {
723
- kind: "remove",
721
+ kind: 'remove',
724
722
  agentId: payload.id,
725
723
  });
726
724
  }
727
725
  catch (error) {
728
- this.sessionLogger.error({ err: error }, "Failed to emit agent update");
726
+ this.sessionLogger.error({ err: error }, 'Failed to emit agent update');
729
727
  }
730
728
  }
731
729
  /**
@@ -734,45 +732,45 @@ export class Session {
734
732
  async handleMessage(msg) {
735
733
  try {
736
734
  switch (msg.type) {
737
- case "voice_audio_chunk":
735
+ case 'voice_audio_chunk':
738
736
  await this.handleAudioChunk(msg);
739
737
  break;
740
- case "abort_request":
738
+ case 'abort_request':
741
739
  await this.handleAbort();
742
740
  break;
743
- case "audio_played":
741
+ case 'audio_played':
744
742
  this.handleAudioPlayed(msg.id);
745
743
  break;
746
- case "fetch_agents_request":
744
+ case 'fetch_agents_request':
747
745
  await this.handleFetchAgents(msg);
748
746
  break;
749
- case "fetch_agent_request":
747
+ case 'fetch_agent_request':
750
748
  await this.handleFetchAgent(msg.agentId, msg.requestId);
751
749
  break;
752
- case "delete_agent_request":
750
+ case 'delete_agent_request':
753
751
  await this.handleDeleteAgentRequest(msg.agentId, msg.requestId);
754
752
  break;
755
- case "archive_agent_request":
753
+ case 'archive_agent_request':
756
754
  await this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
757
755
  break;
758
- case "update_agent_request":
756
+ case 'update_agent_request':
759
757
  await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
760
758
  break;
761
- case "set_voice_mode":
759
+ case 'set_voice_mode':
762
760
  await this.handleSetVoiceMode(msg.enabled, msg.agentId, msg.requestId);
763
761
  break;
764
- case "send_agent_message_request":
762
+ case 'send_agent_message_request':
765
763
  await this.handleSendAgentMessageRequest(msg);
766
764
  break;
767
- case "wait_for_finish_request":
765
+ case 'wait_for_finish_request':
768
766
  await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
769
767
  break;
770
- case "dictation_stream_start":
768
+ case 'dictation_stream_start':
771
769
  {
772
- const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation");
770
+ const unavailable = this.resolveVoiceFeatureUnavailableContext('dictation');
773
771
  if (unavailable) {
774
772
  this.emit({
775
- type: "dictation_stream_error",
773
+ type: 'dictation_stream_error',
776
774
  payload: {
777
775
  dictationId: msg.dictationId,
778
776
  error: unavailable.message,
@@ -786,7 +784,7 @@ export class Session {
786
784
  }
787
785
  await this.dictationStreamManager.handleStart(msg.dictationId, msg.format);
788
786
  break;
789
- case "dictation_stream_chunk":
787
+ case 'dictation_stream_chunk':
790
788
  await this.dictationStreamManager.handleChunk({
791
789
  dictationId: msg.dictationId,
792
790
  seq: msg.seq,
@@ -794,115 +792,115 @@ export class Session {
794
792
  format: msg.format,
795
793
  });
796
794
  break;
797
- case "dictation_stream_finish":
795
+ case 'dictation_stream_finish':
798
796
  await this.dictationStreamManager.handleFinish(msg.dictationId, msg.finalSeq);
799
797
  break;
800
- case "dictation_stream_cancel":
798
+ case 'dictation_stream_cancel':
801
799
  this.dictationStreamManager.handleCancel(msg.dictationId);
802
800
  break;
803
- case "create_agent_request":
801
+ case 'create_agent_request':
804
802
  await this.handleCreateAgentRequest(msg);
805
803
  break;
806
- case "resume_agent_request":
804
+ case 'resume_agent_request':
807
805
  await this.handleResumeAgentRequest(msg);
808
806
  break;
809
- case "refresh_agent_request":
807
+ case 'refresh_agent_request':
810
808
  await this.handleRefreshAgentRequest(msg);
811
809
  break;
812
- case "cancel_agent_request":
810
+ case 'cancel_agent_request':
813
811
  await this.handleCancelAgentRequest(msg.agentId);
814
812
  break;
815
- case "restart_server_request":
813
+ case 'restart_server_request':
816
814
  await this.handleRestartServerRequest(msg.requestId, msg.reason);
817
815
  break;
818
- case "fetch_agent_timeline_request":
816
+ case 'fetch_agent_timeline_request':
819
817
  await this.handleFetchAgentTimelineRequest(msg);
820
818
  break;
821
- case "set_agent_mode_request":
819
+ case 'set_agent_mode_request':
822
820
  await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId);
823
821
  break;
824
- case "set_agent_model_request":
822
+ case 'set_agent_model_request':
825
823
  await this.handleSetAgentModelRequest(msg.agentId, msg.modelId, msg.requestId);
826
824
  break;
827
- case "set_agent_thinking_request":
825
+ case 'set_agent_thinking_request':
828
826
  await this.handleSetAgentThinkingRequest(msg.agentId, msg.thinkingOptionId, msg.requestId);
829
827
  break;
830
- case "agent_permission_response":
828
+ case 'agent_permission_response':
831
829
  await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response);
832
830
  break;
833
- case "checkout_status_request":
831
+ case 'checkout_status_request':
834
832
  await this.handleCheckoutStatusRequest(msg);
835
833
  break;
836
- case "validate_branch_request":
834
+ case 'validate_branch_request':
837
835
  await this.handleValidateBranchRequest(msg);
838
836
  break;
839
- case "branch_suggestions_request":
837
+ case 'branch_suggestions_request':
840
838
  await this.handleBranchSuggestionsRequest(msg);
841
839
  break;
842
- case "directory_suggestions_request":
840
+ case 'directory_suggestions_request':
843
841
  await this.handleDirectorySuggestionsRequest(msg);
844
842
  break;
845
- case "subscribe_checkout_diff_request":
843
+ case 'subscribe_checkout_diff_request':
846
844
  await this.handleSubscribeCheckoutDiffRequest(msg);
847
845
  break;
848
- case "unsubscribe_checkout_diff_request":
846
+ case 'unsubscribe_checkout_diff_request':
849
847
  this.handleUnsubscribeCheckoutDiffRequest(msg);
850
848
  break;
851
- case "checkout_commit_request":
849
+ case 'checkout_commit_request':
852
850
  await this.handleCheckoutCommitRequest(msg);
853
851
  break;
854
- case "checkout_merge_request":
852
+ case 'checkout_merge_request':
855
853
  await this.handleCheckoutMergeRequest(msg);
856
854
  break;
857
- case "checkout_merge_from_base_request":
855
+ case 'checkout_merge_from_base_request':
858
856
  await this.handleCheckoutMergeFromBaseRequest(msg);
859
857
  break;
860
- case "checkout_push_request":
858
+ case 'checkout_push_request':
861
859
  await this.handleCheckoutPushRequest(msg);
862
860
  break;
863
- case "checkout_pr_create_request":
861
+ case 'checkout_pr_create_request':
864
862
  await this.handleCheckoutPrCreateRequest(msg);
865
863
  break;
866
- case "checkout_pr_status_request":
864
+ case 'checkout_pr_status_request':
867
865
  await this.handleCheckoutPrStatusRequest(msg);
868
866
  break;
869
- case "paseo_worktree_list_request":
867
+ case 'paseo_worktree_list_request':
870
868
  await this.handlePaseoWorktreeListRequest(msg);
871
869
  break;
872
- case "paseo_worktree_archive_request":
870
+ case 'paseo_worktree_archive_request':
873
871
  await this.handlePaseoWorktreeArchiveRequest(msg);
874
872
  break;
875
- case "file_explorer_request":
873
+ case 'file_explorer_request':
876
874
  await this.handleFileExplorerRequest(msg);
877
875
  break;
878
- case "project_icon_request":
876
+ case 'project_icon_request':
879
877
  await this.handleProjectIconRequest(msg);
880
878
  break;
881
- case "file_download_token_request":
879
+ case 'file_download_token_request':
882
880
  await this.handleFileDownloadTokenRequest(msg);
883
881
  break;
884
- case "list_provider_models_request":
882
+ case 'list_provider_models_request':
885
883
  await this.handleListProviderModelsRequest(msg);
886
884
  break;
887
- case "list_available_providers_request":
885
+ case 'list_available_providers_request':
888
886
  await this.handleListAvailableProvidersRequest(msg);
889
887
  break;
890
- case "speech_models_list_request":
888
+ case 'speech_models_list_request':
891
889
  await this.handleSpeechModelsListRequest(msg);
892
890
  break;
893
- case "speech_models_download_request":
891
+ case 'speech_models_download_request':
894
892
  await this.handleSpeechModelsDownloadRequest(msg);
895
893
  break;
896
- case "clear_agent_attention":
894
+ case 'clear_agent_attention':
897
895
  await this.handleClearAgentAttention(msg.agentId);
898
896
  break;
899
- case "client_heartbeat":
897
+ case 'client_heartbeat':
900
898
  this.handleClientHeartbeat(msg);
901
899
  break;
902
- case "ping": {
900
+ case 'ping': {
903
901
  const now = Date.now();
904
902
  this.emit({
905
- type: "pong",
903
+ type: 'pong',
906
904
  payload: {
907
905
  requestId: msg.requestId,
908
906
  clientSentAt: msg.clientSentAt,
@@ -912,73 +910,70 @@ export class Session {
912
910
  });
913
911
  break;
914
912
  }
915
- case "list_commands_request":
916
- await this.handleListCommandsRequest(msg.agentId, msg.requestId);
917
- break;
918
- case "execute_command_request":
919
- await this.handleExecuteCommandRequest(msg.agentId, msg.commandName, msg.args, msg.requestId);
913
+ case 'list_commands_request':
914
+ await this.handleListCommandsRequest(msg);
920
915
  break;
921
- case "register_push_token":
916
+ case 'register_push_token':
922
917
  this.handleRegisterPushToken(msg.token);
923
918
  break;
924
- case "subscribe_terminals_request":
919
+ case 'subscribe_terminals_request':
925
920
  this.handleSubscribeTerminalsRequest(msg);
926
921
  break;
927
- case "unsubscribe_terminals_request":
922
+ case 'unsubscribe_terminals_request':
928
923
  this.handleUnsubscribeTerminalsRequest(msg);
929
924
  break;
930
- case "list_terminals_request":
925
+ case 'list_terminals_request':
931
926
  await this.handleListTerminalsRequest(msg);
932
927
  break;
933
- case "create_terminal_request":
928
+ case 'create_terminal_request':
934
929
  await this.handleCreateTerminalRequest(msg);
935
930
  break;
936
- case "subscribe_terminal_request":
931
+ case 'subscribe_terminal_request':
937
932
  await this.handleSubscribeTerminalRequest(msg);
938
933
  break;
939
- case "unsubscribe_terminal_request":
934
+ case 'unsubscribe_terminal_request':
940
935
  this.handleUnsubscribeTerminalRequest(msg);
941
936
  break;
942
- case "terminal_input":
937
+ case 'terminal_input':
943
938
  this.handleTerminalInput(msg);
944
939
  break;
945
- case "kill_terminal_request":
940
+ case 'kill_terminal_request':
946
941
  await this.handleKillTerminalRequest(msg);
947
942
  break;
948
- case "attach_terminal_stream_request":
943
+ case 'attach_terminal_stream_request':
949
944
  await this.handleAttachTerminalStreamRequest(msg);
950
945
  break;
951
- case "detach_terminal_stream_request":
946
+ case 'detach_terminal_stream_request':
952
947
  this.handleDetachTerminalStreamRequest(msg);
953
948
  break;
954
949
  }
955
950
  }
956
951
  catch (error) {
957
952
  const err = error instanceof Error ? error : new Error(String(error));
958
- this.sessionLogger.error({ err }, "Error handling message");
953
+ this.sessionLogger.error({ err }, 'Error handling message');
959
954
  const requestId = msg.requestId;
960
- if (typeof requestId === "string") {
955
+ if (typeof requestId === 'string') {
961
956
  try {
962
957
  this.emit({
963
- type: "rpc_error",
958
+ type: 'rpc_error',
964
959
  payload: {
965
960
  requestId,
966
961
  requestType: msg.type,
967
- error: "Request failed",
968
- code: "handler_error",
962
+ error: 'Request failed',
963
+ code: 'handler_error',
969
964
  },
970
965
  });
971
966
  }
972
967
  catch (emitError) {
973
- this.sessionLogger.error({ err: emitError }, "Failed to emit rpc_error");
968
+ this.sessionLogger.error({ err: emitError }, 'Failed to emit rpc_error');
974
969
  }
975
970
  }
976
971
  this.emit({
977
- type: "activity_log",
972
+ type: 'activity_log',
978
973
  payload: {
979
974
  id: uuidv4(),
980
975
  timestamp: new Date(),
981
- type: "error",
976
+ type: 'error',
982
977
  content: `Error: ${err.message}`,
983
978
  },
984
979
  });
@@ -990,7 +985,7 @@ export class Session {
990
985
  this.handleTerminalBinaryFrame(frame);
991
986
  break;
992
987
  default:
993
- 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');
994
989
  break;
995
990
  }
996
991
  }
@@ -998,7 +993,7 @@ export class Session {
998
993
  if (frame.messageType === TerminalBinaryMessageType.InputUtf8) {
999
994
  const binding = this.terminalStreams.get(frame.streamId);
1000
995
  if (!binding) {
1001
- 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');
1002
997
  return;
1003
998
  }
1004
999
  if (!this.terminalManager) {
@@ -1013,11 +1008,11 @@ export class Session {
1013
1008
  if (payload.byteLength === 0) {
1014
1009
  return;
1015
1010
  }
1016
- const text = Buffer.from(payload).toString("utf8");
1011
+ const text = Buffer.from(payload).toString('utf8');
1017
1012
  if (!text) {
1018
1013
  return;
1019
1014
  }
1020
- session.send({ type: "input", data: text });
1015
+ session.send({ type: 'input', data: text });
1021
1016
  return;
1022
1017
  }
1023
1018
  if (frame.messageType === TerminalBinaryMessageType.Ack) {
@@ -1034,30 +1029,30 @@ export class Session {
1034
1029
  }
1035
1030
  return;
1036
1031
  }
1037
- 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');
1038
1033
  }
1039
1034
  async handleRestartServerRequest(requestId, reason) {
1040
1035
  if (restartRequested) {
1041
- this.sessionLogger.debug("Restart already requested, ignoring duplicate");
1036
+ this.sessionLogger.debug('Restart already requested, ignoring duplicate');
1042
1037
  return;
1043
1038
  }
1044
1039
  restartRequested = true;
1045
1040
  const payload = {
1046
- status: "restart_requested",
1041
+ status: 'restart_requested',
1047
1042
  clientId: this.clientId,
1048
1043
  };
1049
1044
  if (reason && reason.trim().length > 0) {
1050
1045
  payload.reason = reason;
1051
1046
  }
1052
1047
  payload.requestId = requestId;
1053
- this.sessionLogger.warn({ reason }, "Restart requested via websocket");
1048
+ this.sessionLogger.warn({ reason }, 'Restart requested via websocket');
1054
1049
  this.emit({
1055
- type: "status",
1050
+ type: 'status',
1056
1051
  payload,
1057
1052
  });
1058
- if (typeof process.send === "function") {
1053
+ if (typeof process.send === 'function') {
1059
1054
  process.send({
1060
- type: "paseo:restart",
1055
+ type: 'paseo:restart',
1061
1056
  ...(reason ? { reason } : {}),
1062
1057
  });
1063
1058
  return;
@@ -1083,7 +1078,7 @@ export class Session {
1083
1078
  this.sessionLogger.error({ err: error, agentId }, `Failed to remove agent ${agentId} from registry`);
1084
1079
  }
1085
1080
  this.emit({
1086
- type: "agent_deleted",
1081
+ type: 'agent_deleted',
1087
1082
  payload: {
1088
1083
  agentId,
1089
1084
  requestId,
@@ -1091,7 +1086,7 @@ export class Session {
1091
1086
  });
1092
1087
  if (this.agentUpdatesSubscription) {
1093
1088
  this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
1094
- kind: "remove",
1089
+ kind: 'remove',
1095
1090
  agentId,
1096
1091
  });
1097
1092
  }
@@ -1099,40 +1094,59 @@ export class Session {
1099
1094
  async handleArchiveAgentRequest(agentId, requestId) {
1100
1095
  this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
1101
1096
  const archivedAt = new Date().toISOString();
1102
- try {
1103
- const existing = await this.agentStorage.get(agentId);
1104
- if (existing) {
1105
- await this.agentStorage.upsert({
1106
- ...existing,
1107
- archivedAt,
1108
- });
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}`);
1109
1111
  }
1110
- this.agentManager.notifyAgentState(agentId);
1111
- }
1112
- catch (error) {
1113
- this.sessionLogger.error({ err: error, agentId }, `Failed to archive agent ${agentId}`);
1114
1112
  }
1113
+ archivedRecord = {
1114
+ ...archivedRecord,
1115
+ archivedAt,
1116
+ };
1117
+ await this.agentStorage.upsert(archivedRecord);
1118
+ this.agentManager.notifyAgentState(agentId);
1115
1119
  this.emit({
1116
- type: "agent_archived",
1120
+ type: 'agent_archived',
1117
1121
  payload: {
1118
1122
  agentId,
1119
1123
  archivedAt,
1120
1124
  requestId,
1121
1125
  },
1122
1126
  });
1127
+ await this.maybeArchiveWorktreeAfterLastAgentArchived({
1128
+ archivedAgentId: agentId,
1129
+ archivedAgentCwd: archivedRecord.cwd,
1130
+ requestId,
1131
+ });
1123
1132
  }
1124
1133
  async handleUpdateAgentRequest(agentId, name, labels, requestId) {
1125
- 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');
1126
1140
  const normalizedName = name?.trim();
1127
1141
  const normalizedLabels = labels && Object.keys(labels).length > 0 ? labels : undefined;
1128
1142
  if (!normalizedName && !normalizedLabels) {
1129
1143
  this.emit({
1130
- type: "update_agent_response",
1144
+ type: 'update_agent_response',
1131
1145
  payload: {
1132
1146
  requestId,
1133
1147
  agentId,
1134
1148
  accepted: false,
1135
- error: "Nothing to update (provide name and/or labels)",
1149
+ error: 'Nothing to update (provide name and/or labels)',
1136
1150
  },
1137
1151
  });
1138
1152
  return;
@@ -1155,34 +1169,32 @@ export class Session {
1155
1169
  await this.agentStorage.upsert({
1156
1170
  ...existing,
1157
1171
  ...(normalizedName ? { title: normalizedName } : {}),
1158
- ...(normalizedLabels
1159
- ? { labels: { ...existing.labels, ...normalizedLabels } }
1160
- : {}),
1172
+ ...(normalizedLabels ? { labels: { ...existing.labels, ...normalizedLabels } } : {}),
1161
1173
  });
1162
1174
  }
1163
1175
  this.emit({
1164
- type: "update_agent_response",
1176
+ type: 'update_agent_response',
1165
1177
  payload: { requestId, agentId, accepted: true, error: null },
1166
1178
  });
1167
1179
  }
1168
1180
  catch (error) {
1169
- 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');
1170
1182
  this.emit({
1171
- type: "activity_log",
1183
+ type: 'activity_log',
1172
1184
  payload: {
1173
1185
  id: uuidv4(),
1174
1186
  timestamp: new Date(),
1175
- type: "error",
1187
+ type: 'error',
1176
1188
  content: `Failed to update agent: ${error.message}`,
1177
1189
  },
1178
1190
  });
1179
1191
  this.emit({
1180
- type: "update_agent_response",
1192
+ type: 'update_agent_response',
1181
1193
  payload: {
1182
1194
  requestId,
1183
1195
  agentId,
1184
1196
  accepted: false,
1185
- error: error?.message ? String(error.message) : "Failed to update agent",
1197
+ error: error?.message ? String(error.message) : 'Failed to update agent',
1186
1198
  },
1187
1199
  });
1188
1200
  }
@@ -1196,7 +1208,7 @@ export class Session {
1196
1208
  };
1197
1209
  }
1198
1210
  resolveModeReadinessState(readiness, mode) {
1199
- if (mode === "voice_mode") {
1211
+ if (mode === 'voice_mode') {
1200
1212
  return readiness.realtimeVoice;
1201
1213
  }
1202
1214
  return readiness.dictation;
@@ -1234,11 +1246,11 @@ export class Session {
1234
1246
  async handleSetVoiceMode(enabled, agentId, requestId) {
1235
1247
  try {
1236
1248
  if (enabled) {
1237
- const unavailable = this.resolveVoiceFeatureUnavailableContext("voice_mode");
1249
+ const unavailable = this.resolveVoiceFeatureUnavailableContext('voice_mode');
1238
1250
  if (unavailable) {
1239
1251
  throw new VoiceFeatureUnavailableError(unavailable);
1240
1252
  }
1241
- const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? "", "set_voice_mode");
1253
+ const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? '', 'set_voice_mode');
1242
1254
  if (this.isVoiceMode &&
1243
1255
  this.voiceModeAgentId &&
1244
1256
  this.voiceModeAgentId !== normalizedAgentId) {
@@ -1251,10 +1263,10 @@ export class Session {
1251
1263
  this.isVoiceMode = true;
1252
1264
  this.sessionLogger.info({
1253
1265
  agentId: this.voiceModeAgentId,
1254
- }, "Voice mode enabled for existing agent");
1266
+ }, 'Voice mode enabled for existing agent');
1255
1267
  if (requestId) {
1256
1268
  this.emit({
1257
- type: "set_voice_mode_response",
1269
+ type: 'set_voice_mode_response',
1258
1270
  payload: {
1259
1271
  requestId,
1260
1272
  enabled: true,
@@ -1268,10 +1280,10 @@ export class Session {
1268
1280
  }
1269
1281
  await this.disableVoiceModeForActiveAgent(true);
1270
1282
  this.isVoiceMode = false;
1271
- this.sessionLogger.info("Voice mode disabled");
1283
+ this.sessionLogger.info('Voice mode disabled');
1272
1284
  if (requestId) {
1273
1285
  this.emit({
1274
- type: "set_voice_mode_response",
1286
+ type: 'set_voice_mode_response',
1275
1287
  payload: {
1276
1288
  requestId,
1277
1289
  enabled: false,
@@ -1283,16 +1295,16 @@ export class Session {
1283
1295
  }
1284
1296
  }
1285
1297
  catch (error) {
1286
- 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';
1287
1299
  const unavailable = this.getVoiceFeatureUnavailableResponseMetadata(error);
1288
1300
  this.sessionLogger.error({
1289
1301
  err: error,
1290
1302
  enabled,
1291
1303
  requestedAgentId: agentId ?? null,
1292
- }, "set_voice_mode failed");
1304
+ }, 'set_voice_mode failed');
1293
1305
  if (requestId) {
1294
1306
  this.emit({
1295
- type: "set_voice_mode_response",
1307
+ type: 'set_voice_mode_response',
1296
1308
  payload: {
1297
1309
  requestId,
1298
1310
  enabled: this.isVoiceMode,
@@ -1323,7 +1335,7 @@ export class Session {
1323
1335
  buildVoiceModeMcpServers(existing, socketPath) {
1324
1336
  const mcpStdio = this.voiceAgentMcpStdio;
1325
1337
  if (!mcpStdio) {
1326
- throw new Error("Voice MCP stdio bridge is not configured");
1338
+ throw new Error('Voice MCP stdio bridge is not configured');
1327
1339
  }
1328
1340
  return {
1329
1341
  ...(existing ?? {}),
@@ -1338,7 +1350,7 @@ export class Session {
1338
1350
  async enableVoiceModeForAgent(agentId) {
1339
1351
  const ensureVoiceSocket = this.ensureVoiceMcpSocketForAgent;
1340
1352
  if (!ensureVoiceSocket) {
1341
- throw new Error("Voice MCP socket bridge is not configured");
1353
+ throw new Error('Voice MCP socket bridge is not configured');
1342
1354
  }
1343
1355
  const existing = await this.ensureAgentLoaded(agentId);
1344
1356
  const socketPath = await ensureVoiceSocket(agentId);
@@ -1366,7 +1378,7 @@ export class Session {
1366
1378
  }
1367
1379
  async disableVoiceModeForActiveAgent(restoreAgentConfig) {
1368
1380
  this.clearVoiceModeInactivityTimeout();
1369
- this.cancelActiveVoiceDictationStream("voice mode disabled");
1381
+ this.cancelActiveVoiceDictationStream('voice mode disabled');
1370
1382
  const agentId = this.voiceModeAgentId;
1371
1383
  if (!agentId) {
1372
1384
  this.voiceModeBaseConfig = null;
@@ -1375,7 +1387,7 @@ export class Session {
1375
1387
  this.unregisterVoiceSpeakHandler?.(agentId);
1376
1388
  this.unregisterVoiceCallerContext?.(agentId);
1377
1389
  await this.removeVoiceMcpSocketForAgent?.(agentId).catch((error) => {
1378
- 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');
1379
1391
  });
1380
1392
  if (restoreAgentConfig && this.voiceModeBaseConfig) {
1381
1393
  const baseConfig = this.voiceModeBaseConfig;
@@ -1386,7 +1398,7 @@ export class Session {
1386
1398
  });
1387
1399
  }
1388
1400
  catch (error) {
1389
- 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');
1390
1402
  }
1391
1403
  }
1392
1404
  this.voiceModeBaseConfig = null;
@@ -1396,9 +1408,9 @@ export class Session {
1396
1408
  return dictationId.startsWith(VOICE_INTERNAL_DICTATION_ID_PREFIX);
1397
1409
  }
1398
1410
  handleDictationManagerMessage(msg) {
1399
- if (msg.type === "activity_log") {
1411
+ if (msg.type === 'activity_log') {
1400
1412
  const metadata = msg.payload.metadata;
1401
- const dictationId = metadata && typeof metadata.dictationId === "string" ? metadata.dictationId : null;
1413
+ const dictationId = metadata && typeof metadata.dictationId === 'string' ? metadata.dictationId : null;
1402
1414
  if (dictationId && this.isInternalVoiceDictationId(dictationId)) {
1403
1415
  return;
1404
1416
  }
@@ -1406,14 +1418,14 @@ export class Session {
1406
1418
  return;
1407
1419
  }
1408
1420
  const payloadWithDictationId = msg.payload;
1409
- const dictationId = payloadWithDictationId && typeof payloadWithDictationId.dictationId === "string"
1421
+ const dictationId = payloadWithDictationId && typeof payloadWithDictationId.dictationId === 'string'
1410
1422
  ? payloadWithDictationId.dictationId
1411
1423
  : null;
1412
1424
  if (!dictationId || !this.isInternalVoiceDictationId(dictationId)) {
1413
1425
  this.emit(msg);
1414
1426
  return;
1415
1427
  }
1416
- if (msg.type === "dictation_stream_final") {
1428
+ if (msg.type === 'dictation_stream_final') {
1417
1429
  if (dictationId !== this.activeVoiceDictationId || !this.activeVoiceDictationResolve) {
1418
1430
  return;
1419
1431
  }
@@ -1425,7 +1437,7 @@ export class Session {
1425
1437
  });
1426
1438
  return;
1427
1439
  }
1428
- if (msg.type === "dictation_stream_error") {
1440
+ if (msg.type === 'dictation_stream_error') {
1429
1441
  if (dictationId !== this.activeVoiceDictationId || !this.activeVoiceDictationReject) {
1430
1442
  return;
1431
1443
  }
@@ -1449,7 +1461,7 @@ export class Session {
1449
1461
  if (!dictationId) {
1450
1462
  return;
1451
1463
  }
1452
- this.sessionLogger.debug({ dictationId, reason }, "Cancelling active internal voice dictation stream");
1464
+ this.sessionLogger.debug({ dictationId, reason }, 'Cancelling active internal voice dictation stream');
1453
1465
  if (this.activeVoiceDictationReject) {
1454
1466
  this.activeVoiceDictationReject(new Error(`Voice dictation cancelled: ${reason}`));
1455
1467
  }
@@ -1464,7 +1476,7 @@ export class Session {
1464
1476
  return;
1465
1477
  }
1466
1478
  if (this.activeVoiceDictationId) {
1467
- await this.finalizeActiveVoiceDictationStream("voice format changed");
1479
+ await this.finalizeActiveVoiceDictationStream('voice format changed');
1468
1480
  }
1469
1481
  const dictationId = `${VOICE_INTERNAL_DICTATION_ID_PREFIX}${uuidv4()}`;
1470
1482
  let resolve = null;
@@ -1482,14 +1494,14 @@ export class Session {
1482
1494
  this.activeVoiceDictationResultPromise = resultPromise;
1483
1495
  this.activeVoiceDictationResolve = resolve;
1484
1496
  this.activeVoiceDictationReject = reject;
1485
- this.setPhase("transcribing");
1497
+ this.setPhase('transcribing');
1486
1498
  this.emit({
1487
- type: "activity_log",
1499
+ type: 'activity_log',
1488
1500
  payload: {
1489
1501
  id: uuidv4(),
1490
1502
  timestamp: new Date(),
1491
- type: "system",
1492
- content: "Transcribing audio...",
1503
+ type: 'system',
1504
+ content: 'Transcribing audio...',
1493
1505
  },
1494
1506
  });
1495
1507
  const startPromise = this.voiceStreamManager.handleStart(dictationId, format);
@@ -1514,7 +1526,7 @@ export class Session {
1514
1526
  await this.ensureActiveVoiceDictationStream(format);
1515
1527
  const dictationId = this.activeVoiceDictationId;
1516
1528
  if (!dictationId) {
1517
- throw new Error("Voice dictation stream did not initialize");
1529
+ throw new Error('Voice dictation stream did not initialize');
1518
1530
  }
1519
1531
  const seq = this.activeVoiceDictationNextSeq;
1520
1532
  this.activeVoiceDictationNextSeq += 1;
@@ -1545,7 +1557,7 @@ export class Session {
1545
1557
  return;
1546
1558
  }
1547
1559
  this.activeVoiceDictationFinalizePromise = (async () => {
1548
- this.sessionLogger.debug({ dictationId, finalSeq, reason }, "Finalizing internal voice dictation stream");
1560
+ this.sessionLogger.debug({ dictationId, finalSeq, reason }, 'Finalizing internal voice dictation stream');
1549
1561
  await this.voiceStreamManager.handleFinish(dictationId, finalSeq);
1550
1562
  const result = await resultPromise;
1551
1563
  this.resetActiveVoiceDictationState();
@@ -1556,12 +1568,12 @@ export class Session {
1556
1568
  isVoiceMode: this.isVoiceMode,
1557
1569
  transcriptLength: transcriptText.length,
1558
1570
  transcript: transcriptText,
1559
- }, "Transcription result");
1571
+ }, 'Transcription result');
1560
1572
  await this.handleTranscriptionResultPayload({
1561
1573
  text: result.text,
1562
1574
  requestId,
1563
1575
  ...(result.debugRecordingPath
1564
- ? { debugRecordingPath: result.debugRecordingPath, format: "audio/wav" }
1576
+ ? { debugRecordingPath: result.debugRecordingPath, format: 'audio/wav' }
1565
1577
  : {}),
1566
1578
  });
1567
1579
  })();
@@ -1570,14 +1582,14 @@ export class Session {
1570
1582
  }
1571
1583
  catch (error) {
1572
1584
  this.resetActiveVoiceDictationState();
1573
- this.setPhase("idle");
1574
- this.clearSpeechInProgress("transcription error");
1585
+ this.setPhase('idle');
1586
+ this.clearSpeechInProgress('transcription error');
1575
1587
  this.emit({
1576
- type: "activity_log",
1588
+ type: 'activity_log',
1577
1589
  payload: {
1578
1590
  id: uuidv4(),
1579
1591
  timestamp: new Date(),
1580
- type: "error",
1592
+ type: 'error',
1581
1593
  content: `Transcription error: ${error instanceof Error ? error.message : String(error)}`,
1582
1594
  },
1583
1595
  });
@@ -1593,14 +1605,14 @@ export class Session {
1593
1605
  await this.ensureAgentLoaded(agentId);
1594
1606
  }
1595
1607
  catch (error) {
1596
- this.handleAgentRunError(agentId, error, "Failed to initialize agent before sending prompt");
1608
+ this.handleAgentRunError(agentId, error, 'Failed to initialize agent before sending prompt');
1597
1609
  return;
1598
1610
  }
1599
1611
  try {
1600
1612
  await this.interruptAgentIfRunning(agentId);
1601
1613
  }
1602
1614
  catch (error) {
1603
- 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');
1604
1616
  return;
1605
1617
  }
1606
1618
  const prompt = this.buildAgentPrompt(text, images);
@@ -1620,7 +1632,7 @@ export class Session {
1620
1632
  */
1621
1633
  async handleCreateAgentRequest(msg) {
1622
1634
  const { config, worktreeName, requestId, initialPrompt, outputSchema, git, images, labels } = msg;
1623
- 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}` : ''}`);
1624
1636
  try {
1625
1637
  const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
1626
1638
  const snapshot = await this.agentManager.createAgent(sessionConfig, undefined, { labels });
@@ -1642,11 +1654,11 @@ export class Session {
1642
1654
  catch (promptError) {
1643
1655
  this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
1644
1656
  this.emit({
1645
- type: "activity_log",
1657
+ type: 'activity_log',
1646
1658
  payload: {
1647
1659
  id: uuidv4(),
1648
1660
  timestamp: new Date(),
1649
- type: "error",
1661
+ type: 'error',
1650
1662
  content: `Initial prompt failed: ${promptError?.message ?? promptError}`,
1651
1663
  },
1652
1664
  });
@@ -1658,9 +1670,9 @@ export class Session {
1658
1670
  throw new Error(`Agent ${snapshot.id} not found after creation`);
1659
1671
  }
1660
1672
  this.emit({
1661
- type: "status",
1673
+ type: 'status',
1662
1674
  payload: {
1663
- status: "agent_created",
1675
+ status: 'agent_created',
1664
1676
  agentId: snapshot.id,
1665
1677
  requestId,
1666
1678
  agent: agentPayload,
@@ -1688,23 +1700,23 @@ export class Session {
1688
1700
  this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1689
1701
  }
1690
1702
  catch (error) {
1691
- this.sessionLogger.error({ err: error }, "Failed to create agent");
1703
+ this.sessionLogger.error({ err: error }, 'Failed to create agent');
1692
1704
  if (requestId) {
1693
1705
  this.emit({
1694
- type: "status",
1706
+ type: 'status',
1695
1707
  payload: {
1696
- status: "agent_create_failed",
1708
+ status: 'agent_create_failed',
1697
1709
  requestId,
1698
1710
  error: error?.message ?? String(error),
1699
1711
  },
1700
1712
  });
1701
1713
  }
1702
1714
  this.emit({
1703
- type: "activity_log",
1715
+ type: 'activity_log',
1704
1716
  payload: {
1705
1717
  id: uuidv4(),
1706
1718
  timestamp: new Date(),
1707
- type: "error",
1719
+ type: 'error',
1708
1720
  content: `Failed to create agent: ${error.message}`,
1709
1721
  },
1710
1722
  });
@@ -1713,14 +1725,14 @@ export class Session {
1713
1725
  async handleResumeAgentRequest(msg) {
1714
1726
  const { handle, overrides, requestId } = msg;
1715
1727
  if (!handle) {
1716
- this.sessionLogger.warn("Resume request missing persistence handle");
1728
+ this.sessionLogger.warn('Resume request missing persistence handle');
1717
1729
  this.emit({
1718
- type: "activity_log",
1730
+ type: 'activity_log',
1719
1731
  payload: {
1720
1732
  id: uuidv4(),
1721
1733
  timestamp: new Date(),
1722
- type: "error",
1723
- content: "Unable to resume agent: missing persistence handle",
1734
+ type: 'error',
1735
+ content: 'Unable to resume agent: missing persistence handle',
1724
1736
  },
1725
1737
  });
1726
1738
  return;
@@ -1737,9 +1749,9 @@ export class Session {
1737
1749
  throw new Error(`Agent ${snapshot.id} not found after resume`);
1738
1750
  }
1739
1751
  this.emit({
1740
- type: "status",
1752
+ type: 'status',
1741
1753
  payload: {
1742
- status: "agent_resumed",
1754
+ status: 'agent_resumed',
1743
1755
  agentId: snapshot.id,
1744
1756
  requestId,
1745
1757
  timelineSize,
@@ -1749,13 +1761,13 @@ export class Session {
1749
1761
  }
1750
1762
  }
1751
1763
  catch (error) {
1752
- this.sessionLogger.error({ err: error }, "Failed to resume agent");
1764
+ this.sessionLogger.error({ err: error }, 'Failed to resume agent');
1753
1765
  this.emit({
1754
- type: "activity_log",
1766
+ type: 'activity_log',
1755
1767
  payload: {
1756
1768
  id: uuidv4(),
1757
1769
  timestamp: new Date(),
1758
- type: "error",
1770
+ type: 'error',
1759
1771
  content: `Failed to resume agent: ${error.message}`,
1760
1772
  },
1761
1773
  });
@@ -1792,9 +1804,9 @@ export class Session {
1792
1804
  const timelineSize = this.agentManager.getTimeline(agentId).length;
1793
1805
  if (requestId) {
1794
1806
  this.emit({
1795
- type: "status",
1807
+ type: 'status',
1796
1808
  payload: {
1797
- status: "agent_refreshed",
1809
+ status: 'agent_refreshed',
1798
1810
  agentId,
1799
1811
  requestId,
1800
1812
  timelineSize,
@@ -1805,11 +1817,11 @@ export class Session {
1805
1817
  catch (error) {
1806
1818
  this.sessionLogger.error({ err: error, agentId }, `Failed to refresh agent ${agentId}`);
1807
1819
  this.emit({
1808
- type: "activity_log",
1820
+ type: 'activity_log',
1809
1821
  payload: {
1810
1822
  id: uuidv4(),
1811
1823
  timestamp: new Date(),
1812
- type: "error",
1824
+ type: 'error',
1813
1825
  content: `Failed to refresh agent: ${error.message}`,
1814
1826
  },
1815
1827
  });
@@ -1821,7 +1833,7 @@ export class Session {
1821
1833
  await this.interruptAgentIfRunning(agentId);
1822
1834
  }
1823
1835
  catch (error) {
1824
- this.handleAgentRunError(agentId, error, "Failed to cancel running agent on request");
1836
+ this.handleAgentRunError(agentId, error, 'Failed to cancel running agent on request');
1825
1837
  }
1826
1838
  }
1827
1839
  async buildAgentSessionConfig(config, gitOptions, legacyWorktreeName, _labels) {
@@ -1843,14 +1855,14 @@ export class Session {
1843
1855
  }
1844
1856
  else {
1845
1857
  // Resolve current branch name from HEAD
1846
- const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
1858
+ const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
1847
1859
  cwd,
1848
1860
  env: READ_ONLY_GIT_ENV,
1849
1861
  });
1850
1862
  targetBranch = stdout.trim();
1851
1863
  }
1852
1864
  if (!targetBranch) {
1853
- 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.');
1854
1866
  }
1855
1867
  this.sessionLogger.info({ worktreeSlug: normalized.worktreeSlug ?? targetBranch, branch: targetBranch }, `Creating worktree '${normalized.worktreeSlug ?? targetBranch}' for branch ${targetBranch}`);
1856
1868
  const createdWorktree = await createAgentWorktree({
@@ -1888,7 +1900,7 @@ export class Session {
1888
1900
  cwd: msg.cwd ? expandTilde(msg.cwd) : undefined,
1889
1901
  });
1890
1902
  this.emit({
1891
- type: "list_provider_models_response",
1903
+ type: 'list_provider_models_response',
1892
1904
  payload: {
1893
1905
  provider: msg.provider,
1894
1906
  models,
@@ -1901,7 +1913,7 @@ export class Session {
1901
1913
  catch (error) {
1902
1914
  this.sessionLogger.error({ err: error, provider: msg.provider }, `Failed to list models for ${msg.provider}`);
1903
1915
  this.emit({
1904
- type: "list_provider_models_response",
1916
+ type: 'list_provider_models_response',
1905
1917
  payload: {
1906
1918
  provider: msg.provider,
1907
1919
  error: error?.message ?? String(error),
@@ -1916,7 +1928,7 @@ export class Session {
1916
1928
  try {
1917
1929
  const providers = await this.agentManager.listProviderAvailability();
1918
1930
  this.emit({
1919
- type: "list_available_providers_response",
1931
+ type: 'list_available_providers_response',
1920
1932
  payload: {
1921
1933
  providers,
1922
1934
  error: null,
@@ -1926,9 +1938,9 @@ export class Session {
1926
1938
  });
1927
1939
  }
1928
1940
  catch (error) {
1929
- this.sessionLogger.error({ err: error }, "Failed to list provider availability");
1941
+ this.sessionLogger.error({ err: error }, 'Failed to list provider availability');
1930
1942
  this.emit({
1931
- type: "list_available_providers_response",
1943
+ type: 'list_available_providers_response',
1932
1944
  payload: {
1933
1945
  providers: [],
1934
1946
  error: error?.message ?? String(error),
@@ -1968,7 +1980,7 @@ export class Session {
1968
1980
  };
1969
1981
  }));
1970
1982
  this.emit({
1971
- type: "speech_models_list_response",
1983
+ type: 'speech_models_list_response',
1972
1984
  payload: {
1973
1985
  modelsDir,
1974
1986
  models,
@@ -1978,18 +1990,16 @@ export class Session {
1978
1990
  }
1979
1991
  async handleSpeechModelsDownloadRequest(msg) {
1980
1992
  const modelsDir = this.localSpeechModelsDir;
1981
- const modelIdsRaw = msg.modelIds && msg.modelIds.length > 0
1982
- ? msg.modelIds
1983
- : this.defaultLocalSpeechModelIds;
1993
+ const modelIdsRaw = msg.modelIds && msg.modelIds.length > 0 ? msg.modelIds : this.defaultLocalSpeechModelIds;
1984
1994
  const allModelIds = new Set(listLocalSpeechModels().map((m) => m.id));
1985
1995
  const invalid = modelIdsRaw.filter((id) => !allModelIds.has(id));
1986
1996
  if (invalid.length > 0) {
1987
1997
  this.emit({
1988
- type: "speech_models_download_response",
1998
+ type: 'speech_models_download_response',
1989
1999
  payload: {
1990
2000
  modelsDir,
1991
2001
  downloadedModelIds: [],
1992
- error: `Unknown speech model id(s): ${invalid.join(", ")}`,
2002
+ error: `Unknown speech model id(s): ${invalid.join(', ')}`,
1993
2003
  requestId: msg.requestId,
1994
2004
  },
1995
2005
  });
@@ -2003,7 +2013,7 @@ export class Session {
2003
2013
  logger: this.sessionLogger,
2004
2014
  });
2005
2015
  this.emit({
2006
- type: "speech_models_download_response",
2016
+ type: 'speech_models_download_response',
2007
2017
  payload: {
2008
2018
  modelsDir,
2009
2019
  downloadedModelIds: modelIds,
@@ -2013,9 +2023,9 @@ export class Session {
2013
2023
  });
2014
2024
  }
2015
2025
  catch (error) {
2016
- this.sessionLogger.error({ err: error, modelIds }, "Failed to download speech models");
2026
+ this.sessionLogger.error({ err: error, modelIds }, 'Failed to download speech models');
2017
2027
  this.emit({
2018
- type: "speech_models_download_response",
2028
+ type: 'speech_models_download_response',
2019
2029
  payload: {
2020
2030
  modelsDir,
2021
2031
  downloadedModelIds: [],
@@ -2041,9 +2051,7 @@ export class Session {
2041
2051
  const baseBranch = merged.baseBranch?.trim() || undefined;
2042
2052
  const createWorktree = Boolean(merged.createWorktree);
2043
2053
  const createNewBranch = Boolean(merged.createNewBranch);
2044
- const normalizedBranchName = merged.newBranchName
2045
- ? slugify(merged.newBranchName)
2046
- : undefined;
2054
+ const normalizedBranchName = merged.newBranchName ? slugify(merged.newBranchName) : undefined;
2047
2055
  const normalizedWorktreeSlug = merged.worktreeSlug
2048
2056
  ? slugify(merged.worktreeSlug)
2049
2057
  : normalizedBranchName;
@@ -2051,17 +2059,17 @@ export class Session {
2051
2059
  return null;
2052
2060
  }
2053
2061
  if (baseBranch) {
2054
- this.assertSafeGitRef(baseBranch, "base branch");
2062
+ this.assertSafeGitRef(baseBranch, 'base branch');
2055
2063
  }
2056
2064
  if (createWorktree && !baseBranch) {
2057
- throw new Error("Base branch is required when creating a worktree");
2065
+ throw new Error('Base branch is required when creating a worktree');
2058
2066
  }
2059
2067
  if (createNewBranch && !baseBranch) {
2060
- 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');
2061
2069
  }
2062
2070
  if (createNewBranch) {
2063
2071
  if (!normalizedBranchName) {
2064
- throw new Error("New branch name is required");
2072
+ throw new Error('New branch name is required');
2065
2073
  }
2066
2074
  const validation = validateBranchSlug(normalizedBranchName);
2067
2075
  if (!validation.valid) {
@@ -2083,24 +2091,24 @@ export class Session {
2083
2091
  };
2084
2092
  }
2085
2093
  assertSafeGitRef(ref, label) {
2086
- if (!SAFE_GIT_REF_PATTERN.test(ref) || ref.includes("..") || ref.includes("@{")) {
2094
+ if (!SAFE_GIT_REF_PATTERN.test(ref) || ref.includes('..') || ref.includes('@{')) {
2087
2095
  throw new Error(`Invalid ${label}: ${ref}`);
2088
2096
  }
2089
2097
  }
2090
2098
  toCheckoutError(error) {
2091
2099
  if (error instanceof NotGitRepoError) {
2092
- return { code: "NOT_GIT_REPO", message: error.message };
2100
+ return { code: 'NOT_GIT_REPO', message: error.message };
2093
2101
  }
2094
2102
  if (error instanceof MergeConflictError) {
2095
- return { code: "MERGE_CONFLICT", message: error.message };
2103
+ return { code: 'MERGE_CONFLICT', message: error.message };
2096
2104
  }
2097
2105
  if (error instanceof MergeFromBaseConflictError) {
2098
- return { code: "MERGE_CONFLICT", message: error.message };
2106
+ return { code: 'MERGE_CONFLICT', message: error.message };
2099
2107
  }
2100
2108
  if (error instanceof Error) {
2101
- return { code: "UNKNOWN", message: error.message };
2109
+ return { code: 'UNKNOWN', message: error.message };
2102
2110
  }
2103
- return { code: "UNKNOWN", message: String(error) };
2111
+ return { code: 'UNKNOWN', message: String(error) };
2104
2112
  }
2105
2113
  isPathWithinRoot(rootPath, candidatePath) {
2106
2114
  const resolvedRoot = resolve(rootPath);
@@ -2111,47 +2119,47 @@ export class Session {
2111
2119
  return resolvedCandidate.startsWith(resolvedRoot + sep);
2112
2120
  }
2113
2121
  async generateCommitMessage(cwd) {
2114
- 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 });
2115
2123
  const schema = z.object({
2116
2124
  message: z
2117
2125
  .string()
2118
2126
  .min(1)
2119
2127
  .max(72)
2120
- .describe("Concise git commit message, imperative mood, no trailing period."),
2128
+ .describe('Concise git commit message, imperative mood, no trailing period.'),
2121
2129
  });
2122
2130
  const fileList = diff.structured && diff.structured.length > 0
2123
2131
  ? [
2124
- "Files changed:",
2132
+ 'Files changed:',
2125
2133
  ...diff.structured.map((file) => {
2126
- const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
2127
- 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}]` : '';
2128
2136
  return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
2129
2137
  }),
2130
- ].join("\n")
2131
- : "Files changed: (unknown)";
2138
+ ].join('\n')
2139
+ : 'Files changed: (unknown)';
2132
2140
  const maxPatchChars = 120000;
2133
2141
  const patch = diff.diff.length > maxPatchChars
2134
2142
  ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
2135
2143
  : diff.diff;
2136
2144
  const prompt = [
2137
- "Write a concise git commit message for the changes below.",
2145
+ 'Write a concise git commit message for the changes below.',
2138
2146
  "Return JSON only with a single field 'message'.",
2139
- "",
2147
+ '',
2140
2148
  fileList,
2141
- "",
2142
- patch.length > 0 ? patch : "(No diff available)",
2143
- ].join("\n");
2149
+ '',
2150
+ patch.length > 0 ? patch : '(No diff available)',
2151
+ ].join('\n');
2144
2152
  try {
2145
2153
  const result = await generateStructuredAgentResponseWithFallback({
2146
2154
  manager: this.agentManager,
2147
2155
  cwd,
2148
2156
  prompt,
2149
2157
  schema,
2150
- schemaName: "CommitMessage",
2158
+ schemaName: 'CommitMessage',
2151
2159
  maxRetries: 2,
2152
2160
  providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2153
2161
  agentConfigOverrides: {
2154
- title: "Commit generator",
2162
+ title: 'Commit generator',
2155
2163
  internal: true,
2156
2164
  },
2157
2165
  });
@@ -2160,14 +2168,14 @@ export class Session {
2160
2168
  catch (error) {
2161
2169
  if (error instanceof StructuredAgentResponseError ||
2162
2170
  error instanceof StructuredAgentFallbackError) {
2163
- return "Update files";
2171
+ return 'Update files';
2164
2172
  }
2165
2173
  throw error;
2166
2174
  }
2167
2175
  }
2168
2176
  async generatePullRequestText(cwd, baseRef) {
2169
2177
  const diff = await getCheckoutDiff(cwd, {
2170
- mode: "base",
2178
+ mode: 'base',
2171
2179
  baseRef,
2172
2180
  includeStructured: true,
2173
2181
  }, { paseoHome: this.paseoHome });
@@ -2177,37 +2185,37 @@ export class Session {
2177
2185
  });
2178
2186
  const fileList = diff.structured && diff.structured.length > 0
2179
2187
  ? [
2180
- "Files changed:",
2188
+ 'Files changed:',
2181
2189
  ...diff.structured.map((file) => {
2182
- const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
2183
- 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}]` : '';
2184
2192
  return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
2185
2193
  }),
2186
- ].join("\n")
2187
- : "Files changed: (unknown)";
2194
+ ].join('\n')
2195
+ : 'Files changed: (unknown)';
2188
2196
  const maxPatchChars = 200000;
2189
2197
  const patch = diff.diff.length > maxPatchChars
2190
2198
  ? `${diff.diff.slice(0, maxPatchChars)}\n\n... (diff truncated to ${maxPatchChars} chars)\n`
2191
2199
  : diff.diff;
2192
2200
  const prompt = [
2193
- "Write a pull request title and body for the changes below.",
2201
+ 'Write a pull request title and body for the changes below.',
2194
2202
  "Return JSON only with fields 'title' and 'body'.",
2195
- "",
2203
+ '',
2196
2204
  fileList,
2197
- "",
2198
- patch.length > 0 ? patch : "(No diff available)",
2199
- ].join("\n");
2205
+ '',
2206
+ patch.length > 0 ? patch : '(No diff available)',
2207
+ ].join('\n');
2200
2208
  try {
2201
2209
  return await generateStructuredAgentResponseWithFallback({
2202
2210
  manager: this.agentManager,
2203
2211
  cwd,
2204
2212
  prompt,
2205
2213
  schema,
2206
- schemaName: "PullRequest",
2214
+ schemaName: 'PullRequest',
2207
2215
  maxRetries: 2,
2208
2216
  providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2209
2217
  agentConfigOverrides: {
2210
- title: "PR generator",
2218
+ title: 'PR generator',
2211
2219
  internal: true,
2212
2220
  },
2213
2221
  });
@@ -2216,8 +2224,8 @@ export class Session {
2216
2224
  if (error instanceof StructuredAgentResponseError ||
2217
2225
  error instanceof StructuredAgentFallbackError) {
2218
2226
  return {
2219
- title: "Update changes",
2220
- body: "Automated PR generated by Paseo.",
2227
+ title: 'Update changes',
2228
+ body: 'Automated PR generated by Paseo.',
2221
2229
  };
2222
2230
  }
2223
2231
  throw error;
@@ -2226,12 +2234,12 @@ export class Session {
2226
2234
  async ensureCleanWorkingTree(cwd) {
2227
2235
  const dirty = await this.isWorkingTreeDirty(cwd);
2228
2236
  if (dirty) {
2229
- 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.');
2230
2238
  }
2231
2239
  }
2232
2240
  async isWorkingTreeDirty(cwd) {
2233
2241
  try {
2234
- const { stdout } = await execAsync("git status --porcelain", {
2242
+ const { stdout } = await execAsync('git status --porcelain', {
2235
2243
  cwd,
2236
2244
  env: READ_ONLY_GIT_ENV,
2237
2245
  });
@@ -2242,14 +2250,14 @@ export class Session {
2242
2250
  }
2243
2251
  }
2244
2252
  async checkoutExistingBranch(cwd, branch) {
2245
- this.assertSafeGitRef(branch, "branch");
2253
+ this.assertSafeGitRef(branch, 'branch');
2246
2254
  try {
2247
2255
  await execAsync(`git rev-parse --verify ${branch}`, { cwd });
2248
2256
  }
2249
2257
  catch (error) {
2250
2258
  throw new Error(`Branch not found: ${branch}`);
2251
2259
  }
2252
- const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
2260
+ const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
2253
2261
  cwd,
2254
2262
  });
2255
2263
  const current = stdout.trim();
@@ -2261,7 +2269,7 @@ export class Session {
2261
2269
  }
2262
2270
  async createBranchFromBase(params) {
2263
2271
  const { cwd, baseBranch, newBranchName } = params;
2264
- this.assertSafeGitRef(baseBranch, "base branch");
2272
+ this.assertSafeGitRef(baseBranch, 'base branch');
2265
2273
  try {
2266
2274
  await execAsync(`git rev-parse --verify ${baseBranch}`, { cwd });
2267
2275
  }
@@ -2292,99 +2300,97 @@ export class Session {
2292
2300
  * Handle set agent mode request
2293
2301
  */
2294
2302
  async handleSetAgentModeRequest(agentId, modeId, requestId) {
2295
- this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request");
2303
+ this.sessionLogger.info({ agentId, modeId, requestId }, 'session: set_agent_mode_request');
2296
2304
  try {
2297
2305
  await this.agentManager.setAgentMode(agentId, modeId);
2298
- 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');
2299
2307
  this.emit({
2300
- type: "set_agent_mode_response",
2308
+ type: 'set_agent_mode_response',
2301
2309
  payload: { requestId, agentId, accepted: true, error: null },
2302
2310
  });
2303
2311
  }
2304
2312
  catch (error) {
2305
- 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');
2306
2314
  this.emit({
2307
- type: "activity_log",
2315
+ type: 'activity_log',
2308
2316
  payload: {
2309
2317
  id: uuidv4(),
2310
2318
  timestamp: new Date(),
2311
- type: "error",
2319
+ type: 'error',
2312
2320
  content: `Failed to set agent mode: ${error.message}`,
2313
2321
  },
2314
2322
  });
2315
2323
  this.emit({
2316
- type: "set_agent_mode_response",
2324
+ type: 'set_agent_mode_response',
2317
2325
  payload: {
2318
2326
  requestId,
2319
2327
  agentId,
2320
2328
  accepted: false,
2321
- error: error?.message ? String(error.message) : "Failed to set agent mode",
2329
+ error: error?.message ? String(error.message) : 'Failed to set agent mode',
2322
2330
  },
2323
2331
  });
2324
2332
  }
2325
2333
  }
2326
2334
  async handleSetAgentModelRequest(agentId, modelId, requestId) {
2327
- this.sessionLogger.info({ agentId, modelId, requestId }, "session: set_agent_model_request");
2335
+ this.sessionLogger.info({ agentId, modelId, requestId }, 'session: set_agent_model_request');
2328
2336
  try {
2329
2337
  await this.agentManager.setAgentModel(agentId, modelId);
2330
- 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');
2331
2339
  this.emit({
2332
- type: "set_agent_model_response",
2340
+ type: 'set_agent_model_response',
2333
2341
  payload: { requestId, agentId, accepted: true, error: null },
2334
2342
  });
2335
2343
  }
2336
2344
  catch (error) {
2337
- 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');
2338
2346
  this.emit({
2339
- type: "activity_log",
2347
+ type: 'activity_log',
2340
2348
  payload: {
2341
2349
  id: uuidv4(),
2342
2350
  timestamp: new Date(),
2343
- type: "error",
2351
+ type: 'error',
2344
2352
  content: `Failed to set agent model: ${error.message}`,
2345
2353
  },
2346
2354
  });
2347
2355
  this.emit({
2348
- type: "set_agent_model_response",
2356
+ type: 'set_agent_model_response',
2349
2357
  payload: {
2350
2358
  requestId,
2351
2359
  agentId,
2352
2360
  accepted: false,
2353
- error: error?.message ? String(error.message) : "Failed to set agent model",
2361
+ error: error?.message ? String(error.message) : 'Failed to set agent model',
2354
2362
  },
2355
2363
  });
2356
2364
  }
2357
2365
  }
2358
2366
  async handleSetAgentThinkingRequest(agentId, thinkingOptionId, requestId) {
2359
- this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request");
2367
+ this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, 'session: set_agent_thinking_request');
2360
2368
  try {
2361
2369
  await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
2362
- 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');
2363
2371
  this.emit({
2364
- type: "set_agent_thinking_response",
2372
+ type: 'set_agent_thinking_response',
2365
2373
  payload: { requestId, agentId, accepted: true, error: null },
2366
2374
  });
2367
2375
  }
2368
2376
  catch (error) {
2369
- 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');
2370
2378
  this.emit({
2371
- type: "activity_log",
2379
+ type: 'activity_log',
2372
2380
  payload: {
2373
2381
  id: uuidv4(),
2374
2382
  timestamp: new Date(),
2375
- type: "error",
2383
+ type: 'error',
2376
2384
  content: `Failed to set agent thinking option: ${error.message}`,
2377
2385
  },
2378
2386
  });
2379
2387
  this.emit({
2380
- type: "set_agent_thinking_response",
2388
+ type: 'set_agent_thinking_response',
2381
2389
  payload: {
2382
2390
  requestId,
2383
2391
  agentId,
2384
2392
  accepted: false,
2385
- error: error?.message
2386
- ? String(error.message)
2387
- : "Failed to set agent thinking option",
2393
+ error: error?.message ? String(error.message) : 'Failed to set agent thinking option',
2388
2394
  },
2389
2395
  });
2390
2396
  }
@@ -2394,12 +2400,12 @@ export class Session {
2394
2400
  */
2395
2401
  async handleClearAgentAttention(agentId) {
2396
2402
  const agentIds = Array.isArray(agentId) ? agentId : [agentId];
2397
- 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(', ')}`);
2398
2404
  try {
2399
2405
  await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id)));
2400
2406
  }
2401
2407
  catch (error) {
2402
- this.sessionLogger.error({ err: error, agentIds }, "Failed to clear agent attention");
2408
+ this.sessionLogger.error({ err: error, agentIds }, 'Failed to clear agent attention');
2403
2409
  // Don't throw - this is not critical
2404
2410
  }
2405
2411
  }
@@ -2423,116 +2429,69 @@ export class Session {
2423
2429
  */
2424
2430
  handleRegisterPushToken(token) {
2425
2431
  this.pushTokenStore.addToken(token);
2426
- this.sessionLogger.info("Registered push token");
2432
+ this.sessionLogger.info('Registered push token');
2427
2433
  }
2428
2434
  /**
2429
2435
  * Handle list commands request for an agent
2430
2436
  */
2431
- async handleListCommandsRequest(agentId, requestId) {
2432
- 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}`);
2433
2440
  try {
2434
2441
  const agents = this.agentManager.listAgents();
2435
2442
  const agent = agents.find((a) => a.id === agentId);
2436
- if (!agent) {
2443
+ if (agent?.session?.listCommands) {
2444
+ const commands = await agent.session.listCommands();
2437
2445
  this.emit({
2438
- type: "list_commands_response",
2446
+ type: 'list_commands_response',
2439
2447
  payload: {
2440
2448
  agentId,
2441
- commands: [],
2442
- error: `Agent not found: ${agentId}`,
2449
+ commands,
2450
+ error: null,
2443
2451
  requestId,
2444
2452
  },
2445
2453
  });
2446
2454
  return;
2447
2455
  }
2448
- const session = agent.session;
2449
- 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);
2450
2467
  this.emit({
2451
- type: "list_commands_response",
2468
+ type: 'list_commands_response',
2452
2469
  payload: {
2453
2470
  agentId,
2454
- commands: [],
2455
- error: `Agent does not support listing commands`,
2471
+ commands,
2472
+ error: null,
2456
2473
  requestId,
2457
2474
  },
2458
2475
  });
2459
2476
  return;
2460
2477
  }
2461
- const commands = await session.listCommands();
2462
2478
  this.emit({
2463
- type: "list_commands_response",
2464
- payload: {
2465
- agentId,
2466
- commands,
2467
- error: null,
2468
- requestId,
2469
- },
2470
- });
2471
- }
2472
- catch (error) {
2473
- this.sessionLogger.error({ err: error, agentId }, "Failed to list commands");
2474
- this.emit({
2475
- type: "list_commands_response",
2479
+ type: 'list_commands_response',
2476
2480
  payload: {
2477
2481
  agentId,
2478
2482
  commands: [],
2479
- error: error.message,
2480
- requestId,
2481
- },
2482
- });
2483
- }
2484
- }
2485
- /**
2486
- * Handle execute command request for an agent
2487
- */
2488
- async handleExecuteCommandRequest(agentId, commandName, args, requestId) {
2489
- this.sessionLogger.debug({ agentId, commandName }, `Handling execute command request for agent ${agentId}`);
2490
- try {
2491
- const agents = this.agentManager.listAgents();
2492
- const agent = agents.find((a) => a.id === agentId);
2493
- if (!agent) {
2494
- this.emit({
2495
- type: "execute_command_response",
2496
- payload: {
2497
- agentId,
2498
- result: null,
2499
- error: `Agent not found: ${agentId}`,
2500
- requestId,
2501
- },
2502
- });
2503
- return;
2504
- }
2505
- const session = agent.session;
2506
- if (!session || !session.executeCommand) {
2507
- this.emit({
2508
- type: "execute_command_response",
2509
- payload: {
2510
- agentId,
2511
- result: null,
2512
- error: `Agent does not support executing commands`,
2513
- requestId,
2514
- },
2515
- });
2516
- return;
2517
- }
2518
- const result = await session.executeCommand(commandName, args);
2519
- this.emit({
2520
- type: "execute_command_response",
2521
- payload: {
2522
- agentId,
2523
- result,
2524
- error: null,
2483
+ error: agent ? `Agent does not support listing commands` : `Agent not found: ${agentId}`,
2525
2484
  requestId,
2526
2485
  },
2527
2486
  });
2528
2487
  }
2529
2488
  catch (error) {
2530
- this.sessionLogger.error({ err: error, agentId, commandName }, "Failed to execute command");
2489
+ this.sessionLogger.error({ err: error, agentId, draftConfig }, 'Failed to list commands');
2531
2490
  this.emit({
2532
- type: "execute_command_response",
2491
+ type: 'list_commands_response',
2533
2492
  payload: {
2534
2493
  agentId,
2535
- result: null,
2494
+ commands: [],
2536
2495
  error: error.message,
2537
2496
  requestId,
2538
2497
  },
@@ -2549,13 +2508,13 @@ export class Session {
2549
2508
  this.sessionLogger.debug({ agentId }, `Permission response forwarded to agent ${agentId}`);
2550
2509
  }
2551
2510
  catch (error) {
2552
- 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');
2553
2512
  this.emit({
2554
- type: "activity_log",
2513
+ type: 'activity_log',
2555
2514
  payload: {
2556
2515
  id: uuidv4(),
2557
2516
  timestamp: new Date(),
2558
- type: "error",
2517
+ type: 'error',
2559
2518
  content: `Failed to respond to permission: ${error.message}`,
2560
2519
  },
2561
2520
  });
@@ -2569,7 +2528,7 @@ export class Session {
2569
2528
  const status = await getCheckoutStatus(resolvedCwd, { paseoHome: this.paseoHome });
2570
2529
  if (!status.isGit) {
2571
2530
  this.emit({
2572
- type: "checkout_status_response",
2531
+ type: 'checkout_status_response',
2573
2532
  payload: {
2574
2533
  cwd,
2575
2534
  isGit: false,
@@ -2579,6 +2538,7 @@ export class Session {
2579
2538
  baseRef: null,
2580
2539
  aheadBehind: null,
2581
2540
  aheadOfOrigin: null,
2541
+ behindOfOrigin: null,
2582
2542
  hasRemote: false,
2583
2543
  remoteUrl: null,
2584
2544
  isPaseoOwnedWorktree: false,
@@ -2590,7 +2550,7 @@ export class Session {
2590
2550
  }
2591
2551
  if (status.isPaseoOwnedWorktree) {
2592
2552
  this.emit({
2593
- type: "checkout_status_response",
2553
+ type: 'checkout_status_response',
2594
2554
  payload: {
2595
2555
  cwd,
2596
2556
  isGit: true,
@@ -2601,6 +2561,7 @@ export class Session {
2601
2561
  baseRef: status.baseRef,
2602
2562
  aheadBehind: status.aheadBehind ?? null,
2603
2563
  aheadOfOrigin: status.aheadOfOrigin ?? null,
2564
+ behindOfOrigin: status.behindOfOrigin ?? null,
2604
2565
  hasRemote: status.hasRemote,
2605
2566
  remoteUrl: status.remoteUrl,
2606
2567
  isPaseoOwnedWorktree: true,
@@ -2611,7 +2572,7 @@ export class Session {
2611
2572
  return;
2612
2573
  }
2613
2574
  this.emit({
2614
- type: "checkout_status_response",
2575
+ type: 'checkout_status_response',
2615
2576
  payload: {
2616
2577
  cwd,
2617
2578
  isGit: true,
@@ -2621,6 +2582,7 @@ export class Session {
2621
2582
  baseRef: status.baseRef ?? null,
2622
2583
  aheadBehind: status.aheadBehind ?? null,
2623
2584
  aheadOfOrigin: status.aheadOfOrigin ?? null,
2585
+ behindOfOrigin: status.behindOfOrigin ?? null,
2624
2586
  hasRemote: status.hasRemote,
2625
2587
  remoteUrl: status.remoteUrl,
2626
2588
  isPaseoOwnedWorktree: false,
@@ -2631,7 +2593,7 @@ export class Session {
2631
2593
  }
2632
2594
  catch (error) {
2633
2595
  this.emit({
2634
- type: "checkout_status_response",
2596
+ type: 'checkout_status_response',
2635
2597
  payload: {
2636
2598
  cwd,
2637
2599
  isGit: false,
@@ -2641,6 +2603,7 @@ export class Session {
2641
2603
  baseRef: null,
2642
2604
  aheadBehind: null,
2643
2605
  aheadOfOrigin: null,
2606
+ behindOfOrigin: null,
2644
2607
  hasRemote: false,
2645
2608
  remoteUrl: null,
2646
2609
  isPaseoOwnedWorktree: false,
@@ -2661,7 +2624,7 @@ export class Session {
2661
2624
  env: READ_ONLY_GIT_ENV,
2662
2625
  });
2663
2626
  this.emit({
2664
- type: "validate_branch_response",
2627
+ type: 'validate_branch_response',
2665
2628
  payload: {
2666
2629
  exists: true,
2667
2630
  resolvedRef: branchName,
@@ -2682,7 +2645,7 @@ export class Session {
2682
2645
  env: READ_ONLY_GIT_ENV,
2683
2646
  });
2684
2647
  this.emit({
2685
- type: "validate_branch_response",
2648
+ type: 'validate_branch_response',
2686
2649
  payload: {
2687
2650
  exists: true,
2688
2651
  resolvedRef: `origin/${branchName}`,
@@ -2698,7 +2661,7 @@ export class Session {
2698
2661
  }
2699
2662
  // Branch not found anywhere
2700
2663
  this.emit({
2701
- type: "validate_branch_response",
2664
+ type: 'validate_branch_response',
2702
2665
  payload: {
2703
2666
  exists: false,
2704
2667
  resolvedRef: null,
@@ -2710,7 +2673,7 @@ export class Session {
2710
2673
  }
2711
2674
  catch (error) {
2712
2675
  this.emit({
2713
- type: "validate_branch_response",
2676
+ type: 'validate_branch_response',
2714
2677
  payload: {
2715
2678
  exists: false,
2716
2679
  resolvedRef: null,
@@ -2727,7 +2690,7 @@ export class Session {
2727
2690
  const resolvedCwd = expandTilde(cwd);
2728
2691
  const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
2729
2692
  this.emit({
2730
- type: "branch_suggestions_response",
2693
+ type: 'branch_suggestions_response',
2731
2694
  payload: {
2732
2695
  branches,
2733
2696
  error: null,
@@ -2737,7 +2700,7 @@ export class Session {
2737
2700
  }
2738
2701
  catch (error) {
2739
2702
  this.emit({
2740
- type: "branch_suggestions_response",
2703
+ type: 'branch_suggestions_response',
2741
2704
  payload: {
2742
2705
  branches: [],
2743
2706
  error: error instanceof Error ? error.message : String(error),
@@ -2747,17 +2710,30 @@ export class Session {
2747
2710
  }
2748
2711
  }
2749
2712
  async handleDirectorySuggestionsRequest(msg) {
2750
- const { query, limit, requestId } = msg;
2713
+ const { query, limit, requestId, cwd, includeFiles, includeDirectories } = msg;
2751
2714
  try {
2752
- const directories = await searchHomeDirectories({
2753
- homeDir: process.env.HOME ?? homedir(),
2754
- query,
2755
- limit,
2756
- });
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);
2757
2732
  this.emit({
2758
- type: "directory_suggestions_response",
2733
+ type: 'directory_suggestions_response',
2759
2734
  payload: {
2760
2735
  directories,
2736
+ entries,
2761
2737
  error: null,
2762
2738
  requestId,
2763
2739
  },
@@ -2765,9 +2741,10 @@ export class Session {
2765
2741
  }
2766
2742
  catch (error) {
2767
2743
  this.emit({
2768
- type: "directory_suggestions_response",
2744
+ type: 'directory_suggestions_response',
2769
2745
  payload: {
2770
2746
  directories: [],
2747
+ entries: [],
2771
2748
  error: error instanceof Error ? error.message : String(error),
2772
2749
  requestId,
2773
2750
  },
@@ -2775,19 +2752,17 @@ export class Session {
2775
2752
  }
2776
2753
  }
2777
2754
  normalizeCheckoutDiffCompare(compare) {
2778
- if (compare.mode === "uncommitted") {
2779
- return { mode: "uncommitted" };
2755
+ if (compare.mode === 'uncommitted') {
2756
+ return { mode: 'uncommitted' };
2780
2757
  }
2781
2758
  const trimmedBaseRef = compare.baseRef?.trim();
2782
- return trimmedBaseRef
2783
- ? { mode: "base", baseRef: trimmedBaseRef }
2784
- : { mode: "base" };
2759
+ return trimmedBaseRef ? { mode: 'base', baseRef: trimmedBaseRef } : { mode: 'base' };
2785
2760
  }
2786
2761
  buildCheckoutDiffTargetKey(cwd, compare) {
2787
2762
  return JSON.stringify([
2788
2763
  cwd,
2789
2764
  compare.mode,
2790
- compare.mode === "base" ? (compare.baseRef ?? "") : "",
2765
+ compare.mode === 'base' ? (compare.baseRef ?? '') : '',
2791
2766
  ]);
2792
2767
  }
2793
2768
  closeCheckoutDiffWatchTarget(target) {
@@ -2822,7 +2797,7 @@ export class Session {
2822
2797
  }
2823
2798
  async resolveCheckoutGitDir(cwd) {
2824
2799
  try {
2825
- const { stdout } = await execAsync("git rev-parse --absolute-git-dir", {
2800
+ const { stdout } = await execAsync('git rev-parse --absolute-git-dir', {
2826
2801
  cwd,
2827
2802
  env: READ_ONLY_GIT_ENV,
2828
2803
  });
@@ -2835,7 +2810,7 @@ export class Session {
2835
2810
  }
2836
2811
  async resolveCheckoutWatchRoot(cwd) {
2837
2812
  try {
2838
- 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', {
2839
2814
  cwd,
2840
2815
  env: READ_ONLY_GIT_ENV,
2841
2816
  });
@@ -2861,7 +2836,7 @@ export class Session {
2861
2836
  }
2862
2837
  for (const subscriptionId of target.subscriptions) {
2863
2838
  this.emit({
2864
- type: "checkout_diff_update",
2839
+ type: 'checkout_diff_update',
2865
2840
  payload: {
2866
2841
  subscriptionId,
2867
2842
  ...snapshot,
@@ -2908,7 +2883,9 @@ export class Session {
2908
2883
  target.refreshPromise = (async () => {
2909
2884
  do {
2910
2885
  target.refreshQueued = false;
2911
- 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
+ });
2912
2889
  target.latestPayload = snapshot;
2913
2890
  const fingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
2914
2891
  if (fingerprint !== target.latestFingerprint) {
@@ -2952,7 +2929,7 @@ export class Session {
2952
2929
  watchPaths.add(gitDir);
2953
2930
  }
2954
2931
  let hasRecursiveRepoCoverage = false;
2955
- const allowRecursiveRepoWatch = process.platform !== "linux";
2932
+ const allowRecursiveRepoWatch = process.platform !== 'linux';
2956
2933
  for (const watchPath of watchPaths) {
2957
2934
  const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
2958
2935
  const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
@@ -2973,21 +2950,21 @@ export class Session {
2973
2950
  if (shouldTryRecursive) {
2974
2951
  try {
2975
2952
  watcher = createWatcher(false);
2976
- 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');
2977
2954
  }
2978
2955
  catch (fallbackError) {
2979
- 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');
2980
2957
  }
2981
2958
  }
2982
2959
  else {
2983
- 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');
2984
2961
  }
2985
2962
  }
2986
2963
  if (!watcher) {
2987
2964
  continue;
2988
2965
  }
2989
- watcher.on("error", (error) => {
2990
- 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');
2991
2968
  });
2992
2969
  target.watchers.push(watcher);
2993
2970
  if (watchPath === repoWatchPath && watcherIsRecursive) {
@@ -3003,10 +2980,8 @@ export class Session {
3003
2980
  cwd,
3004
2981
  compare,
3005
2982
  intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
3006
- reason: target.watchers.length === 0
3007
- ? "no_watchers"
3008
- : "missing_recursive_repo_root_coverage",
3009
- }, "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');
3010
2985
  }
3011
2986
  this.checkoutDiffTargets.set(targetKey, target);
3012
2987
  return target;
@@ -3027,7 +3002,7 @@ export class Session {
3027
3002
  target.latestPayload = snapshot;
3028
3003
  target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
3029
3004
  this.emit({
3030
- type: "subscribe_checkout_diff_response",
3005
+ type: 'subscribe_checkout_diff_response',
3031
3006
  payload: {
3032
3007
  subscriptionId: msg.subscriptionId,
3033
3008
  ...snapshot,
@@ -3050,12 +3025,12 @@ export class Session {
3050
3025
  async handleCheckoutCommitRequest(msg) {
3051
3026
  const { cwd, requestId } = msg;
3052
3027
  try {
3053
- let message = msg.message?.trim() ?? "";
3028
+ let message = msg.message?.trim() ?? '';
3054
3029
  if (!message) {
3055
3030
  message = await this.generateCommitMessage(cwd);
3056
3031
  }
3057
3032
  if (!message) {
3058
- throw new Error("Commit message is required");
3033
+ throw new Error('Commit message is required');
3059
3034
  }
3060
3035
  await commitChanges(cwd, {
3061
3036
  message,
@@ -3063,7 +3038,7 @@ export class Session {
3063
3038
  });
3064
3039
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3065
3040
  this.emit({
3066
- type: "checkout_commit_response",
3041
+ type: 'checkout_commit_response',
3067
3042
  payload: {
3068
3043
  cwd,
3069
3044
  success: true,
@@ -3074,7 +3049,7 @@ export class Session {
3074
3049
  }
3075
3050
  catch (error) {
3076
3051
  this.emit({
3077
- type: "checkout_commit_response",
3052
+ type: 'checkout_commit_response',
3078
3053
  payload: {
3079
3054
  cwd,
3080
3055
  success: false,
@@ -3090,13 +3065,13 @@ export class Session {
3090
3065
  const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
3091
3066
  if (!status.isGit) {
3092
3067
  try {
3093
- await execAsync("git rev-parse --is-inside-work-tree", {
3068
+ await execAsync('git rev-parse --is-inside-work-tree', {
3094
3069
  cwd,
3095
3070
  env: READ_ONLY_GIT_ENV,
3096
3071
  });
3097
3072
  }
3098
3073
  catch (error) {
3099
- const details = typeof error?.stderr === "string"
3074
+ const details = typeof error?.stderr === 'string'
3100
3075
  ? String(error.stderr).trim()
3101
3076
  : error instanceof Error
3102
3077
  ? error.message
@@ -3105,28 +3080,28 @@ export class Session {
3105
3080
  }
3106
3081
  }
3107
3082
  if (msg.requireCleanTarget) {
3108
- const { stdout } = await execAsync("git status --porcelain", {
3083
+ const { stdout } = await execAsync('git status --porcelain', {
3109
3084
  cwd,
3110
3085
  env: READ_ONLY_GIT_ENV,
3111
3086
  });
3112
3087
  if (stdout.trim().length > 0) {
3113
- throw new Error("Working directory has uncommitted changes.");
3088
+ throw new Error('Working directory has uncommitted changes.');
3114
3089
  }
3115
3090
  }
3116
3091
  let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
3117
3092
  if (!baseRef) {
3118
- throw new Error("Base branch is required for merge");
3093
+ throw new Error('Base branch is required for merge');
3119
3094
  }
3120
- if (baseRef.startsWith("origin/")) {
3121
- baseRef = baseRef.slice("origin/".length);
3095
+ if (baseRef.startsWith('origin/')) {
3096
+ baseRef = baseRef.slice('origin/'.length);
3122
3097
  }
3123
3098
  await mergeToBase(cwd, {
3124
3099
  baseRef,
3125
- mode: msg.strategy === "squash" ? "squash" : "merge",
3100
+ mode: msg.strategy === 'squash' ? 'squash' : 'merge',
3126
3101
  }, { paseoHome: this.paseoHome });
3127
3102
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3128
3103
  this.emit({
3129
- type: "checkout_merge_response",
3104
+ type: 'checkout_merge_response',
3130
3105
  payload: {
3131
3106
  cwd,
3132
3107
  success: true,
@@ -3137,7 +3112,7 @@ export class Session {
3137
3112
  }
3138
3113
  catch (error) {
3139
3114
  this.emit({
3140
- type: "checkout_merge_response",
3115
+ type: 'checkout_merge_response',
3141
3116
  payload: {
3142
3117
  cwd,
3143
3118
  success: false,
@@ -3151,12 +3126,12 @@ export class Session {
3151
3126
  const { cwd, requestId } = msg;
3152
3127
  try {
3153
3128
  if (msg.requireCleanTarget ?? true) {
3154
- const { stdout } = await execAsync("git status --porcelain", {
3129
+ const { stdout } = await execAsync('git status --porcelain', {
3155
3130
  cwd,
3156
3131
  env: READ_ONLY_GIT_ENV,
3157
3132
  });
3158
3133
  if (stdout.trim().length > 0) {
3159
- throw new Error("Working directory has uncommitted changes.");
3134
+ throw new Error('Working directory has uncommitted changes.');
3160
3135
  }
3161
3136
  }
3162
3137
  await mergeFromBase(cwd, {
@@ -3165,7 +3140,7 @@ export class Session {
3165
3140
  });
3166
3141
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3167
3142
  this.emit({
3168
- type: "checkout_merge_from_base_response",
3143
+ type: 'checkout_merge_from_base_response',
3169
3144
  payload: {
3170
3145
  cwd,
3171
3146
  success: true,
@@ -3176,7 +3151,7 @@ export class Session {
3176
3151
  }
3177
3152
  catch (error) {
3178
3153
  this.emit({
3179
- type: "checkout_merge_from_base_response",
3154
+ type: 'checkout_merge_from_base_response',
3180
3155
  payload: {
3181
3156
  cwd,
3182
3157
  success: false,
@@ -3191,7 +3166,7 @@ export class Session {
3191
3166
  try {
3192
3167
  await pushCurrentBranch(cwd);
3193
3168
  this.emit({
3194
- type: "checkout_push_response",
3169
+ type: 'checkout_push_response',
3195
3170
  payload: {
3196
3171
  cwd,
3197
3172
  success: true,
@@ -3202,7 +3177,7 @@ export class Session {
3202
3177
  }
3203
3178
  catch (error) {
3204
3179
  this.emit({
3205
- type: "checkout_push_response",
3180
+ type: 'checkout_push_response',
3206
3181
  payload: {
3207
3182
  cwd,
3208
3183
  success: false,
@@ -3215,8 +3190,8 @@ export class Session {
3215
3190
  async handleCheckoutPrCreateRequest(msg) {
3216
3191
  const { cwd, requestId } = msg;
3217
3192
  try {
3218
- let title = msg.title?.trim() ?? "";
3219
- let body = msg.body?.trim() ?? "";
3193
+ let title = msg.title?.trim() ?? '';
3194
+ let body = msg.body?.trim() ?? '';
3220
3195
  if (!title || !body) {
3221
3196
  const generated = await this.generatePullRequestText(cwd, msg.baseRef);
3222
3197
  if (!title)
@@ -3230,7 +3205,7 @@ export class Session {
3230
3205
  base: msg.baseRef,
3231
3206
  });
3232
3207
  this.emit({
3233
- type: "checkout_pr_create_response",
3208
+ type: 'checkout_pr_create_response',
3234
3209
  payload: {
3235
3210
  cwd,
3236
3211
  url: result.url ?? null,
@@ -3242,7 +3217,7 @@ export class Session {
3242
3217
  }
3243
3218
  catch (error) {
3244
3219
  this.emit({
3245
- type: "checkout_pr_create_response",
3220
+ type: 'checkout_pr_create_response',
3246
3221
  payload: {
3247
3222
  cwd,
3248
3223
  url: null,
@@ -3258,7 +3233,7 @@ export class Session {
3258
3233
  try {
3259
3234
  const prStatus = await getPullRequestStatus(cwd);
3260
3235
  this.emit({
3261
- type: "checkout_pr_status_response",
3236
+ type: 'checkout_pr_status_response',
3262
3237
  payload: {
3263
3238
  cwd,
3264
3239
  status: prStatus.status,
@@ -3270,7 +3245,7 @@ export class Session {
3270
3245
  }
3271
3246
  catch (error) {
3272
3247
  this.emit({
3273
- type: "checkout_pr_status_response",
3248
+ type: 'checkout_pr_status_response',
3274
3249
  payload: {
3275
3250
  cwd,
3276
3251
  status: null,
@@ -3286,10 +3261,10 @@ export class Session {
3286
3261
  const cwd = msg.repoRoot ?? msg.cwd;
3287
3262
  if (!cwd) {
3288
3263
  this.emit({
3289
- type: "paseo_worktree_list_response",
3264
+ type: 'paseo_worktree_list_response',
3290
3265
  payload: {
3291
3266
  worktrees: [],
3292
- error: { code: "UNKNOWN", message: "cwd or repoRoot is required" },
3267
+ error: { code: 'UNKNOWN', message: 'cwd or repoRoot is required' },
3293
3268
  requestId,
3294
3269
  },
3295
3270
  });
@@ -3298,7 +3273,7 @@ export class Session {
3298
3273
  try {
3299
3274
  const worktrees = await listPaseoWorktrees({ cwd, paseoHome: this.paseoHome });
3300
3275
  this.emit({
3301
- type: "paseo_worktree_list_response",
3276
+ type: 'paseo_worktree_list_response',
3302
3277
  payload: {
3303
3278
  worktrees: worktrees.map((entry) => ({
3304
3279
  worktreePath: entry.path,
@@ -3312,7 +3287,7 @@ export class Session {
3312
3287
  }
3313
3288
  catch (error) {
3314
3289
  this.emit({
3315
- type: "paseo_worktree_list_response",
3290
+ type: 'paseo_worktree_list_response',
3316
3291
  payload: {
3317
3292
  worktrees: [],
3318
3293
  error: this.toCheckoutError(error),
@@ -3321,6 +3296,115 @@ export class Session {
3321
3296
  });
3322
3297
  }
3323
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
+ }
3324
3408
  async handlePaseoWorktreeArchiveRequest(msg) {
3325
3409
  const { requestId } = msg;
3326
3410
  let targetPath = msg.worktreePath;
@@ -3328,7 +3412,7 @@ export class Session {
3328
3412
  try {
3329
3413
  if (!targetPath) {
3330
3414
  if (!repoRoot || !msg.branchName) {
3331
- throw new Error("worktreePath or repoRoot+branchName is required");
3415
+ throw new Error('worktreePath or repoRoot+branchName is required');
3332
3416
  }
3333
3417
  const worktrees = await listPaseoWorktrees({ cwd: repoRoot, paseoHome: this.paseoHome });
3334
3418
  const match = worktrees.find((entry) => entry.branchName === msg.branchName);
@@ -3337,16 +3421,18 @@ export class Session {
3337
3421
  }
3338
3422
  targetPath = match.path;
3339
3423
  }
3340
- const ownership = await isPaseoOwnedWorktreeCwd(targetPath, { paseoHome: this.paseoHome });
3424
+ const ownership = await isPaseoOwnedWorktreeCwd(targetPath, {
3425
+ paseoHome: this.paseoHome,
3426
+ });
3341
3427
  if (!ownership.allowed) {
3342
3428
  this.emit({
3343
- type: "paseo_worktree_archive_response",
3429
+ type: 'paseo_worktree_archive_response',
3344
3430
  payload: {
3345
3431
  success: false,
3346
3432
  removedAgents: [],
3347
3433
  error: {
3348
- code: "NOT_ALLOWED",
3349
- message: "Worktree is not a Paseo-owned worktree",
3434
+ code: 'NOT_ALLOWED',
3435
+ message: 'Worktree is not a Paseo-owned worktree',
3350
3436
  },
3351
3437
  requestId,
3352
3438
  },
@@ -3355,64 +3441,18 @@ export class Session {
3355
3441
  }
3356
3442
  repoRoot = ownership.repoRoot ?? repoRoot ?? null;
3357
3443
  if (!repoRoot) {
3358
- throw new Error("Unable to resolve repo root for worktree");
3359
- }
3360
- const resolvedWorktree = await resolvePaseoWorktreeRootForCwd(targetPath, {
3361
- paseoHome: this.paseoHome,
3362
- });
3363
- if (resolvedWorktree) {
3364
- targetPath = resolvedWorktree.worktreePath;
3365
- }
3366
- const removedAgents = new Set();
3367
- const agents = this.agentManager.listAgents();
3368
- for (const agent of agents) {
3369
- if (this.isPathWithinRoot(targetPath, agent.cwd)) {
3370
- removedAgents.add(agent.id);
3371
- try {
3372
- await this.agentManager.closeAgent(agent.id);
3373
- }
3374
- catch {
3375
- // ignore cleanup errors
3376
- }
3377
- try {
3378
- await this.agentStorage.remove(agent.id);
3379
- }
3380
- catch {
3381
- // ignore cleanup errors
3382
- }
3383
- }
3384
- }
3385
- const registryRecords = await this.agentStorage.list();
3386
- for (const record of registryRecords) {
3387
- if (this.isPathWithinRoot(targetPath, record.cwd)) {
3388
- removedAgents.add(record.id);
3389
- try {
3390
- await this.agentStorage.remove(record.id);
3391
- }
3392
- catch {
3393
- // ignore cleanup errors
3394
- }
3395
- }
3444
+ throw new Error('Unable to resolve repo root for worktree');
3396
3445
  }
3397
- await deletePaseoWorktree({
3398
- cwd: repoRoot,
3399
- worktreePath: targetPath,
3400
- paseoHome: this.paseoHome,
3446
+ const removedAgents = await this.archivePaseoWorktree({
3447
+ targetPath,
3448
+ repoRoot,
3449
+ requestId,
3401
3450
  });
3402
- for (const agentId of removedAgents) {
3403
- this.emit({
3404
- type: "agent_deleted",
3405
- payload: {
3406
- agentId,
3407
- requestId,
3408
- },
3409
- });
3410
- }
3411
3451
  this.emit({
3412
- type: "paseo_worktree_archive_response",
3452
+ type: 'paseo_worktree_archive_response',
3413
3453
  payload: {
3414
3454
  success: true,
3415
- removedAgents: Array.from(removedAgents),
3455
+ removedAgents,
3416
3456
  error: null,
3417
3457
  requestId,
3418
3458
  },
@@ -3420,7 +3460,7 @@ export class Session {
3420
3460
  }
3421
3461
  catch (error) {
3422
3462
  this.emit({
3423
- type: "paseo_worktree_archive_response",
3463
+ type: 'paseo_worktree_archive_response',
3424
3464
  payload: {
3425
3465
  success: false,
3426
3466
  removedAgents: [],
@@ -3434,14 +3474,14 @@ export class Session {
3434
3474
  * Handle read-only file explorer requests scoped to an agent's cwd
3435
3475
  */
3436
3476
  async handleFileExplorerRequest(request) {
3437
- const { agentId, path: requestedPath = ".", mode, requestId } = request;
3477
+ const { agentId, path: requestedPath = '.', mode, requestId } = request;
3438
3478
  this.sessionLogger.debug({ agentId, mode, path: requestedPath }, `Handling file explorer request for agent ${agentId} (${mode} ${requestedPath})`);
3439
3479
  try {
3440
3480
  const agents = this.agentManager.listAgents();
3441
3481
  const agent = agents.find((a) => a.id === agentId);
3442
3482
  if (!agent) {
3443
3483
  this.emit({
3444
- type: "file_explorer_response",
3484
+ type: 'file_explorer_response',
3445
3485
  payload: {
3446
3486
  agentId,
3447
3487
  path: requestedPath,
@@ -3454,13 +3494,13 @@ export class Session {
3454
3494
  });
3455
3495
  return;
3456
3496
  }
3457
- if (mode === "list") {
3497
+ if (mode === 'list') {
3458
3498
  const directory = await listDirectoryEntries({
3459
3499
  root: agent.cwd,
3460
3500
  relativePath: requestedPath,
3461
3501
  });
3462
3502
  this.emit({
3463
- type: "file_explorer_response",
3503
+ type: 'file_explorer_response',
3464
3504
  payload: {
3465
3505
  agentId,
3466
3506
  path: directory.path,
@@ -3478,7 +3518,7 @@ export class Session {
3478
3518
  relativePath: requestedPath,
3479
3519
  });
3480
3520
  this.emit({
3481
- type: "file_explorer_response",
3521
+ type: 'file_explorer_response',
3482
3522
  payload: {
3483
3523
  agentId,
3484
3524
  path: file.path,
@@ -3494,7 +3534,7 @@ export class Session {
3494
3534
  catch (error) {
3495
3535
  this.sessionLogger.error({ err: error, agentId, path: requestedPath }, `Failed to fulfill file explorer request for agent ${agentId}`);
3496
3536
  this.emit({
3497
- type: "file_explorer_response",
3537
+ type: 'file_explorer_response',
3498
3538
  payload: {
3499
3539
  agentId,
3500
3540
  path: requestedPath,
@@ -3515,7 +3555,7 @@ export class Session {
3515
3555
  try {
3516
3556
  const icon = await getProjectIcon(cwd);
3517
3557
  this.emit({
3518
- type: "project_icon_response",
3558
+ type: 'project_icon_response',
3519
3559
  payload: {
3520
3560
  cwd,
3521
3561
  icon,
@@ -3526,7 +3566,7 @@ export class Session {
3526
3566
  }
3527
3567
  catch (error) {
3528
3568
  this.emit({
3529
- type: "project_icon_response",
3569
+ type: 'project_icon_response',
3530
3570
  payload: {
3531
3571
  cwd,
3532
3572
  icon: null,
@@ -3547,7 +3587,7 @@ export class Session {
3547
3587
  const agent = agents.find((a) => a.id === agentId);
3548
3588
  if (!agent) {
3549
3589
  this.emit({
3550
- type: "file_download_token_response",
3590
+ type: 'file_download_token_response',
3551
3591
  payload: {
3552
3592
  agentId,
3553
3593
  path: requestedPath,
@@ -3574,7 +3614,7 @@ export class Session {
3574
3614
  size: info.size,
3575
3615
  });
3576
3616
  this.emit({
3577
- type: "file_download_token_response",
3617
+ type: 'file_download_token_response',
3578
3618
  payload: {
3579
3619
  agentId,
3580
3620
  path: info.path,
@@ -3590,7 +3630,7 @@ export class Session {
3590
3630
  catch (error) {
3591
3631
  this.sessionLogger.error({ err: error, agentId, path: requestedPath }, `Failed to issue download token for agent ${agentId}`);
3592
3632
  this.emit({
3593
- type: "file_download_token_response",
3633
+ type: 'file_download_token_response',
3594
3634
  payload: {
3595
3635
  agentId,
3596
3636
  path: requestedPath,
@@ -3629,7 +3669,7 @@ export class Session {
3629
3669
  async resolveAgentIdentifier(identifier) {
3630
3670
  const trimmed = identifier.trim();
3631
3671
  if (!trimmed) {
3632
- return { ok: false, error: "Agent identifier cannot be empty" };
3672
+ return { ok: false, error: 'Agent identifier cannot be empty' };
3633
3673
  }
3634
3674
  const stored = await this.agentStorage.list();
3635
3675
  const storedRecords = stored.filter((record) => !record.internal);
@@ -3653,7 +3693,7 @@ export class Session {
3653
3693
  error: `Agent identifier "${trimmed}" is ambiguous (${prefixMatches
3654
3694
  .slice(0, 5)
3655
3695
  .map((id) => id.slice(0, 8))
3656
- .join(", ")}${prefixMatches.length > 5 ? ", …" : ""})`,
3696
+ .join(', ')}${prefixMatches.length > 5 ? ', …' : ''})`,
3657
3697
  };
3658
3698
  }
3659
3699
  const titleMatches = storedRecords.filter((record) => record.title === trimmed);
@@ -3666,7 +3706,7 @@ export class Session {
3666
3706
  error: `Agent title "${trimmed}" is ambiguous (${titleMatches
3667
3707
  .slice(0, 5)
3668
3708
  .map((r) => r.id.slice(0, 8))
3669
- .join(", ")}${titleMatches.length > 5 ? ", …" : ""})`,
3709
+ .join(', ')}${titleMatches.length > 5 ? ', …' : ''})`,
3670
3710
  };
3671
3711
  }
3672
3712
  return { ok: false, error: `Agent not found: ${trimmed}` };
@@ -3683,9 +3723,7 @@ export class Session {
3683
3723
  return this.buildStoredAgentPayload(record);
3684
3724
  }
3685
3725
  normalizeFetchAgentsSort(sort) {
3686
- const fallback = [
3687
- { key: "updated_at", direction: "desc" },
3688
- ];
3726
+ const fallback = [{ key: 'updated_at', direction: 'desc' }];
3689
3727
  if (!sort || sort.length === 0) {
3690
3728
  return fallback;
3691
3729
  }
@@ -3703,30 +3741,30 @@ export class Session {
3703
3741
  getStatusPriority(agent) {
3704
3742
  const requiresAttention = agent.requiresAttention ?? false;
3705
3743
  const attentionReason = agent.attentionReason ?? null;
3706
- if (requiresAttention && attentionReason === "permission") {
3744
+ if (requiresAttention && attentionReason === 'permission') {
3707
3745
  return 0;
3708
3746
  }
3709
- if (agent.status === "error" || attentionReason === "error") {
3747
+ if (agent.status === 'error' || attentionReason === 'error') {
3710
3748
  return 1;
3711
3749
  }
3712
- if (agent.status === "running") {
3750
+ if (agent.status === 'running') {
3713
3751
  return 2;
3714
3752
  }
3715
- if (agent.status === "initializing") {
3753
+ if (agent.status === 'initializing') {
3716
3754
  return 3;
3717
3755
  }
3718
3756
  return 4;
3719
3757
  }
3720
3758
  getFetchAgentsSortValue(entry, key) {
3721
3759
  switch (key) {
3722
- case "status_priority":
3760
+ case 'status_priority':
3723
3761
  return this.getStatusPriority(entry.agent);
3724
- case "created_at":
3762
+ case 'created_at':
3725
3763
  return Date.parse(entry.agent.createdAt);
3726
- case "updated_at":
3764
+ case 'updated_at':
3727
3765
  return Date.parse(entry.agent.updatedAt);
3728
- case "title":
3729
- return entry.agent.title?.toLocaleLowerCase() ?? "";
3766
+ case 'title':
3767
+ return entry.agent.title?.toLocaleLowerCase() ?? '';
3730
3768
  }
3731
3769
  }
3732
3770
  compareSortValues(left, right) {
@@ -3739,7 +3777,7 @@ export class Session {
3739
3777
  if (right === null) {
3740
3778
  return 1;
3741
3779
  }
3742
- if (typeof left === "number" && typeof right === "number") {
3780
+ if (typeof left === 'number' && typeof right === 'number') {
3743
3781
  return left < right ? -1 : 1;
3744
3782
  }
3745
3783
  return String(left).localeCompare(String(right));
@@ -3752,7 +3790,7 @@ export class Session {
3752
3790
  if (base === 0) {
3753
3791
  continue;
3754
3792
  }
3755
- return spec.direction === "asc" ? base : -base;
3793
+ return spec.direction === 'asc' ? base : -base;
3756
3794
  }
3757
3795
  return left.agent.id.localeCompare(right.agent.id);
3758
3796
  }
@@ -3765,49 +3803,48 @@ export class Session {
3765
3803
  sort,
3766
3804
  values,
3767
3805
  id: entry.agent.id,
3768
- }), "utf8").toString("base64url");
3806
+ }), 'utf8').toString('base64url');
3769
3807
  }
3770
3808
  decodeFetchAgentsCursor(cursor, sort) {
3771
3809
  let parsed;
3772
3810
  try {
3773
- parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
3811
+ parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
3774
3812
  }
3775
3813
  catch {
3776
- throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3814
+ throw new SessionRequestError('invalid_cursor', 'Invalid fetch_agents cursor');
3777
3815
  }
3778
- if (!parsed || typeof parsed !== "object") {
3779
- 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');
3780
3818
  }
3781
3819
  const payload = parsed;
3782
- if (!Array.isArray(payload.sort) || typeof payload.id !== "string") {
3783
- 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');
3784
3822
  }
3785
- if (!payload.values || typeof payload.values !== "object") {
3786
- 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');
3787
3825
  }
3788
3826
  const cursorSort = [];
3789
3827
  for (const item of payload.sort) {
3790
3828
  if (!item ||
3791
- typeof item !== "object" ||
3792
- typeof item.key !== "string" ||
3793
- typeof item.direction !== "string") {
3794
- 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');
3795
3833
  }
3796
3834
  const key = item.key;
3797
3835
  const direction = item.direction;
3798
- if ((key !== "status_priority" &&
3799
- key !== "created_at" &&
3800
- key !== "updated_at" &&
3801
- key !== "title") ||
3802
- (direction !== "asc" && direction !== "desc")) {
3803
- 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');
3804
3842
  }
3805
3843
  cursorSort.push({ key, direction });
3806
3844
  }
3807
3845
  if (cursorSort.length !== sort.length ||
3808
- cursorSort.some((entry, index) => entry.key !== sort[index]?.key ||
3809
- entry.direction !== sort[index]?.direction)) {
3810
- 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');
3811
3848
  }
3812
3849
  return {
3813
3850
  sort: cursorSort,
@@ -3818,12 +3855,12 @@ export class Session {
3818
3855
  compareEntryWithCursor(entry, cursor, sort) {
3819
3856
  for (const spec of sort) {
3820
3857
  const leftValue = this.getFetchAgentsSortValue(entry, spec.key);
3821
- 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;
3822
3859
  const base = this.compareSortValues(leftValue, rightValue);
3823
3860
  if (base === 0) {
3824
3861
  continue;
3825
3862
  }
3826
- return spec.direction === "asc" ? base : -base;
3863
+ return spec.direction === 'asc' ? base : -base;
3827
3864
  }
3828
3865
  return entry.agent.id.localeCompare(cursor.id);
3829
3866
  }
@@ -3898,28 +3935,26 @@ export class Session {
3898
3935
  }
3899
3936
  }
3900
3937
  this.emit({
3901
- type: "fetch_agents_response",
3938
+ type: 'fetch_agents_response',
3902
3939
  payload: {
3903
3940
  requestId: request.requestId,
3904
3941
  ...(subscriptionId ? { subscriptionId } : {}),
3905
3942
  ...payload,
3906
3943
  },
3907
3944
  });
3908
- if (subscriptionId &&
3909
- this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
3945
+ if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
3910
3946
  this.flushBootstrappedAgentUpdates({ snapshotUpdatedAtByAgentId });
3911
3947
  }
3912
3948
  }
3913
3949
  catch (error) {
3914
- if (subscriptionId &&
3915
- this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
3950
+ if (subscriptionId && this.agentUpdatesSubscription?.subscriptionId === subscriptionId) {
3916
3951
  this.agentUpdatesSubscription = null;
3917
3952
  }
3918
- const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
3919
- const message = error instanceof Error ? error.message : "Failed to fetch agents";
3920
- this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
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');
3921
3956
  this.emit({
3922
- type: "rpc_error",
3957
+ type: 'rpc_error',
3923
3958
  payload: {
3924
3959
  requestId: request.requestId,
3925
3960
  requestType: request.type,
@@ -3933,7 +3968,7 @@ export class Session {
3933
3968
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
3934
3969
  if (!resolved.ok) {
3935
3970
  this.emit({
3936
- type: "fetch_agent_response",
3971
+ type: 'fetch_agent_response',
3937
3972
  payload: { requestId, agent: null, error: resolved.error },
3938
3973
  });
3939
3974
  return;
@@ -3941,20 +3976,20 @@ export class Session {
3941
3976
  const agent = await this.getAgentPayloadById(resolved.agentId);
3942
3977
  if (!agent) {
3943
3978
  this.emit({
3944
- type: "fetch_agent_response",
3979
+ type: 'fetch_agent_response',
3945
3980
  payload: { requestId, agent: null, error: `Agent not found: ${resolved.agentId}` },
3946
3981
  });
3947
3982
  return;
3948
3983
  }
3949
3984
  this.emit({
3950
- type: "fetch_agent_response",
3985
+ type: 'fetch_agent_response',
3951
3986
  payload: { requestId, agent, error: null },
3952
3987
  });
3953
3988
  }
3954
3989
  async handleFetchAgentTimelineRequest(msg) {
3955
- const direction = msg.direction ?? (msg.cursor ? "after" : "tail");
3956
- const projection = msg.projection ?? "projected";
3957
- 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);
3958
3993
  const cursor = msg.cursor
3959
3994
  ? {
3960
3995
  epoch: msg.cursor.epoch,
@@ -3971,14 +4006,10 @@ export class Session {
3971
4006
  const projected = projectTimelineRows(timeline.rows, snapshot.provider, projection);
3972
4007
  const firstRow = timeline.rows[0];
3973
4008
  const lastRow = timeline.rows[timeline.rows.length - 1];
3974
- const startCursor = firstRow
3975
- ? { epoch: timeline.epoch, seq: firstRow.seq }
3976
- : null;
3977
- const endCursor = lastRow
3978
- ? { epoch: timeline.epoch, seq: lastRow.seq }
3979
- : null;
4009
+ const startCursor = firstRow ? { epoch: timeline.epoch, seq: firstRow.seq } : null;
4010
+ const endCursor = lastRow ? { epoch: timeline.epoch, seq: lastRow.seq } : null;
3980
4011
  this.emit({
3981
- type: "fetch_agent_timeline_response",
4012
+ type: 'fetch_agent_timeline_response',
3982
4013
  payload: {
3983
4014
  requestId: msg.requestId,
3984
4015
  agentId: msg.agentId,
@@ -3999,15 +4030,15 @@ export class Session {
3999
4030
  });
4000
4031
  }
4001
4032
  catch (error) {
4002
- 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');
4003
4034
  this.emit({
4004
- type: "fetch_agent_timeline_response",
4035
+ type: 'fetch_agent_timeline_response',
4005
4036
  payload: {
4006
4037
  requestId: msg.requestId,
4007
4038
  agentId: msg.agentId,
4008
4039
  direction,
4009
4040
  projection,
4010
- epoch: "",
4041
+ epoch: '',
4011
4042
  reset: false,
4012
4043
  staleCursor: false,
4013
4044
  gap: false,
@@ -4026,7 +4057,7 @@ export class Session {
4026
4057
  const resolved = await this.resolveAgentIdentifier(msg.agentId);
4027
4058
  if (!resolved.ok) {
4028
4059
  this.emit({
4029
- type: "send_agent_message_response",
4060
+ type: 'send_agent_message_response',
4030
4061
  payload: {
4031
4062
  requestId: msg.requestId,
4032
4063
  agentId: msg.agentId,
@@ -4047,13 +4078,13 @@ export class Session {
4047
4078
  });
4048
4079
  }
4049
4080
  catch (error) {
4050
- 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');
4051
4082
  }
4052
4083
  const prompt = this.buildAgentPrompt(msg.text, msg.images);
4053
4084
  const started = this.startAgentStream(agentId, prompt);
4054
4085
  if (!started.ok) {
4055
4086
  this.emit({
4056
- type: "send_agent_message_response",
4087
+ type: 'send_agent_message_response',
4057
4088
  payload: {
4058
4089
  requestId: msg.requestId,
4059
4090
  agentId,
@@ -4065,14 +4096,18 @@ export class Session {
4065
4096
  }
4066
4097
  const startAbort = new AbortController();
4067
4098
  const startTimeoutMs = 15000;
4068
- const startTimeout = setTimeout(() => startAbort.abort("timeout"), startTimeoutMs);
4099
+ const startTimeout = setTimeout(() => startAbort.abort('timeout'), startTimeoutMs);
4069
4100
  try {
4070
4101
  await this.agentManager.waitForAgentRunStart(agentId, { signal: startAbort.signal });
4071
4102
  }
4072
4103
  catch (error) {
4073
- 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';
4074
4109
  this.emit({
4075
- type: "send_agent_message_response",
4110
+ type: 'send_agent_message_response',
4076
4111
  payload: {
4077
4112
  requestId: msg.requestId,
4078
4113
  agentId,
@@ -4086,7 +4121,7 @@ export class Session {
4086
4121
  clearTimeout(startTimeout);
4087
4122
  }
4088
4123
  this.emit({
4089
- type: "send_agent_message_response",
4124
+ type: 'send_agent_message_response',
4090
4125
  payload: {
4091
4126
  requestId: msg.requestId,
4092
4127
  agentId,
@@ -4096,9 +4131,9 @@ export class Session {
4096
4131
  });
4097
4132
  }
4098
4133
  catch (error) {
4099
- 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';
4100
4135
  this.emit({
4101
- type: "send_agent_message_response",
4136
+ type: 'send_agent_message_response',
4102
4137
  payload: {
4103
4138
  requestId: msg.requestId,
4104
4139
  agentId: resolved.agentId,
@@ -4112,8 +4147,14 @@ export class Session {
4112
4147
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
4113
4148
  if (!resolved.ok) {
4114
4149
  this.emit({
4115
- type: "wait_for_finish_response",
4116
- 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
+ },
4117
4158
  });
4118
4159
  return;
4119
4160
  }
@@ -4123,10 +4164,10 @@ export class Session {
4123
4164
  const record = await this.agentStorage.get(agentId);
4124
4165
  if (!record || record.internal) {
4125
4166
  this.emit({
4126
- type: "wait_for_finish_response",
4167
+ type: 'wait_for_finish_response',
4127
4168
  payload: {
4128
4169
  requestId,
4129
- status: "error",
4170
+ status: 'error',
4130
4171
  final: null,
4131
4172
  error: `Agent not found: ${agentId}`,
4132
4173
  lastMessage: null,
@@ -4135,13 +4176,13 @@ export class Session {
4135
4176
  return;
4136
4177
  }
4137
4178
  const final = this.buildStoredAgentPayload(record);
4138
- const status = record.attentionReason === "permission"
4139
- ? "permission"
4140
- : record.lastStatus === "error"
4141
- ? "error"
4142
- : "idle";
4179
+ const status = record.attentionReason === 'permission'
4180
+ ? 'permission'
4181
+ : record.lastStatus === 'error'
4182
+ ? 'error'
4183
+ : 'idle';
4143
4184
  this.emit({
4144
- type: "wait_for_finish_response",
4185
+ type: 'wait_for_finish_response',
4145
4186
  payload: { requestId, status, final, error: null, lastMessage: null },
4146
4187
  });
4147
4188
  return;
@@ -4149,7 +4190,7 @@ export class Session {
4149
4190
  const abortController = new AbortController();
4150
4191
  const effectiveTimeoutMs = timeoutMs ?? 600000; // 10 minutes default
4151
4192
  const timeoutHandle = setTimeout(() => {
4152
- abortController.abort("timeout");
4193
+ abortController.abort('timeout');
4153
4194
  }, effectiveTimeoutMs);
4154
4195
  try {
4155
4196
  const result = await this.agentManager.waitForAgentEvent(agentId, {
@@ -4159,28 +4200,28 @@ export class Session {
4159
4200
  if (!final) {
4160
4201
  throw new Error(`Agent ${agentId} disappeared while waiting`);
4161
4202
  }
4162
- const status = result.permission
4163
- ? "permission"
4164
- : result.status === "error"
4165
- ? "error"
4166
- : "idle";
4203
+ const status = result.permission ? 'permission' : result.status === 'error' ? 'error' : 'idle';
4167
4204
  this.emit({
4168
- type: "wait_for_finish_response",
4205
+ type: 'wait_for_finish_response',
4169
4206
  payload: { requestId, status, final, error: null, lastMessage: result.lastMessage },
4170
4207
  });
4171
4208
  }
4172
4209
  catch (error) {
4173
4210
  const isAbort = error instanceof Error &&
4174
- (error.name === "AbortError" || error.message.toLowerCase().includes("aborted"));
4211
+ (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted'));
4175
4212
  if (!isAbort) {
4176
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
4177
- 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');
4178
4219
  const final = await this.getAgentPayloadById(agentId);
4179
4220
  this.emit({
4180
- type: "wait_for_finish_response",
4221
+ type: 'wait_for_finish_response',
4181
4222
  payload: {
4182
4223
  requestId,
4183
- status: "error",
4224
+ status: 'error',
4184
4225
  final,
4185
4226
  error: message,
4186
4227
  lastMessage: null,
@@ -4193,8 +4234,8 @@ export class Session {
4193
4234
  throw new Error(`Agent ${agentId} disappeared while waiting`);
4194
4235
  }
4195
4236
  this.emit({
4196
- type: "wait_for_finish_response",
4197
- 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 },
4198
4239
  });
4199
4240
  }
4200
4241
  finally {
@@ -4206,24 +4247,24 @@ export class Session {
4206
4247
  */
4207
4248
  async handleAudioChunk(msg) {
4208
4249
  if (!this.isVoiceMode) {
4209
- 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');
4210
4251
  }
4211
4252
  await this.handleVoiceSpeechStart();
4212
- const chunkFormat = msg.format || "audio/wav";
4253
+ const chunkFormat = msg.format || 'audio/wav';
4213
4254
  if (this.isVoiceMode) {
4214
4255
  await this.appendToActiveVoiceDictationStream(msg.audio, chunkFormat);
4215
4256
  if (!msg.isLast) {
4216
4257
  this.setVoiceModeInactivityTimeout();
4217
- this.sessionLogger.debug("Voice mode: streaming chunk, waiting for speech end");
4258
+ this.sessionLogger.debug('Voice mode: streaming chunk, waiting for speech end');
4218
4259
  return;
4219
4260
  }
4220
4261
  this.clearVoiceModeInactivityTimeout();
4221
- this.sessionLogger.debug("Voice mode: speech ended, finalizing streaming transcription");
4222
- await this.finalizeActiveVoiceDictationStream("speech ended");
4262
+ this.sessionLogger.debug('Voice mode: speech ended, finalizing streaming transcription');
4263
+ await this.finalizeActiveVoiceDictationStream('speech ended');
4223
4264
  return;
4224
4265
  }
4225
- const chunkBuffer = Buffer.from(msg.audio, "base64");
4226
- const isPCMChunk = chunkFormat.toLowerCase().includes("pcm");
4266
+ const chunkBuffer = Buffer.from(msg.audio, 'base64');
4267
+ const isPCMChunk = chunkFormat.toLowerCase().includes('pcm');
4227
4268
  if (!this.audioBuffer) {
4228
4269
  this.audioBuffer = {
4229
4270
  chunks: [],
@@ -4234,7 +4275,10 @@ export class Session {
4234
4275
  }
4235
4276
  // If the format changes mid-stream, flush what we have first
4236
4277
  if (this.audioBuffer.isPCM !== isPCMChunk) {
4237
- 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`);
4238
4282
  const finalized = this.finalizeBufferedAudio();
4239
4283
  if (finalized) {
4240
4284
  await this.processCompletedAudio(finalized.audio, finalized.format);
@@ -4267,7 +4311,10 @@ export class Session {
4267
4311
  return;
4268
4312
  }
4269
4313
  if (!msg.isLast && reachedStreamingThreshold) {
4270
- 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`);
4271
4318
  }
4272
4319
  else {
4273
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))`);
@@ -4285,7 +4332,7 @@ export class Session {
4285
4332
  const wavBuffer = convertPCMToWavBuffer(pcmBuffer, PCM_SAMPLE_RATE, PCM_CHANNELS, PCM_BITS_PER_SAMPLE);
4286
4333
  return {
4287
4334
  audio: wavBuffer,
4288
- format: "audio/wav",
4335
+ format: 'audio/wav',
4289
4336
  };
4290
4337
  }
4291
4338
  return {
@@ -4294,8 +4341,7 @@ export class Session {
4294
4341
  };
4295
4342
  }
4296
4343
  async processCompletedAudio(audio, format) {
4297
- const shouldBuffer = this.processingPhase === "transcribing" &&
4298
- this.pendingAudioSegments.length === 0;
4344
+ const shouldBuffer = this.processingPhase === 'transcribing' && this.pendingAudioSegments.length === 0;
4299
4345
  if (shouldBuffer) {
4300
4346
  this.sessionLogger.debug({ phase: this.processingPhase }, `Buffering audio segment (phase: ${this.processingPhase})`);
4301
4347
  this.pendingAudioSegments.push({
@@ -4325,21 +4371,21 @@ export class Session {
4325
4371
  * Process audio through STT and then LLM
4326
4372
  */
4327
4373
  async processAudio(audio, format) {
4328
- this.setPhase("transcribing");
4374
+ this.setPhase('transcribing');
4329
4375
  this.emit({
4330
- type: "activity_log",
4376
+ type: 'activity_log',
4331
4377
  payload: {
4332
4378
  id: uuidv4(),
4333
4379
  timestamp: new Date(),
4334
- type: "system",
4335
- content: "Transcribing audio...",
4380
+ type: 'system',
4381
+ content: 'Transcribing audio...',
4336
4382
  },
4337
4383
  });
4338
4384
  try {
4339
4385
  const requestId = uuidv4();
4340
4386
  const result = await this.sttManager.transcribe(audio, format, {
4341
4387
  requestId,
4342
- label: this.isVoiceMode ? "voice" : "buffered",
4388
+ label: this.isVoiceMode ? 'voice' : 'buffered',
4343
4389
  });
4344
4390
  const transcriptText = result.text.trim();
4345
4391
  this.sessionLogger.info({
@@ -4347,7 +4393,7 @@ export class Session {
4347
4393
  isVoiceMode: this.isVoiceMode,
4348
4394
  transcriptLength: transcriptText.length,
4349
4395
  transcript: transcriptText,
4350
- }, "Transcription result");
4396
+ }, 'Transcription result');
4351
4397
  await this.handleTranscriptionResultPayload({
4352
4398
  text: result.text,
4353
4399
  language: result.language,
@@ -4361,14 +4407,14 @@ export class Session {
4361
4407
  });
4362
4408
  }
4363
4409
  catch (error) {
4364
- this.setPhase("idle");
4365
- this.clearSpeechInProgress("transcription error");
4410
+ this.setPhase('idle');
4411
+ this.clearSpeechInProgress('transcription error');
4366
4412
  this.emit({
4367
- type: "activity_log",
4413
+ type: 'activity_log',
4368
4414
  payload: {
4369
4415
  id: uuidv4(),
4370
4416
  timestamp: new Date(),
4371
- type: "error",
4417
+ type: 'error',
4372
4418
  content: `Transcription error: ${error.message}`,
4373
4419
  },
4374
4420
  });
@@ -4378,34 +4424,36 @@ export class Session {
4378
4424
  async handleTranscriptionResultPayload(result) {
4379
4425
  const transcriptText = result.text.trim();
4380
4426
  this.emit({
4381
- type: "transcription_result",
4427
+ type: 'transcription_result',
4382
4428
  payload: {
4383
4429
  text: result.text,
4384
4430
  ...(result.language ? { language: result.language } : {}),
4385
4431
  ...(result.duration !== undefined ? { duration: result.duration } : {}),
4386
4432
  requestId: result.requestId,
4387
4433
  ...(result.avgLogprob !== undefined ? { avgLogprob: result.avgLogprob } : {}),
4388
- ...(result.isLowConfidence !== undefined ? { isLowConfidence: result.isLowConfidence } : {}),
4434
+ ...(result.isLowConfidence !== undefined
4435
+ ? { isLowConfidence: result.isLowConfidence }
4436
+ : {}),
4389
4437
  ...(result.byteLength !== undefined ? { byteLength: result.byteLength } : {}),
4390
4438
  ...(result.format ? { format: result.format } : {}),
4391
4439
  ...(result.debugRecordingPath ? { debugRecordingPath: result.debugRecordingPath } : {}),
4392
4440
  },
4393
4441
  });
4394
4442
  if (!transcriptText) {
4395
- this.sessionLogger.debug("Empty transcription (false positive), not aborting");
4396
- this.setPhase("idle");
4397
- this.clearSpeechInProgress("empty transcription");
4443
+ this.sessionLogger.debug('Empty transcription (false positive), not aborting');
4444
+ this.setPhase('idle');
4445
+ this.clearSpeechInProgress('empty transcription');
4398
4446
  return;
4399
4447
  }
4400
4448
  // Has content - abort any in-progress stream now
4401
4449
  this.createAbortController();
4402
4450
  if (result.debugRecordingPath) {
4403
4451
  this.emit({
4404
- type: "activity_log",
4452
+ type: 'activity_log',
4405
4453
  payload: {
4406
4454
  id: uuidv4(),
4407
4455
  timestamp: new Date(),
4408
- type: "system",
4456
+ type: 'system',
4409
4457
  content: `Saved input audio: ${result.debugRecordingPath}`,
4410
4458
  metadata: {
4411
4459
  recordingPath: result.debugRecordingPath,
@@ -4416,11 +4464,11 @@ export class Session {
4416
4464
  });
4417
4465
  }
4418
4466
  this.emit({
4419
- type: "activity_log",
4467
+ type: 'activity_log',
4420
4468
  payload: {
4421
4469
  id: uuidv4(),
4422
4470
  timestamp: new Date(),
4423
- type: "transcript",
4471
+ type: 'transcript',
4424
4472
  content: result.text,
4425
4473
  metadata: {
4426
4474
  ...(result.language ? { language: result.language } : {}),
@@ -4428,15 +4476,15 @@ export class Session {
4428
4476
  },
4429
4477
  },
4430
4478
  });
4431
- this.clearSpeechInProgress("transcription complete");
4432
- this.setPhase("idle");
4479
+ this.clearSpeechInProgress('transcription complete');
4480
+ this.setPhase('idle');
4433
4481
  if (!this.isVoiceMode) {
4434
- 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');
4435
4483
  return;
4436
4484
  }
4437
4485
  const agentId = this.voiceModeAgentId;
4438
4486
  if (!agentId) {
4439
- 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');
4440
4488
  return;
4441
4489
  }
4442
4490
  // Route voice utterances through the same send path as regular text input:
@@ -4449,22 +4497,22 @@ export class Session {
4449
4497
  agentId,
4450
4498
  textLength: text.length,
4451
4499
  preview: text.slice(0, 160),
4452
- }, "Voice speak tool call received by session handler");
4500
+ }, 'Voice speak tool call received by session handler');
4453
4501
  const abortSignal = signal ?? this.abortController.signal;
4454
4502
  await this.ttsManager.generateAndWaitForPlayback(text, (msg) => this.emit(msg), abortSignal, true);
4455
- 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');
4456
4504
  this.emit({
4457
- type: "activity_log",
4505
+ type: 'activity_log',
4458
4506
  payload: {
4459
4507
  id: uuidv4(),
4460
4508
  timestamp: new Date(),
4461
- type: "assistant",
4509
+ type: 'assistant',
4462
4510
  content: text,
4463
4511
  },
4464
4512
  });
4465
4513
  });
4466
4514
  this.registerVoiceCallerContext?.(agentId, {
4467
- childAgentDefaultLabels: { ui: "true" },
4515
+ childAgentDefaultLabels: { ui: 'true' },
4468
4516
  allowCustomCwd: false,
4469
4517
  enableVoiceTools: true,
4470
4518
  });
@@ -4475,24 +4523,24 @@ export class Session {
4475
4523
  async handleAbort() {
4476
4524
  this.sessionLogger.info({ phase: this.processingPhase }, `Abort request, phase: ${this.processingPhase}`);
4477
4525
  this.abortController.abort();
4478
- this.ttsManager.cancelPendingPlaybacks("abort request");
4526
+ this.ttsManager.cancelPendingPlaybacks('abort request');
4479
4527
  // Voice abort should always interrupt active agent output immediately.
4480
4528
  if (this.isVoiceMode && this.voiceModeAgentId) {
4481
4529
  try {
4482
4530
  await this.interruptAgentIfRunning(this.voiceModeAgentId);
4483
4531
  }
4484
4532
  catch (error) {
4485
- 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');
4486
4534
  }
4487
4535
  }
4488
- if (this.processingPhase === "transcribing") {
4536
+ if (this.processingPhase === 'transcribing') {
4489
4537
  // Still in STT phase - we'll buffer the next audio
4490
- this.sessionLogger.debug("Will buffer next audio (currently transcribing)");
4538
+ this.sessionLogger.debug('Will buffer next audio (currently transcribing)');
4491
4539
  // Phase stays as 'transcribing', handleAudioChunk will handle buffering
4492
4540
  return;
4493
4541
  }
4494
4542
  // Reset phase to idle and clear pending non-voice buffers.
4495
- this.setPhase("idle");
4543
+ this.setPhase('idle');
4496
4544
  this.pendingAudioSegments = [];
4497
4545
  this.clearBufferTimeout();
4498
4546
  }
@@ -4513,24 +4561,22 @@ export class Session {
4513
4561
  const phaseBeforeAbort = this.processingPhase;
4514
4562
  const hadActiveStream = this.hasActiveAgentRun(this.voiceModeAgentId);
4515
4563
  this.speechInProgress = true;
4516
- 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');
4517
4565
  if (this.pendingAudioSegments.length > 0) {
4518
4566
  this.sessionLogger.debug({ segmentCount: this.pendingAudioSegments.length }, `Dropping ${this.pendingAudioSegments.length} buffered audio segment(s) due to voice speech`);
4519
4567
  this.pendingAudioSegments = [];
4520
4568
  }
4521
4569
  if (this.audioBuffer) {
4522
- 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
4523
- ? `, ${this.audioBuffer.totalPCMBytes} PCM bytes`
4524
- : ""})`);
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` : ''})`);
4525
4571
  this.audioBuffer = null;
4526
4572
  }
4527
- this.cancelActiveVoiceDictationStream("new speech turn started");
4573
+ this.cancelActiveVoiceDictationStream('new speech turn started');
4528
4574
  this.clearVoiceModeInactivityTimeout();
4529
4575
  this.clearBufferTimeout();
4530
4576
  this.abortController.abort();
4531
4577
  await this.handleAbort();
4532
4578
  const latencyMs = Date.now() - chunkReceivedAt;
4533
- 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');
4534
4580
  }
4535
4581
  /**
4536
4582
  * Clear speech-in-progress flag once the user turn has completed
@@ -4564,7 +4610,7 @@ export class Session {
4564
4610
  setBufferTimeout() {
4565
4611
  this.clearBufferTimeout();
4566
4612
  this.bufferTimeout = setTimeout(async () => {
4567
- this.sessionLogger.debug("Buffer timeout reached, processing pending segments");
4613
+ this.sessionLogger.debug('Buffer timeout reached, processing pending segments');
4568
4614
  if (this.pendingAudioSegments.length > 0) {
4569
4615
  const segments = [...this.pendingAudioSegments];
4570
4616
  this.pendingAudioSegments = [];
@@ -4588,9 +4634,9 @@ export class Session {
4588
4634
  timeoutMs: VOICE_MODE_INACTIVITY_FLUSH_MS,
4589
4635
  dictationId: this.activeVoiceDictationId,
4590
4636
  nextSeq: this.activeVoiceDictationNextSeq,
4591
- }, "Voice mode inactivity timeout reached without isLast; finalizing active voice dictation stream");
4592
- void this.finalizeActiveVoiceDictationStream("inactivity timeout").catch((error) => {
4593
- 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');
4594
4640
  });
4595
4641
  }, VOICE_MODE_INACTIVITY_FLUSH_MS);
4596
4642
  }
@@ -4613,15 +4659,15 @@ export class Session {
4613
4659
  * Emit a message to the client
4614
4660
  */
4615
4661
  emit(msg) {
4616
- if (msg.type === "audio_output" &&
4662
+ if (msg.type === 'audio_output' &&
4617
4663
  (process.env.TTS_DEBUG_AUDIO_DIR || isPaseoDictationDebugEnabled()) &&
4618
4664
  msg.payload.groupId &&
4619
- typeof msg.payload.audio === "string") {
4665
+ typeof msg.payload.audio === 'string') {
4620
4666
  const groupId = msg.payload.groupId;
4621
4667
  const existing = this.ttsDebugStreams.get(groupId) ??
4622
4668
  { format: msg.payload.format, chunks: [] };
4623
4669
  try {
4624
- existing.chunks.push(Buffer.from(msg.payload.audio, "base64"));
4670
+ existing.chunks.push(Buffer.from(msg.payload.audio, 'base64'));
4625
4671
  existing.format = msg.payload.format;
4626
4672
  this.ttsDebugStreams.set(groupId, existing);
4627
4673
  }
@@ -4636,11 +4682,11 @@ export class Session {
4636
4682
  const recordingPath = await maybePersistTtsDebugAudio(Buffer.concat(final.chunks), { sessionId: this.sessionId, groupId, format: final.format }, this.sessionLogger);
4637
4683
  if (recordingPath) {
4638
4684
  this.onMessage({
4639
- type: "activity_log",
4685
+ type: 'activity_log',
4640
4686
  payload: {
4641
4687
  id: uuidv4(),
4642
4688
  timestamp: new Date(),
4643
- type: "system",
4689
+ type: 'system',
4644
4690
  content: `Saved TTS audio: ${recordingPath}`,
4645
4691
  metadata: { recordingPath, format: final.format, groupId },
4646
4692
  },
@@ -4660,14 +4706,14 @@ export class Session {
4660
4706
  this.onBinaryMessage(frame);
4661
4707
  }
4662
4708
  catch (error) {
4663
- this.sessionLogger.error({ err: error }, "Failed to emit binary frame");
4709
+ this.sessionLogger.error({ err: error }, 'Failed to emit binary frame');
4664
4710
  }
4665
4711
  }
4666
4712
  /**
4667
4713
  * Clean up session resources
4668
4714
  */
4669
4715
  async cleanup() {
4670
- this.sessionLogger.trace("Cleaning up");
4716
+ this.sessionLogger.trace('Cleaning up');
4671
4717
  if (this.unsubscribeAgentEvents) {
4672
4718
  this.unsubscribeAgentEvents();
4673
4719
  this.unsubscribeAgentEvents = null;
@@ -4678,7 +4724,7 @@ export class Session {
4678
4724
  this.clearVoiceModeInactivityTimeout();
4679
4725
  this.clearBufferTimeout();
4680
4726
  // Clear buffers
4681
- this.cancelActiveVoiceDictationStream("session cleanup");
4727
+ this.cancelActiveVoiceDictationStream('session cleanup');
4682
4728
  this.pendingAudioSegments = [];
4683
4729
  this.audioBuffer = null;
4684
4730
  // Cleanup managers
@@ -4690,10 +4736,10 @@ export class Session {
4690
4736
  if (this.agentMcpClient) {
4691
4737
  try {
4692
4738
  await this.agentMcpClient.close();
4693
- this.sessionLogger.debug("Agent MCP client closed");
4739
+ this.sessionLogger.debug('Agent MCP client closed');
4694
4740
  }
4695
4741
  catch (error) {
4696
- this.sessionLogger.error({ err: error }, "Failed to close Agent MCP client");
4742
+ this.sessionLogger.error({ err: error }, 'Failed to close Agent MCP client');
4697
4743
  }
4698
4744
  this.agentMcpClient = null;
4699
4745
  this.agentTools = null;
@@ -4745,18 +4791,18 @@ export class Session {
4745
4791
  unsubscribe();
4746
4792
  }
4747
4793
  catch (error) {
4748
- 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');
4749
4795
  }
4750
4796
  this.terminalSubscriptions.delete(terminalId);
4751
4797
  }
4752
4798
  const streamId = this.terminalStreamByTerminalId.get(terminalId);
4753
- if (typeof streamId === "number") {
4799
+ if (typeof streamId === 'number') {
4754
4800
  this.detachTerminalStream(streamId, { emitExit: true });
4755
4801
  }
4756
4802
  }
4757
4803
  emitTerminalsChangedSnapshot(input) {
4758
4804
  this.emit({
4759
- type: "terminals_changed",
4805
+ type: 'terminals_changed',
4760
4806
  payload: {
4761
4807
  cwd: input.cwd,
4762
4808
  terminals: input.terminals,
@@ -4786,9 +4832,7 @@ export class Session {
4786
4832
  if (!this.terminalManager || !this.subscribedTerminalDirectories.has(cwd)) {
4787
4833
  return;
4788
4834
  }
4789
- const hadDirectoryBeforeSubscribe = this.terminalManager
4790
- .listDirectories()
4791
- .includes(cwd);
4835
+ const hadDirectoryBeforeSubscribe = this.terminalManager.listDirectories().includes(cwd);
4792
4836
  try {
4793
4837
  const terminals = await this.terminalManager.getTerminals(cwd);
4794
4838
  for (const terminal of terminals) {
@@ -4811,13 +4855,13 @@ export class Session {
4811
4855
  });
4812
4856
  }
4813
4857
  catch (error) {
4814
- 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');
4815
4859
  }
4816
4860
  }
4817
4861
  async handleListTerminalsRequest(msg) {
4818
4862
  if (!this.terminalManager) {
4819
4863
  this.emit({
4820
- type: "list_terminals_response",
4864
+ type: 'list_terminals_response',
4821
4865
  payload: {
4822
4866
  cwd: msg.cwd,
4823
4867
  terminals: [],
@@ -4832,7 +4876,7 @@ export class Session {
4832
4876
  this.ensureTerminalExitSubscription(terminal);
4833
4877
  }
4834
4878
  this.emit({
4835
- type: "list_terminals_response",
4879
+ type: 'list_terminals_response',
4836
4880
  payload: {
4837
4881
  cwd: msg.cwd,
4838
4882
  terminals: terminals.map((t) => ({ id: t.id, name: t.name })),
@@ -4841,9 +4885,9 @@ export class Session {
4841
4885
  });
4842
4886
  }
4843
4887
  catch (error) {
4844
- 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');
4845
4889
  this.emit({
4846
- type: "list_terminals_response",
4890
+ type: 'list_terminals_response',
4847
4891
  payload: {
4848
4892
  cwd: msg.cwd,
4849
4893
  terminals: [],
@@ -4855,10 +4899,10 @@ export class Session {
4855
4899
  async handleCreateTerminalRequest(msg) {
4856
4900
  if (!this.terminalManager) {
4857
4901
  this.emit({
4858
- type: "create_terminal_response",
4902
+ type: 'create_terminal_response',
4859
4903
  payload: {
4860
4904
  terminal: null,
4861
- error: "Terminal manager not available",
4905
+ error: 'Terminal manager not available',
4862
4906
  requestId: msg.requestId,
4863
4907
  },
4864
4908
  });
@@ -4871,7 +4915,7 @@ export class Session {
4871
4915
  });
4872
4916
  this.ensureTerminalExitSubscription(session);
4873
4917
  this.emit({
4874
- type: "create_terminal_response",
4918
+ type: 'create_terminal_response',
4875
4919
  payload: {
4876
4920
  terminal: { id: session.id, name: session.name, cwd: session.cwd },
4877
4921
  error: null,
@@ -4880,9 +4924,9 @@ export class Session {
4880
4924
  });
4881
4925
  }
4882
4926
  catch (error) {
4883
- 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');
4884
4928
  this.emit({
4885
- type: "create_terminal_response",
4929
+ type: 'create_terminal_response',
4886
4930
  payload: {
4887
4931
  terminal: null,
4888
4932
  error: error.message,
@@ -4894,11 +4938,11 @@ export class Session {
4894
4938
  async handleSubscribeTerminalRequest(msg) {
4895
4939
  if (!this.terminalManager) {
4896
4940
  this.emit({
4897
- type: "subscribe_terminal_response",
4941
+ type: 'subscribe_terminal_response',
4898
4942
  payload: {
4899
4943
  terminalId: msg.terminalId,
4900
4944
  state: null,
4901
- error: "Terminal manager not available",
4945
+ error: 'Terminal manager not available',
4902
4946
  requestId: msg.requestId,
4903
4947
  },
4904
4948
  });
@@ -4907,11 +4951,11 @@ export class Session {
4907
4951
  const session = this.terminalManager.getTerminal(msg.terminalId);
4908
4952
  if (!session) {
4909
4953
  this.emit({
4910
- type: "subscribe_terminal_response",
4954
+ type: 'subscribe_terminal_response',
4911
4955
  payload: {
4912
4956
  terminalId: msg.terminalId,
4913
4957
  state: null,
4914
- error: "Terminal not found",
4958
+ error: 'Terminal not found',
4915
4959
  requestId: msg.requestId,
4916
4960
  },
4917
4961
  });
@@ -4925,9 +4969,9 @@ export class Session {
4925
4969
  }
4926
4970
  // Subscribe to terminal updates
4927
4971
  const unsubscribe = session.subscribe((serverMsg) => {
4928
- if (serverMsg.type === "full") {
4972
+ if (serverMsg.type === 'full') {
4929
4973
  this.emit({
4930
- type: "terminal_output",
4974
+ type: 'terminal_output',
4931
4975
  payload: {
4932
4976
  terminalId: msg.terminalId,
4933
4977
  state: serverMsg.state,
@@ -4938,7 +4982,7 @@ export class Session {
4938
4982
  this.terminalSubscriptions.set(msg.terminalId, unsubscribe);
4939
4983
  // Send initial state
4940
4984
  this.emit({
4941
- type: "subscribe_terminal_response",
4985
+ type: 'subscribe_terminal_response',
4942
4986
  payload: {
4943
4987
  terminalId: msg.terminalId,
4944
4988
  state: session.getState(),
@@ -4960,16 +5004,55 @@ export class Session {
4960
5004
  }
4961
5005
  const session = this.terminalManager.getTerminal(msg.terminalId);
4962
5006
  if (!session) {
4963
- this.sessionLogger.warn({ terminalId: msg.terminalId }, "Terminal not found for input");
5007
+ this.sessionLogger.warn({ terminalId: msg.terminalId }, 'Terminal not found for input');
4964
5008
  return;
4965
5009
  }
4966
5010
  this.ensureTerminalExitSubscription(session);
4967
5011
  session.send(msg.message);
4968
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
+ }
4969
5052
  async handleKillTerminalRequest(msg) {
4970
5053
  if (!this.terminalManager) {
4971
5054
  this.emit({
4972
- type: "kill_terminal_response",
5055
+ type: 'kill_terminal_response',
4973
5056
  payload: {
4974
5057
  terminalId: msg.terminalId,
4975
5058
  success: false,
@@ -4978,19 +5061,9 @@ export class Session {
4978
5061
  });
4979
5062
  return;
4980
5063
  }
4981
- // Unsubscribe first
4982
- const unsubscribe = this.terminalSubscriptions.get(msg.terminalId);
4983
- if (unsubscribe) {
4984
- unsubscribe();
4985
- this.terminalSubscriptions.delete(msg.terminalId);
4986
- }
4987
- const streamId = this.terminalStreamByTerminalId.get(msg.terminalId);
4988
- if (typeof streamId === "number") {
4989
- this.detachTerminalStream(streamId, { emitExit: true });
4990
- }
4991
- this.terminalManager.killTerminal(msg.terminalId);
5064
+ this.killTrackedTerminal(msg.terminalId, { emitExit: true });
4992
5065
  this.emit({
4993
- type: "kill_terminal_response",
5066
+ type: 'kill_terminal_response',
4994
5067
  payload: {
4995
5068
  terminalId: msg.terminalId,
4996
5069
  success: true,
@@ -5001,7 +5074,7 @@ export class Session {
5001
5074
  async handleAttachTerminalStreamRequest(msg) {
5002
5075
  if (!this.terminalManager || !this.onBinaryMessage) {
5003
5076
  this.emit({
5004
- type: "attach_terminal_stream_response",
5077
+ type: 'attach_terminal_stream_response',
5005
5078
  payload: {
5006
5079
  terminalId: msg.terminalId,
5007
5080
  streamId: null,
@@ -5009,7 +5082,7 @@ export class Session {
5009
5082
  currentOffset: 0,
5010
5083
  earliestAvailableOffset: 0,
5011
5084
  reset: true,
5012
- error: "Terminal streaming not available",
5085
+ error: 'Terminal streaming not available',
5013
5086
  requestId: msg.requestId,
5014
5087
  },
5015
5088
  });
@@ -5018,7 +5091,7 @@ export class Session {
5018
5091
  const session = this.terminalManager.getTerminal(msg.terminalId);
5019
5092
  if (!session) {
5020
5093
  this.emit({
5021
- type: "attach_terminal_stream_response",
5094
+ type: 'attach_terminal_stream_response',
5022
5095
  payload: {
5023
5096
  terminalId: msg.terminalId,
5024
5097
  streamId: null,
@@ -5026,7 +5099,7 @@ export class Session {
5026
5099
  currentOffset: 0,
5027
5100
  earliestAvailableOffset: 0,
5028
5101
  reset: true,
5029
- error: "Terminal not found",
5102
+ error: 'Terminal not found',
5030
5103
  requestId: msg.requestId,
5031
5104
  },
5032
5105
  });
@@ -5035,13 +5108,13 @@ export class Session {
5035
5108
  if (msg.rows || msg.cols) {
5036
5109
  const state = session.getState();
5037
5110
  session.send({
5038
- type: "resize",
5111
+ type: 'resize',
5039
5112
  rows: msg.rows ?? state.rows,
5040
5113
  cols: msg.cols ?? state.cols,
5041
5114
  });
5042
5115
  }
5043
5116
  const existingStreamId = this.terminalStreamByTerminalId.get(msg.terminalId);
5044
- if (typeof existingStreamId === "number") {
5117
+ if (typeof existingStreamId === 'number') {
5045
5118
  this.detachTerminalStream(existingStreamId, { emitExit: false });
5046
5119
  }
5047
5120
  const streamId = this.allocateTerminalStreamId();
@@ -5083,7 +5156,7 @@ export class Session {
5083
5156
  }
5084
5157
  this.flushPendingTerminalStreamChunks(streamId, binding);
5085
5158
  this.emit({
5086
- type: "attach_terminal_stream_response",
5159
+ type: 'attach_terminal_stream_response',
5087
5160
  payload: {
5088
5161
  terminalId: msg.terminalId,
5089
5162
  streamId,
@@ -5103,7 +5176,7 @@ export class Session {
5103
5176
  return chunk.startOffset < binding.lastAckOffset + TERMINAL_STREAM_WINDOW_BYTES;
5104
5177
  }
5105
5178
  emitTerminalStreamChunk(streamId, binding, chunk) {
5106
- const payload = new Uint8Array(Buffer.from(chunk.data, "utf8"));
5179
+ const payload = new Uint8Array(Buffer.from(chunk.data, 'utf8'));
5107
5180
  this.emitBinary({
5108
5181
  channel: BinaryMuxChannel.Terminal,
5109
5182
  messageType: TerminalBinaryMessageType.OutputUtf8,
@@ -5124,7 +5197,7 @@ export class Session {
5124
5197
  pendingChunks: binding.pendingChunks.length,
5125
5198
  pendingBytes: binding.pendingBytes,
5126
5199
  chunkBytes,
5127
- }, "Terminal stream pending buffer overflow; closing stream");
5200
+ }, 'Terminal stream pending buffer overflow; closing stream');
5128
5201
  this.detachTerminalStream(streamId, { emitExit: true });
5129
5202
  return;
5130
5203
  }
@@ -5151,7 +5224,7 @@ export class Session {
5151
5224
  handleDetachTerminalStreamRequest(msg) {
5152
5225
  const success = this.detachTerminalStream(msg.streamId, { emitExit: false });
5153
5226
  this.emit({
5154
- type: "detach_terminal_stream_response",
5227
+ type: 'detach_terminal_stream_response',
5155
5228
  payload: {
5156
5229
  streamId: msg.streamId,
5157
5230
  success,
@@ -5173,7 +5246,7 @@ export class Session {
5173
5246
  binding.unsubscribe();
5174
5247
  }
5175
5248
  catch (error) {
5176
- this.sessionLogger.warn({ err: error, streamId }, "Failed to unsubscribe terminal stream");
5249
+ this.sessionLogger.warn({ err: error, streamId }, 'Failed to unsubscribe terminal stream');
5177
5250
  }
5178
5251
  this.terminalStreams.delete(streamId);
5179
5252
  if (this.terminalStreamByTerminalId.get(binding.terminalId) === streamId) {
@@ -5181,7 +5254,7 @@ export class Session {
5181
5254
  }
5182
5255
  if (options?.emitExit) {
5183
5256
  this.emit({
5184
- type: "terminal_stream_exit",
5257
+ type: 'terminal_stream_exit',
5185
5258
  payload: {
5186
5259
  streamId,
5187
5260
  terminalId: binding.terminalId,
@@ -5204,7 +5277,7 @@ export class Session {
5204
5277
  }
5205
5278
  attempts += 1;
5206
5279
  }
5207
- throw new Error("Unable to allocate terminal stream id");
5280
+ throw new Error('Unable to allocate terminal stream id');
5208
5281
  }
5209
5282
  }
5210
5283
  //# sourceMappingURL=session.js.map