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