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