@getpaseo/server 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/client/daemon-client-relay-e2ee-transport.d.ts +8 -0
- package/dist/server/client/daemon-client-relay-e2ee-transport.d.ts.map +1 -0
- package/dist/server/client/daemon-client-relay-e2ee-transport.js +161 -0
- package/dist/server/client/daemon-client-relay-e2ee-transport.js.map +1 -0
- package/dist/server/client/daemon-client-terminal-stream-manager.d.ts +43 -0
- package/dist/server/client/daemon-client-terminal-stream-manager.d.ts.map +1 -0
- package/dist/server/client/daemon-client-terminal-stream-manager.js +130 -0
- package/dist/server/client/daemon-client-terminal-stream-manager.js.map +1 -0
- package/dist/server/client/daemon-client-transport-types.d.ts +34 -0
- package/dist/server/client/daemon-client-transport-types.d.ts.map +1 -0
- package/dist/server/client/daemon-client-transport-types.js +2 -0
- package/dist/server/client/daemon-client-transport-types.js.map +1 -0
- package/dist/server/client/daemon-client-transport-utils.d.ts +9 -0
- package/dist/server/client/daemon-client-transport-utils.d.ts.map +1 -0
- package/dist/server/client/daemon-client-transport-utils.js +121 -0
- package/dist/server/client/daemon-client-transport-utils.js.map +1 -0
- package/dist/server/client/daemon-client-transport.d.ts +5 -0
- package/dist/server/client/daemon-client-transport.d.ts.map +1 -0
- package/dist/server/client/daemon-client-transport.js +4 -0
- package/dist/server/client/daemon-client-transport.js.map +1 -0
- package/dist/server/client/daemon-client-websocket-transport.d.ts +7 -0
- package/dist/server/client/daemon-client-websocket-transport.d.ts.map +1 -0
- package/dist/server/client/daemon-client-websocket-transport.js +64 -0
- package/dist/server/client/daemon-client-websocket-transport.js.map +1 -0
- package/dist/server/client/daemon-client.d.ts +54 -36
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +497 -800
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +52 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +238 -10
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-storage.d.ts +4 -4
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +62 -12
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts +4 -4
- package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js +213 -50
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.d.ts +7 -0
- package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.js +100 -19
- package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts +0 -1
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js +67 -30
- package/dist/server/server/agent/providers/codex/tool-call-detail-parser.js.map +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-mapper.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex/tool-call-mapper.js +293 -266
- package/dist/server/server/agent/providers/codex/tool-call-mapper.js.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +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 +49 -14
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/codex-rollout-timeline.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-rollout-timeline.js +5 -4
- package/dist/server/server/agent/providers/codex-rollout-timeline.js.map +1 -1
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js +8 -2
- package/dist/server/server/agent/providers/opencode/tool-call-detail-parser.js.map +1 -1
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts +1 -1
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.js +121 -45
- package/dist/server/server/agent/providers/opencode/tool-call-mapper.js.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.js +87 -35
- package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +2 -2
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js +23 -6
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
- package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts +0 -1
- package/dist/server/server/agent/providers/tool-call-mapper-utils.d.ts.map +1 -1
- package/dist/server/server/agent/providers/tool-call-mapper-utils.js +0 -10
- package/dist/server/server/agent/providers/tool-call-mapper-utils.js.map +1 -1
- package/dist/server/server/agent/stt-manager.d.ts +3 -2
- package/dist/server/server/agent/stt-manager.d.ts.map +1 -1
- package/dist/server/server/agent/stt-manager.js +5 -3
- package/dist/server/server/agent/stt-manager.js.map +1 -1
- package/dist/server/server/agent/timeline-projection.d.ts +19 -0
- package/dist/server/server/agent/timeline-projection.d.ts.map +1 -0
- package/dist/server/server/agent/timeline-projection.js +142 -0
- package/dist/server/server/agent/timeline-projection.js.map +1 -0
- package/dist/server/server/agent/tts-manager.d.ts +3 -2
- package/dist/server/server/agent/tts-manager.d.ts.map +1 -1
- package/dist/server/server/agent/tts-manager.js +5 -17
- package/dist/server/server/agent/tts-manager.js.map +1 -1
- package/dist/server/server/agent-attention-policy.d.ts +20 -0
- package/dist/server/server/agent-attention-policy.d.ts.map +1 -0
- package/dist/server/server/agent-attention-policy.js +40 -0
- package/dist/server/server/agent-attention-policy.js.map +1 -0
- package/dist/server/server/bootstrap.d.ts.map +1 -1
- package/dist/server/server/bootstrap.js +13 -20
- package/dist/server/server/bootstrap.js.map +1 -1
- package/dist/server/server/dictation/dictation-stream-manager.d.ts +10 -2
- package/dist/server/server/dictation/dictation-stream-manager.d.ts.map +1 -1
- package/dist/server/server/dictation/dictation-stream-manager.js +83 -27
- package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
- package/dist/server/server/exports.d.ts +2 -1
- package/dist/server/server/exports.d.ts.map +1 -1
- package/dist/server/server/exports.js +1 -0
- package/dist/server/server/exports.js.map +1 -1
- package/dist/server/server/persisted-config.d.ts +36 -22
- package/dist/server/server/persisted-config.d.ts.map +1 -1
- package/dist/server/server/persisted-config.js +2 -0
- package/dist/server/server/persisted-config.js.map +1 -1
- package/dist/server/server/relay-transport.d.ts +3 -2
- package/dist/server/server/relay-transport.d.ts.map +1 -1
- package/dist/server/server/relay-transport.js +99 -16
- package/dist/server/server/relay-transport.js.map +1 -1
- package/dist/server/server/session.d.ts +34 -16
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +619 -328
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/speech/provider-resolver.d.ts +3 -0
- package/dist/server/server/speech/provider-resolver.d.ts.map +1 -0
- package/dist/server/server/speech/provider-resolver.js +7 -0
- package/dist/server/server/speech/provider-resolver.js.map +1 -0
- package/dist/server/server/speech/providers/local/config.d.ts.map +1 -1
- package/dist/server/server/speech/providers/local/config.js +11 -8
- package/dist/server/server/speech/providers/local/config.js.map +1 -1
- package/dist/server/server/speech/providers/local/runtime.d.ts +1 -0
- package/dist/server/server/speech/providers/local/runtime.d.ts.map +1 -1
- package/dist/server/server/speech/providers/local/runtime.js +13 -9
- package/dist/server/server/speech/providers/local/runtime.js.map +1 -1
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.d.ts.map +1 -1
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.js +73 -54
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.js.map +1 -1
- package/dist/server/server/speech/providers/openai/config.d.ts.map +1 -1
- package/dist/server/server/speech/providers/openai/config.js +10 -5
- package/dist/server/server/speech/providers/openai/config.js.map +1 -1
- package/dist/server/server/speech/providers/openai/runtime.d.ts.map +1 -1
- package/dist/server/server/speech/providers/openai/runtime.js +17 -6
- package/dist/server/server/speech/providers/openai/runtime.js.map +1 -1
- package/dist/server/server/speech/speech-config-resolver.d.ts.map +1 -1
- package/dist/server/server/speech/speech-config-resolver.js +25 -4
- package/dist/server/server/speech/speech-config-resolver.js.map +1 -1
- package/dist/server/server/speech/speech-runtime.d.ts +26 -3
- package/dist/server/server/speech/speech-runtime.d.ts.map +1 -1
- package/dist/server/server/speech/speech-runtime.js +468 -85
- package/dist/server/server/speech/speech-runtime.js.map +1 -1
- package/dist/server/server/speech/speech-types.d.ts +3 -0
- package/dist/server/server/speech/speech-types.d.ts.map +1 -1
- package/dist/server/server/speech/speech-types.js +1 -0
- package/dist/server/server/speech/speech-types.js.map +1 -1
- package/dist/server/server/websocket-server.d.ts +23 -7
- package/dist/server/server/websocket-server.d.ts.map +1 -1
- package/dist/server/server/websocket-server.js +288 -102
- package/dist/server/server/websocket-server.js.map +1 -1
- package/dist/server/shared/binary-mux.d.ts +31 -0
- package/dist/server/shared/binary-mux.d.ts.map +1 -0
- package/dist/server/shared/binary-mux.js +101 -0
- package/dist/server/shared/binary-mux.js.map +1 -0
- package/dist/server/shared/messages.d.ts +5206 -4814
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +225 -58
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/shared/terminal-key-input.d.ts +9 -0
- package/dist/server/shared/terminal-key-input.d.ts.map +1 -0
- package/dist/server/shared/terminal-key-input.js +132 -0
- package/dist/server/shared/terminal-key-input.js.map +1 -0
- package/dist/server/terminal/terminal.d.ts +17 -0
- package/dist/server/terminal/terminal.d.ts.map +1 -1
- package/dist/server/terminal/terminal.js +89 -0
- package/dist/server/terminal/terminal.js.map +1 -1
- package/dist/server/utils/checkout-git.d.ts +9 -1
- package/dist/server/utils/checkout-git.d.ts.map +1 -1
- package/dist/server/utils/checkout-git.js +314 -75
- package/dist/server/utils/checkout-git.js.map +1 -1
- package/dist/server/utils/worktree.d.ts.map +1 -1
- package/dist/server/utils/worktree.js +33 -4
- package/dist/server/utils/worktree.js.map +1 -1
- package/package.json +2 -2
|
@@ -6,7 +6,7 @@ import { promisify } from "util";
|
|
|
6
6
|
import { join, resolve, sep } from "path";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { serializeAgentStreamEvent, } from "./messages.js";
|
|
9
|
-
import {
|
|
9
|
+
import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from "../shared/binary-mux.js";
|
|
10
10
|
import { TTSManager } from "./agent/tts-manager.js";
|
|
11
11
|
import { STTManager } from "./agent/stt-manager.js";
|
|
12
12
|
import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
|
|
@@ -17,13 +17,14 @@ import { experimental_createMCPClient } from "ai";
|
|
|
17
17
|
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
|
18
18
|
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
|
|
19
19
|
import { toAgentPayload } from "./agent/agent-projections.js";
|
|
20
|
+
import { projectTimelineRows } from "./agent/timeline-projection.js";
|
|
20
21
|
import { StructuredAgentResponseError, generateStructuredAgentResponse, } from "./agent/agent-response-loop.js";
|
|
21
22
|
import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
|
|
22
23
|
import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
|
|
23
24
|
import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
|
|
24
25
|
import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
|
|
25
26
|
import { createWorktree, runWorktreeSetupCommands, WorktreeSetupError, slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
|
|
26
|
-
import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from "../utils/checkout-git.js";
|
|
27
|
+
import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from "../utils/checkout-git.js";
|
|
27
28
|
import { getProjectIcon } from "../utils/project-icon.js";
|
|
28
29
|
import { expandTilde } from "../utils/path.js";
|
|
29
30
|
import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from "./speech/providers/local/models.js";
|
|
@@ -40,6 +41,9 @@ const PROJECT_PLACEMENT_CACHE_TTL_MS = 10000;
|
|
|
40
41
|
const MAX_AGENTS_PER_PROJECT = 5;
|
|
41
42
|
const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
|
|
42
43
|
const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
|
|
44
|
+
const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
|
|
45
|
+
const TERMINAL_STREAM_MAX_PENDING_BYTES = 2 * 1024 * 1024;
|
|
46
|
+
const TERMINAL_STREAM_MAX_PENDING_CHUNKS = 2048;
|
|
43
47
|
/**
|
|
44
48
|
* Default model used for auto-generating commit messages and PR descriptions.
|
|
45
49
|
* Uses Claude Haiku for speed and cost efficiency.
|
|
@@ -117,6 +121,15 @@ const VOICE_INTERNAL_DICTATION_ID_PREFIX = "__voice_turn__:";
|
|
|
117
121
|
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
|
|
118
122
|
const AgentIdSchema = z.string().uuid();
|
|
119
123
|
const VOICE_MCP_SERVER_NAME = "paseo_voice";
|
|
124
|
+
class VoiceFeatureUnavailableError extends Error {
|
|
125
|
+
constructor(context) {
|
|
126
|
+
super(context.message);
|
|
127
|
+
this.name = "VoiceFeatureUnavailableError";
|
|
128
|
+
this.reasonCode = context.reasonCode;
|
|
129
|
+
this.retryable = context.retryable;
|
|
130
|
+
this.missingModelIds = [...context.missingModelIds];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
120
133
|
function convertPCMToWavBuffer(pcmBuffer, sampleRate, channels, bitsPerSample) {
|
|
121
134
|
const headerSize = 44;
|
|
122
135
|
const wavBuffer = Buffer.alloc(headerSize + pcmBuffer.length);
|
|
@@ -200,14 +213,18 @@ export class Session {
|
|
|
200
213
|
this.clientActivity = null;
|
|
201
214
|
this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
|
|
202
215
|
this.terminalSubscriptions = new Map();
|
|
216
|
+
this.terminalStreams = new Map();
|
|
217
|
+
this.terminalStreamByTerminalId = new Map();
|
|
218
|
+
this.nextTerminalStreamId = 1;
|
|
203
219
|
this.checkoutDiffSubscriptions = new Map();
|
|
204
220
|
this.checkoutDiffTargets = new Map();
|
|
205
221
|
this.voiceModeAgentId = null;
|
|
206
222
|
this.voiceModeBaseConfig = null;
|
|
207
|
-
const { clientId, onMessage, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
|
|
223
|
+
const { clientId, onMessage, onBinaryMessage, logger, downloadTokenStore, pushTokenStore, paseoHome, agentManager, agentStorage, createAgentMcpTransport, stt, tts, terminalManager, voice, voiceBridge, dictation, agentProviderRuntimeSettings, } = options;
|
|
208
224
|
this.clientId = clientId;
|
|
209
225
|
this.sessionId = uuidv4();
|
|
210
226
|
this.onMessage = onMessage;
|
|
227
|
+
this.onBinaryMessage = onBinaryMessage ?? null;
|
|
211
228
|
this.downloadTokenStore = downloadTokenStore;
|
|
212
229
|
this.pushTokenStore = pushTokenStore;
|
|
213
230
|
this.paseoHome = paseoHome;
|
|
@@ -231,6 +248,7 @@ export class Session {
|
|
|
231
248
|
this.unregisterVoiceCallerContext = voiceBridge?.unregisterVoiceCallerContext;
|
|
232
249
|
this.ensureVoiceMcpSocketForAgent = voiceBridge?.ensureVoiceMcpSocketForAgent;
|
|
233
250
|
this.removeVoiceMcpSocketForAgent = voiceBridge?.removeVoiceMcpSocketForAgent;
|
|
251
|
+
this.getSpeechReadiness = dictation?.getSpeechReadiness;
|
|
234
252
|
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
|
235
253
|
this.abortController = new AbortController();
|
|
236
254
|
this.sessionLogger = logger.child({
|
|
@@ -261,7 +279,7 @@ export class Session {
|
|
|
261
279
|
// Initialize agent MCP client asynchronously
|
|
262
280
|
void this.initializeAgentMcp();
|
|
263
281
|
this.subscribeToAgentEvents();
|
|
264
|
-
this.sessionLogger.
|
|
282
|
+
this.sessionLogger.trace("Session created");
|
|
265
283
|
}
|
|
266
284
|
/**
|
|
267
285
|
* Get the client's current activity state
|
|
@@ -331,11 +349,11 @@ export class Session {
|
|
|
331
349
|
/**
|
|
332
350
|
* Start streaming an agent run and forward results via the websocket broadcast
|
|
333
351
|
*/
|
|
334
|
-
startAgentStream(agentId, prompt) {
|
|
352
|
+
startAgentStream(agentId, prompt, runOptions) {
|
|
335
353
|
this.sessionLogger.info({ agentId }, `Starting agent stream for ${agentId}`);
|
|
336
354
|
let iterator;
|
|
337
355
|
try {
|
|
338
|
-
iterator = this.agentManager.streamAgent(agentId, prompt);
|
|
356
|
+
iterator = this.agentManager.streamAgent(agentId, prompt, runOptions);
|
|
339
357
|
}
|
|
340
358
|
catch (error) {
|
|
341
359
|
this.handleAgentRunError(agentId, error, "Failed to start agent run");
|
|
@@ -379,7 +397,7 @@ export class Session {
|
|
|
379
397
|
});
|
|
380
398
|
this.agentTools = (await this.agentMcpClient.tools());
|
|
381
399
|
const agentToolCount = Object.keys(this.agentTools ?? {}).length;
|
|
382
|
-
this.sessionLogger.
|
|
400
|
+
this.sessionLogger.trace({ agentToolCount }, `Agent MCP initialized with ${agentToolCount} tools`);
|
|
383
401
|
}
|
|
384
402
|
catch (error) {
|
|
385
403
|
this.sessionLogger.error({ err: error }, "Failed to initialize Agent MCP");
|
|
@@ -416,9 +434,7 @@ export class Session {
|
|
|
416
434
|
}
|
|
417
435
|
// Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
|
|
418
436
|
// for the focused agent, with a short grace window while backgrounded.
|
|
419
|
-
//
|
|
420
|
-
// History catch-up is handled via explicit `initialize_agent_request` which emits a
|
|
421
|
-
// batched `agent_stream_snapshot`.
|
|
437
|
+
// History catch-up is handled via pull-based `fetch_agent_timeline_request`.
|
|
422
438
|
const activity = this.clientActivity;
|
|
423
439
|
if (activity?.deviceType === "mobile") {
|
|
424
440
|
if (!activity.focusedAgentId) {
|
|
@@ -442,6 +458,8 @@ export class Session {
|
|
|
442
458
|
agentId: event.agentId,
|
|
443
459
|
event: serializedEvent,
|
|
444
460
|
timestamp: new Date().toISOString(),
|
|
461
|
+
...(typeof event.seq === "number" ? { seq: event.seq } : {}),
|
|
462
|
+
...(typeof event.epoch === "string" ? { epoch: event.epoch } : {}),
|
|
445
463
|
};
|
|
446
464
|
this.emit({
|
|
447
465
|
type: "agent_stream",
|
|
@@ -534,10 +552,12 @@ export class Session {
|
|
|
534
552
|
let snapshot;
|
|
535
553
|
if (handle) {
|
|
536
554
|
snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, extractTimestamps(record));
|
|
555
|
+
this.sessionLogger.info({ agentId, provider: record.provider }, "Agent resumed from persistence");
|
|
537
556
|
}
|
|
538
557
|
else {
|
|
539
558
|
const config = buildSessionConfig(record);
|
|
540
559
|
snapshot = await this.agentManager.createAgent(config, agentId, { labels: record.labels });
|
|
560
|
+
this.sessionLogger.info({ agentId, provider: record.provider }, "Agent created from stored config");
|
|
541
561
|
}
|
|
542
562
|
await this.agentManager.hydrateTimelineFromProvider(agentId);
|
|
543
563
|
return this.agentManager.getAgent(agentId) ?? snapshot;
|
|
@@ -685,6 +705,9 @@ export class Session {
|
|
|
685
705
|
case "archive_agent_request":
|
|
686
706
|
await this.handleArchiveAgentRequest(msg.agentId, msg.requestId);
|
|
687
707
|
break;
|
|
708
|
+
case "update_agent_request":
|
|
709
|
+
await this.handleUpdateAgentRequest(msg.agentId, msg.name, msg.labels, msg.requestId);
|
|
710
|
+
break;
|
|
688
711
|
case "set_voice_mode":
|
|
689
712
|
await this.handleSetVoiceMode(msg.enabled, msg.agentId, msg.requestId);
|
|
690
713
|
break;
|
|
@@ -695,6 +718,22 @@ export class Session {
|
|
|
695
718
|
await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
|
|
696
719
|
break;
|
|
697
720
|
case "dictation_stream_start":
|
|
721
|
+
{
|
|
722
|
+
const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation");
|
|
723
|
+
if (unavailable) {
|
|
724
|
+
this.emit({
|
|
725
|
+
type: "dictation_stream_error",
|
|
726
|
+
payload: {
|
|
727
|
+
dictationId: msg.dictationId,
|
|
728
|
+
error: unavailable.message,
|
|
729
|
+
retryable: unavailable.retryable,
|
|
730
|
+
reasonCode: unavailable.reasonCode,
|
|
731
|
+
missingModelIds: unavailable.missingModelIds,
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
698
737
|
await this.dictationStreamManager.handleStart(msg.dictationId, msg.format);
|
|
699
738
|
break;
|
|
700
739
|
case "dictation_stream_chunk":
|
|
@@ -726,8 +765,8 @@ export class Session {
|
|
|
726
765
|
case "restart_server_request":
|
|
727
766
|
await this.handleRestartServerRequest(msg.requestId, msg.reason);
|
|
728
767
|
break;
|
|
729
|
-
case "
|
|
730
|
-
await this.
|
|
768
|
+
case "fetch_agent_timeline_request":
|
|
769
|
+
await this.handleFetchAgentTimelineRequest(msg);
|
|
731
770
|
break;
|
|
732
771
|
case "set_agent_mode_request":
|
|
733
772
|
await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId);
|
|
@@ -741,15 +780,15 @@ export class Session {
|
|
|
741
780
|
case "agent_permission_response":
|
|
742
781
|
await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.response);
|
|
743
782
|
break;
|
|
744
|
-
case "git_diff_request":
|
|
745
|
-
await this.handleGitDiffRequest(msg.agentId, msg.requestId);
|
|
746
|
-
break;
|
|
747
783
|
case "checkout_status_request":
|
|
748
784
|
await this.handleCheckoutStatusRequest(msg);
|
|
749
785
|
break;
|
|
750
786
|
case "validate_branch_request":
|
|
751
787
|
await this.handleValidateBranchRequest(msg);
|
|
752
788
|
break;
|
|
789
|
+
case "branch_suggestions_request":
|
|
790
|
+
await this.handleBranchSuggestionsRequest(msg);
|
|
791
|
+
break;
|
|
753
792
|
case "subscribe_checkout_diff_request":
|
|
754
793
|
await this.handleSubscribeCheckoutDiffRequest(msg);
|
|
755
794
|
break;
|
|
@@ -780,9 +819,6 @@ export class Session {
|
|
|
780
819
|
case "paseo_worktree_archive_request":
|
|
781
820
|
await this.handlePaseoWorktreeArchiveRequest(msg);
|
|
782
821
|
break;
|
|
783
|
-
case "highlighted_diff_request":
|
|
784
|
-
await this.handleHighlightedDiffRequest(msg.agentId, msg.requestId);
|
|
785
|
-
break;
|
|
786
822
|
case "file_explorer_request":
|
|
787
823
|
await this.handleFileExplorerRequest(msg);
|
|
788
824
|
break;
|
|
@@ -795,6 +831,9 @@ export class Session {
|
|
|
795
831
|
case "list_provider_models_request":
|
|
796
832
|
await this.handleListProviderModelsRequest(msg);
|
|
797
833
|
break;
|
|
834
|
+
case "list_available_providers_request":
|
|
835
|
+
await this.handleListAvailableProvidersRequest(msg);
|
|
836
|
+
break;
|
|
798
837
|
case "speech_models_list_request":
|
|
799
838
|
await this.handleSpeechModelsListRequest(msg);
|
|
800
839
|
break;
|
|
@@ -847,6 +886,12 @@ export class Session {
|
|
|
847
886
|
case "kill_terminal_request":
|
|
848
887
|
await this.handleKillTerminalRequest(msg);
|
|
849
888
|
break;
|
|
889
|
+
case "attach_terminal_stream_request":
|
|
890
|
+
await this.handleAttachTerminalStreamRequest(msg);
|
|
891
|
+
break;
|
|
892
|
+
case "detach_terminal_stream_request":
|
|
893
|
+
this.handleDetachTerminalStreamRequest(msg);
|
|
894
|
+
break;
|
|
850
895
|
}
|
|
851
896
|
}
|
|
852
897
|
catch (error) {
|
|
@@ -880,6 +925,58 @@ export class Session {
|
|
|
880
925
|
});
|
|
881
926
|
}
|
|
882
927
|
}
|
|
928
|
+
handleBinaryFrame(frame) {
|
|
929
|
+
switch (frame.channel) {
|
|
930
|
+
case BinaryMuxChannel.Terminal:
|
|
931
|
+
this.handleTerminalBinaryFrame(frame);
|
|
932
|
+
break;
|
|
933
|
+
default:
|
|
934
|
+
this.sessionLogger.warn({ channel: frame.channel, messageType: frame.messageType }, "Unhandled binary mux channel");
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
handleTerminalBinaryFrame(frame) {
|
|
939
|
+
if (frame.messageType === TerminalBinaryMessageType.InputUtf8) {
|
|
940
|
+
const binding = this.terminalStreams.get(frame.streamId);
|
|
941
|
+
if (!binding) {
|
|
942
|
+
this.sessionLogger.warn({ streamId: frame.streamId }, "Terminal stream not found for input");
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (!this.terminalManager) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const session = this.terminalManager.getTerminal(binding.terminalId);
|
|
949
|
+
if (!session) {
|
|
950
|
+
this.detachTerminalStream(frame.streamId, { emitExit: true });
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const payload = frame.payload ?? new Uint8Array(0);
|
|
954
|
+
if (payload.byteLength === 0) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const text = Buffer.from(payload).toString("utf8");
|
|
958
|
+
if (!text) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
session.send({ type: "input", data: text });
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (frame.messageType === TerminalBinaryMessageType.Ack) {
|
|
965
|
+
const binding = this.terminalStreams.get(frame.streamId);
|
|
966
|
+
if (binding) {
|
|
967
|
+
if (!Number.isFinite(frame.offset) || frame.offset < 0) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const nextAckOffset = Math.max(binding.lastAckOffset, Math.min(Math.floor(frame.offset), binding.lastOutputOffset));
|
|
971
|
+
if (nextAckOffset > binding.lastAckOffset) {
|
|
972
|
+
binding.lastAckOffset = nextAckOffset;
|
|
973
|
+
this.flushPendingTerminalStreamChunks(frame.streamId, binding);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
this.sessionLogger.warn({ streamId: frame.streamId, messageType: frame.messageType }, "Unhandled terminal binary frame");
|
|
979
|
+
}
|
|
883
980
|
async handleRestartServerRequest(requestId, reason) {
|
|
884
981
|
if (restartRequested) {
|
|
885
982
|
this.sessionLogger.debug("Restart already requested, ignoring duplicate");
|
|
@@ -965,12 +1062,123 @@ export class Session {
|
|
|
965
1062
|
},
|
|
966
1063
|
});
|
|
967
1064
|
}
|
|
1065
|
+
async handleUpdateAgentRequest(agentId, name, labels, requestId) {
|
|
1066
|
+
this.sessionLogger.info({ agentId, requestId, hasName: typeof name === "string", labelCount: labels ? Object.keys(labels).length : 0 }, "session: update_agent_request");
|
|
1067
|
+
const normalizedName = name?.trim();
|
|
1068
|
+
const normalizedLabels = labels && Object.keys(labels).length > 0 ? labels : undefined;
|
|
1069
|
+
if (!normalizedName && !normalizedLabels) {
|
|
1070
|
+
this.emit({
|
|
1071
|
+
type: "update_agent_response",
|
|
1072
|
+
payload: {
|
|
1073
|
+
requestId,
|
|
1074
|
+
agentId,
|
|
1075
|
+
accepted: false,
|
|
1076
|
+
error: "Nothing to update (provide name and/or labels)",
|
|
1077
|
+
},
|
|
1078
|
+
});
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
const liveAgent = this.agentManager.getAgent(agentId);
|
|
1083
|
+
if (liveAgent) {
|
|
1084
|
+
if (normalizedName) {
|
|
1085
|
+
await this.agentManager.setTitle(agentId, normalizedName);
|
|
1086
|
+
}
|
|
1087
|
+
if (normalizedLabels) {
|
|
1088
|
+
await this.agentManager.setLabels(agentId, normalizedLabels);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
const existing = await this.agentStorage.get(agentId);
|
|
1093
|
+
if (!existing) {
|
|
1094
|
+
throw new Error(`Agent not found: ${agentId}`);
|
|
1095
|
+
}
|
|
1096
|
+
await this.agentStorage.upsert({
|
|
1097
|
+
...existing,
|
|
1098
|
+
...(normalizedName ? { title: normalizedName } : {}),
|
|
1099
|
+
...(normalizedLabels
|
|
1100
|
+
? { labels: { ...existing.labels, ...normalizedLabels } }
|
|
1101
|
+
: {}),
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
this.emit({
|
|
1105
|
+
type: "update_agent_response",
|
|
1106
|
+
payload: { requestId, agentId, accepted: true, error: null },
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
catch (error) {
|
|
1110
|
+
this.sessionLogger.error({ err: error, agentId, requestId }, "session: update_agent_request error");
|
|
1111
|
+
this.emit({
|
|
1112
|
+
type: "activity_log",
|
|
1113
|
+
payload: {
|
|
1114
|
+
id: uuidv4(),
|
|
1115
|
+
timestamp: new Date(),
|
|
1116
|
+
type: "error",
|
|
1117
|
+
content: `Failed to update agent: ${error.message}`,
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1120
|
+
this.emit({
|
|
1121
|
+
type: "update_agent_response",
|
|
1122
|
+
payload: {
|
|
1123
|
+
requestId,
|
|
1124
|
+
agentId,
|
|
1125
|
+
accepted: false,
|
|
1126
|
+
error: error?.message ? String(error.message) : "Failed to update agent",
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
toVoiceFeatureUnavailableContext(state) {
|
|
1132
|
+
return {
|
|
1133
|
+
reasonCode: state.reasonCode,
|
|
1134
|
+
message: state.message,
|
|
1135
|
+
retryable: state.retryable,
|
|
1136
|
+
missingModelIds: [...state.missingModelIds],
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
resolveModeReadinessState(readiness, mode) {
|
|
1140
|
+
if (mode === "voice_mode") {
|
|
1141
|
+
return readiness.realtimeVoice;
|
|
1142
|
+
}
|
|
1143
|
+
return readiness.dictation;
|
|
1144
|
+
}
|
|
1145
|
+
getVoiceFeatureUnavailableResponseMetadata(error) {
|
|
1146
|
+
if (!(error instanceof VoiceFeatureUnavailableError)) {
|
|
1147
|
+
return {};
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
reasonCode: error.reasonCode,
|
|
1151
|
+
retryable: error.retryable,
|
|
1152
|
+
missingModelIds: error.missingModelIds,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
resolveVoiceFeatureUnavailableContext(mode) {
|
|
1156
|
+
const readiness = this.getSpeechReadiness?.();
|
|
1157
|
+
if (!readiness) {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
const modeReadiness = this.resolveModeReadinessState(readiness, mode);
|
|
1161
|
+
if (!modeReadiness.enabled) {
|
|
1162
|
+
return this.toVoiceFeatureUnavailableContext(modeReadiness);
|
|
1163
|
+
}
|
|
1164
|
+
if (!readiness.voiceFeature.available) {
|
|
1165
|
+
return this.toVoiceFeatureUnavailableContext(readiness.voiceFeature);
|
|
1166
|
+
}
|
|
1167
|
+
if (!modeReadiness.available) {
|
|
1168
|
+
return this.toVoiceFeatureUnavailableContext(modeReadiness);
|
|
1169
|
+
}
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
968
1172
|
/**
|
|
969
1173
|
* Handle voice mode toggle
|
|
970
1174
|
*/
|
|
971
1175
|
async handleSetVoiceMode(enabled, agentId, requestId) {
|
|
972
1176
|
try {
|
|
973
1177
|
if (enabled) {
|
|
1178
|
+
const unavailable = this.resolveVoiceFeatureUnavailableContext("voice_mode");
|
|
1179
|
+
if (unavailable) {
|
|
1180
|
+
throw new VoiceFeatureUnavailableError(unavailable);
|
|
1181
|
+
}
|
|
974
1182
|
const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? "", "set_voice_mode");
|
|
975
1183
|
if (this.isVoiceMode &&
|
|
976
1184
|
this.voiceModeAgentId &&
|
|
@@ -1017,6 +1225,7 @@ export class Session {
|
|
|
1017
1225
|
}
|
|
1018
1226
|
catch (error) {
|
|
1019
1227
|
const errorMessage = error instanceof Error ? error.message : "Failed to set voice mode";
|
|
1228
|
+
const unavailable = this.getVoiceFeatureUnavailableResponseMetadata(error);
|
|
1020
1229
|
this.sessionLogger.error({
|
|
1021
1230
|
err: error,
|
|
1022
1231
|
enabled,
|
|
@@ -1031,6 +1240,7 @@ export class Session {
|
|
|
1031
1240
|
agentId: this.voiceModeAgentId,
|
|
1032
1241
|
accepted: false,
|
|
1033
1242
|
error: errorMessage,
|
|
1243
|
+
...unavailable,
|
|
1034
1244
|
},
|
|
1035
1245
|
});
|
|
1036
1246
|
return;
|
|
@@ -1318,7 +1528,7 @@ export class Session {
|
|
|
1318
1528
|
/**
|
|
1319
1529
|
* Handle text message to agent (with optional image attachments)
|
|
1320
1530
|
*/
|
|
1321
|
-
async handleSendAgentMessage(agentId, text, messageId, images) {
|
|
1531
|
+
async handleSendAgentMessage(agentId, text, messageId, images, runOptions) {
|
|
1322
1532
|
this.sessionLogger.info({ agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 }, `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ''}`);
|
|
1323
1533
|
try {
|
|
1324
1534
|
await this.ensureAgentLoaded(agentId);
|
|
@@ -1341,46 +1551,13 @@ export class Session {
|
|
|
1341
1551
|
catch (error) {
|
|
1342
1552
|
this.sessionLogger.error({ err: error, agentId }, `Failed to record user message for agent ${agentId}`);
|
|
1343
1553
|
}
|
|
1344
|
-
this.startAgentStream(agentId, prompt);
|
|
1345
|
-
}
|
|
1346
|
-
/**
|
|
1347
|
-
* Handle on-demand agent initialization request from client
|
|
1348
|
-
*/
|
|
1349
|
-
async handleInitializeAgentRequest(agentId, requestId) {
|
|
1350
|
-
this.sessionLogger.info({ agentId }, `Initializing agent ${agentId} on demand`);
|
|
1351
|
-
try {
|
|
1352
|
-
const snapshot = await this.ensureAgentLoaded(agentId);
|
|
1353
|
-
await this.forwardAgentUpdate(snapshot);
|
|
1354
|
-
// Send timeline snapshot after hydration (if any)
|
|
1355
|
-
const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
|
|
1356
|
-
this.emit({
|
|
1357
|
-
type: "initialize_agent_request",
|
|
1358
|
-
payload: {
|
|
1359
|
-
agentId,
|
|
1360
|
-
agentStatus: snapshot.lifecycle,
|
|
1361
|
-
timelineSize,
|
|
1362
|
-
requestId,
|
|
1363
|
-
},
|
|
1364
|
-
});
|
|
1365
|
-
this.sessionLogger.info({ agentId, timelineSize, status: snapshot.lifecycle }, `Agent ${agentId} initialized with ${timelineSize} timeline item(s); status=${snapshot.lifecycle}`);
|
|
1366
|
-
}
|
|
1367
|
-
catch (error) {
|
|
1368
|
-
this.sessionLogger.error({ err: error, agentId }, `Failed to initialize agent ${agentId}`);
|
|
1369
|
-
this.emit({
|
|
1370
|
-
type: "initialize_agent_request",
|
|
1371
|
-
payload: {
|
|
1372
|
-
agentId,
|
|
1373
|
-
requestId,
|
|
1374
|
-
error: error?.message ?? "Failed to initialize agent",
|
|
1375
|
-
},
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1554
|
+
this.startAgentStream(agentId, prompt, runOptions);
|
|
1378
1555
|
}
|
|
1379
1556
|
/**
|
|
1380
1557
|
* Handle create agent request
|
|
1381
1558
|
*/
|
|
1382
1559
|
async handleCreateAgentRequest(msg) {
|
|
1383
|
-
const { config, worktreeName, requestId, initialPrompt, git, images, labels } = msg;
|
|
1560
|
+
const { config, worktreeName, requestId, initialPrompt, outputSchema, git, images, labels } = msg;
|
|
1384
1561
|
this.sessionLogger.info({ cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${worktreeName ? ` with worktree ${worktreeName}` : ""}`);
|
|
1385
1562
|
try {
|
|
1386
1563
|
const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(config, git, worktreeName, labels);
|
|
@@ -1398,7 +1575,7 @@ export class Session {
|
|
|
1398
1575
|
logger: this.sessionLogger,
|
|
1399
1576
|
});
|
|
1400
1577
|
try {
|
|
1401
|
-
await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, uuidv4(), images);
|
|
1578
|
+
await this.handleSendAgentMessage(snapshot.id, trimmedPrompt, uuidv4(), images, outputSchema ? { outputSchema } : undefined);
|
|
1402
1579
|
}
|
|
1403
1580
|
catch (promptError) {
|
|
1404
1581
|
this.sessionLogger.error({ err: promptError, agentId: snapshot.id }, `Failed to run initial prompt for agent ${snapshot.id}`);
|
|
@@ -1476,7 +1653,7 @@ export class Session {
|
|
|
1476
1653
|
const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
|
|
1477
1654
|
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
|
|
1478
1655
|
await this.forwardAgentUpdate(snapshot);
|
|
1479
|
-
const timelineSize = this.
|
|
1656
|
+
const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
|
|
1480
1657
|
if (requestId) {
|
|
1481
1658
|
const agentPayload = await this.getAgentPayloadById(snapshot.id);
|
|
1482
1659
|
if (!agentPayload) {
|
|
@@ -1535,7 +1712,7 @@ export class Session {
|
|
|
1535
1712
|
}
|
|
1536
1713
|
await this.agentManager.hydrateTimelineFromProvider(agentId);
|
|
1537
1714
|
await this.forwardAgentUpdate(snapshot);
|
|
1538
|
-
const timelineSize = this.
|
|
1715
|
+
const timelineSize = this.agentManager.getTimeline(agentId).length;
|
|
1539
1716
|
if (requestId) {
|
|
1540
1717
|
this.emit({
|
|
1541
1718
|
type: "status",
|
|
@@ -1752,6 +1929,33 @@ export class Session {
|
|
|
1752
1929
|
});
|
|
1753
1930
|
}
|
|
1754
1931
|
}
|
|
1932
|
+
async handleListAvailableProvidersRequest(msg) {
|
|
1933
|
+
const fetchedAt = new Date().toISOString();
|
|
1934
|
+
try {
|
|
1935
|
+
const providers = await this.agentManager.listProviderAvailability();
|
|
1936
|
+
this.emit({
|
|
1937
|
+
type: "list_available_providers_response",
|
|
1938
|
+
payload: {
|
|
1939
|
+
providers,
|
|
1940
|
+
error: null,
|
|
1941
|
+
fetchedAt,
|
|
1942
|
+
requestId: msg.requestId,
|
|
1943
|
+
},
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
catch (error) {
|
|
1947
|
+
this.sessionLogger.error({ err: error }, "Failed to list provider availability");
|
|
1948
|
+
this.emit({
|
|
1949
|
+
type: "list_available_providers_response",
|
|
1950
|
+
payload: {
|
|
1951
|
+
providers: [],
|
|
1952
|
+
error: error?.message ?? String(error),
|
|
1953
|
+
fetchedAt,
|
|
1954
|
+
requestId: msg.requestId,
|
|
1955
|
+
},
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1755
1959
|
async handleSpeechModelsListRequest(msg) {
|
|
1756
1960
|
const modelsDir = this.localSpeechModelsDir;
|
|
1757
1961
|
const models = await Promise.all(listLocalSpeechModels().map(async (model) => {
|
|
@@ -2377,52 +2581,6 @@ export class Session {
|
|
|
2377
2581
|
throw error;
|
|
2378
2582
|
}
|
|
2379
2583
|
}
|
|
2380
|
-
/**
|
|
2381
|
-
* Handle git diff request for an agent
|
|
2382
|
-
*/
|
|
2383
|
-
async handleGitDiffRequest(agentId, requestId) {
|
|
2384
|
-
this.sessionLogger.debug({ agentId }, `Handling git diff request for agent ${agentId}`);
|
|
2385
|
-
try {
|
|
2386
|
-
const agents = this.agentManager.listAgents();
|
|
2387
|
-
const agent = agents.find((a) => a.id === agentId);
|
|
2388
|
-
if (!agent) {
|
|
2389
|
-
this.emit({
|
|
2390
|
-
type: "git_diff_response",
|
|
2391
|
-
payload: {
|
|
2392
|
-
agentId,
|
|
2393
|
-
diff: "",
|
|
2394
|
-
error: `Agent not found: ${agentId}`,
|
|
2395
|
-
requestId,
|
|
2396
|
-
},
|
|
2397
|
-
});
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
const diffResult = await getCheckoutDiff(agent.cwd, { mode: "uncommitted" }, { paseoHome: this.paseoHome });
|
|
2401
|
-
const combinedDiff = diffResult.diff;
|
|
2402
|
-
this.emit({
|
|
2403
|
-
type: "git_diff_response",
|
|
2404
|
-
payload: {
|
|
2405
|
-
agentId,
|
|
2406
|
-
diff: combinedDiff,
|
|
2407
|
-
error: null,
|
|
2408
|
-
requestId,
|
|
2409
|
-
},
|
|
2410
|
-
});
|
|
2411
|
-
this.sessionLogger.debug({ agentId, diffBytes: combinedDiff.length }, `Git diff for agent ${agentId} completed (${combinedDiff.length} bytes)`);
|
|
2412
|
-
}
|
|
2413
|
-
catch (error) {
|
|
2414
|
-
this.sessionLogger.error({ err: error, agentId }, `Failed to get git diff for agent ${agentId}`);
|
|
2415
|
-
this.emit({
|
|
2416
|
-
type: "git_diff_response",
|
|
2417
|
-
payload: {
|
|
2418
|
-
agentId,
|
|
2419
|
-
diff: "",
|
|
2420
|
-
error: error.message,
|
|
2421
|
-
requestId,
|
|
2422
|
-
},
|
|
2423
|
-
});
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
2584
|
async handleCheckoutStatusRequest(msg) {
|
|
2427
2585
|
const { cwd, requestId } = msg;
|
|
2428
2586
|
try {
|
|
@@ -2581,6 +2739,31 @@ export class Session {
|
|
|
2581
2739
|
});
|
|
2582
2740
|
}
|
|
2583
2741
|
}
|
|
2742
|
+
async handleBranchSuggestionsRequest(msg) {
|
|
2743
|
+
const { cwd, query, limit, requestId } = msg;
|
|
2744
|
+
try {
|
|
2745
|
+
const resolvedCwd = expandTilde(cwd);
|
|
2746
|
+
const branches = await listBranchSuggestions(resolvedCwd, { query, limit });
|
|
2747
|
+
this.emit({
|
|
2748
|
+
type: "branch_suggestions_response",
|
|
2749
|
+
payload: {
|
|
2750
|
+
branches,
|
|
2751
|
+
error: null,
|
|
2752
|
+
requestId,
|
|
2753
|
+
},
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
catch (error) {
|
|
2757
|
+
this.emit({
|
|
2758
|
+
type: "branch_suggestions_response",
|
|
2759
|
+
payload: {
|
|
2760
|
+
branches: [],
|
|
2761
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2762
|
+
requestId,
|
|
2763
|
+
},
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2584
2767
|
normalizeCheckoutDiffCompare(compare) {
|
|
2585
2768
|
if (compare.mode === "uncommitted") {
|
|
2586
2769
|
return { mode: "uncommitted" };
|
|
@@ -2752,30 +2935,42 @@ export class Session {
|
|
|
2752
2935
|
latestPayload: null,
|
|
2753
2936
|
latestFingerprint: null,
|
|
2754
2937
|
};
|
|
2755
|
-
const
|
|
2756
|
-
|
|
2757
|
-
watchPaths.add(watchRoot);
|
|
2758
|
-
}
|
|
2938
|
+
const repoWatchPath = watchRoot ?? cwd;
|
|
2939
|
+
const watchPaths = new Set([repoWatchPath]);
|
|
2759
2940
|
const gitDir = await this.resolveCheckoutGitDir(cwd);
|
|
2760
2941
|
if (gitDir) {
|
|
2761
2942
|
watchPaths.add(gitDir);
|
|
2762
2943
|
}
|
|
2763
|
-
let
|
|
2944
|
+
let hasRecursiveRepoCoverage = false;
|
|
2945
|
+
const allowRecursiveRepoWatch = process.platform !== "linux";
|
|
2764
2946
|
for (const watchPath of watchPaths) {
|
|
2947
|
+
const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
|
|
2765
2948
|
const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
|
|
2766
2949
|
this.scheduleCheckoutDiffTargetRefresh(target);
|
|
2767
2950
|
});
|
|
2768
2951
|
let watcher = null;
|
|
2952
|
+
let watcherIsRecursive = false;
|
|
2769
2953
|
try {
|
|
2770
|
-
|
|
2954
|
+
if (shouldTryRecursive) {
|
|
2955
|
+
watcher = createWatcher(true);
|
|
2956
|
+
watcherIsRecursive = true;
|
|
2957
|
+
}
|
|
2958
|
+
else {
|
|
2959
|
+
watcher = createWatcher(false);
|
|
2960
|
+
}
|
|
2771
2961
|
}
|
|
2772
2962
|
catch (error) {
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2963
|
+
if (shouldTryRecursive) {
|
|
2964
|
+
try {
|
|
2965
|
+
watcher = createWatcher(false);
|
|
2966
|
+
this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff recursive watch unavailable; using non-recursive fallback");
|
|
2967
|
+
}
|
|
2968
|
+
catch (fallbackError) {
|
|
2969
|
+
this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
|
|
2970
|
+
}
|
|
2776
2971
|
}
|
|
2777
|
-
|
|
2778
|
-
this.sessionLogger.warn({ err:
|
|
2972
|
+
else {
|
|
2973
|
+
this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
|
|
2779
2974
|
}
|
|
2780
2975
|
}
|
|
2781
2976
|
if (!watcher) {
|
|
@@ -2785,11 +2980,11 @@ export class Session {
|
|
|
2785
2980
|
this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff watcher error");
|
|
2786
2981
|
});
|
|
2787
2982
|
target.watchers.push(watcher);
|
|
2788
|
-
if (
|
|
2789
|
-
|
|
2983
|
+
if (watchPath === repoWatchPath && watcherIsRecursive) {
|
|
2984
|
+
hasRecursiveRepoCoverage = true;
|
|
2790
2985
|
}
|
|
2791
2986
|
}
|
|
2792
|
-
const missingRepoCoverage =
|
|
2987
|
+
const missingRepoCoverage = !hasRecursiveRepoCoverage;
|
|
2793
2988
|
if (target.watchers.length === 0 || missingRepoCoverage) {
|
|
2794
2989
|
target.fallbackRefreshInterval = setInterval(() => {
|
|
2795
2990
|
this.scheduleCheckoutDiffTargetRefresh(target);
|
|
@@ -2800,7 +2995,7 @@ export class Session {
|
|
|
2800
2995
|
intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
|
|
2801
2996
|
reason: target.watchers.length === 0
|
|
2802
2997
|
? "no_watchers"
|
|
2803
|
-
: "
|
|
2998
|
+
: "missing_recursive_repo_root_coverage",
|
|
2804
2999
|
}, "Checkout diff watchers unavailable; using timed refresh fallback");
|
|
2805
3000
|
}
|
|
2806
3001
|
this.checkoutDiffTargets.set(targetKey, target);
|
|
@@ -3051,12 +3246,13 @@ export class Session {
|
|
|
3051
3246
|
async handleCheckoutPrStatusRequest(msg) {
|
|
3052
3247
|
const { cwd, requestId } = msg;
|
|
3053
3248
|
try {
|
|
3054
|
-
const
|
|
3249
|
+
const prStatus = await getPullRequestStatus(cwd);
|
|
3055
3250
|
this.emit({
|
|
3056
3251
|
type: "checkout_pr_status_response",
|
|
3057
3252
|
payload: {
|
|
3058
3253
|
cwd,
|
|
3059
|
-
status,
|
|
3254
|
+
status: prStatus.status,
|
|
3255
|
+
githubFeaturesEnabled: prStatus.githubFeaturesEnabled,
|
|
3060
3256
|
error: null,
|
|
3061
3257
|
requestId,
|
|
3062
3258
|
},
|
|
@@ -3068,6 +3264,7 @@ export class Session {
|
|
|
3068
3264
|
payload: {
|
|
3069
3265
|
cwd,
|
|
3070
3266
|
status: null,
|
|
3267
|
+
githubFeaturesEnabled: true,
|
|
3071
3268
|
error: this.toCheckoutError(error),
|
|
3072
3269
|
requestId,
|
|
3073
3270
|
},
|
|
@@ -3223,185 +3420,6 @@ export class Session {
|
|
|
3223
3420
|
});
|
|
3224
3421
|
}
|
|
3225
3422
|
}
|
|
3226
|
-
/**
|
|
3227
|
-
* Handle highlighted diff request - returns parsed and syntax-highlighted diff
|
|
3228
|
-
*/
|
|
3229
|
-
async handleHighlightedDiffRequest(agentId, requestId) {
|
|
3230
|
-
this.sessionLogger.debug({ agentId }, `Handling highlighted diff request for agent ${agentId}`);
|
|
3231
|
-
// Maximum lines changed before we skip showing the diff content
|
|
3232
|
-
const MAX_DIFF_LINES = 5000;
|
|
3233
|
-
try {
|
|
3234
|
-
const agents = this.agentManager.listAgents();
|
|
3235
|
-
const agent = agents.find((a) => a.id === agentId);
|
|
3236
|
-
if (!agent) {
|
|
3237
|
-
this.emit({
|
|
3238
|
-
type: "highlighted_diff_response",
|
|
3239
|
-
payload: {
|
|
3240
|
-
agentId,
|
|
3241
|
-
files: [],
|
|
3242
|
-
error: `Agent not found: ${agentId}`,
|
|
3243
|
-
requestId,
|
|
3244
|
-
},
|
|
3245
|
-
});
|
|
3246
|
-
return;
|
|
3247
|
-
}
|
|
3248
|
-
// Step 1: Get the list of changed files with their stats (numstat gives additions/deletions per file)
|
|
3249
|
-
const { stdout: numstatOutput } = await execAsync("git diff --numstat HEAD", { cwd: agent.cwd });
|
|
3250
|
-
// Get file statuses (A=added, D=deleted, M=modified) to detect deleted files
|
|
3251
|
-
const { stdout: nameStatusOutput } = await execAsync("git diff --name-status HEAD", { cwd: agent.cwd });
|
|
3252
|
-
const deletedFiles = new Set();
|
|
3253
|
-
const addedFiles = new Set();
|
|
3254
|
-
for (const line of nameStatusOutput.trim().split("\n").filter(Boolean)) {
|
|
3255
|
-
const [status, ...pathParts] = line.split("\t");
|
|
3256
|
-
const path = pathParts.join("\t");
|
|
3257
|
-
if (status === "D") {
|
|
3258
|
-
deletedFiles.add(path);
|
|
3259
|
-
}
|
|
3260
|
-
else if (status === "A") {
|
|
3261
|
-
addedFiles.add(path);
|
|
3262
|
-
}
|
|
3263
|
-
}
|
|
3264
|
-
const fileStats = [];
|
|
3265
|
-
for (const line of numstatOutput.trim().split("\n").filter(Boolean)) {
|
|
3266
|
-
const parts = line.split("\t");
|
|
3267
|
-
if (parts.length >= 3) {
|
|
3268
|
-
const [addStr, delStr, ...pathParts] = parts;
|
|
3269
|
-
const path = pathParts.join("\t"); // Handle paths with tabs
|
|
3270
|
-
const isBinary = addStr === "-" && delStr === "-";
|
|
3271
|
-
fileStats.push({
|
|
3272
|
-
path,
|
|
3273
|
-
additions: isBinary ? 0 : parseInt(addStr, 10),
|
|
3274
|
-
deletions: isBinary ? 0 : parseInt(delStr, 10),
|
|
3275
|
-
isBinary,
|
|
3276
|
-
isTracked: true,
|
|
3277
|
-
isDeleted: deletedFiles.has(path),
|
|
3278
|
-
isNew: addedFiles.has(path),
|
|
3279
|
-
});
|
|
3280
|
-
}
|
|
3281
|
-
}
|
|
3282
|
-
// Step 2: Get untracked files
|
|
3283
|
-
try {
|
|
3284
|
-
const { stdout: untrackedFiles } = await execAsync("git ls-files --others --exclude-standard", { cwd: agent.cwd });
|
|
3285
|
-
for (const filePath of untrackedFiles.trim().split("\n").filter(Boolean)) {
|
|
3286
|
-
// Use git's numstat with --no-index to detect binary files (cross-platform)
|
|
3287
|
-
// Binary files show as "-\t-\tfilepath", text files show line counts
|
|
3288
|
-
try {
|
|
3289
|
-
const { stdout: numstatLine } = await execAsync(`git diff --numstat --no-index /dev/null "${filePath}" || true`, { cwd: agent.cwd });
|
|
3290
|
-
const parts = numstatLine.trim().split("\t");
|
|
3291
|
-
const isBinary = parts[0] === "-" && parts[1] === "-";
|
|
3292
|
-
const additions = isBinary ? 0 : (parseInt(parts[0], 10) || 0);
|
|
3293
|
-
fileStats.push({
|
|
3294
|
-
path: filePath,
|
|
3295
|
-
additions,
|
|
3296
|
-
deletions: 0,
|
|
3297
|
-
isBinary,
|
|
3298
|
-
isTracked: false,
|
|
3299
|
-
isDeleted: false,
|
|
3300
|
-
isNew: true,
|
|
3301
|
-
});
|
|
3302
|
-
}
|
|
3303
|
-
catch {
|
|
3304
|
-
// If we can't determine, assume text and try to get it
|
|
3305
|
-
fileStats.push({
|
|
3306
|
-
path: filePath,
|
|
3307
|
-
additions: 0,
|
|
3308
|
-
deletions: 0,
|
|
3309
|
-
isBinary: false,
|
|
3310
|
-
isTracked: false,
|
|
3311
|
-
isDeleted: false,
|
|
3312
|
-
isNew: true,
|
|
3313
|
-
});
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3317
|
-
catch {
|
|
3318
|
-
// Ignore errors getting untracked files
|
|
3319
|
-
}
|
|
3320
|
-
// Step 3: Fetch diffs per-file, respecting limits
|
|
3321
|
-
const allFiles = [];
|
|
3322
|
-
for (const stats of fileStats) {
|
|
3323
|
-
const totalLines = stats.additions + stats.deletions;
|
|
3324
|
-
// Handle binary files
|
|
3325
|
-
if (stats.isBinary) {
|
|
3326
|
-
allFiles.push({
|
|
3327
|
-
path: stats.path,
|
|
3328
|
-
isNew: stats.isNew,
|
|
3329
|
-
isDeleted: stats.isDeleted,
|
|
3330
|
-
additions: 0,
|
|
3331
|
-
deletions: 0,
|
|
3332
|
-
hunks: [],
|
|
3333
|
-
status: "binary",
|
|
3334
|
-
});
|
|
3335
|
-
continue;
|
|
3336
|
-
}
|
|
3337
|
-
// Handle files that are too large
|
|
3338
|
-
if (totalLines > MAX_DIFF_LINES) {
|
|
3339
|
-
allFiles.push({
|
|
3340
|
-
path: stats.path,
|
|
3341
|
-
isNew: stats.isNew,
|
|
3342
|
-
isDeleted: stats.isDeleted,
|
|
3343
|
-
additions: stats.additions,
|
|
3344
|
-
deletions: stats.deletions,
|
|
3345
|
-
hunks: [],
|
|
3346
|
-
status: "too_large",
|
|
3347
|
-
});
|
|
3348
|
-
continue;
|
|
3349
|
-
}
|
|
3350
|
-
// Fetch the actual diff for this file
|
|
3351
|
-
try {
|
|
3352
|
-
let fileDiff;
|
|
3353
|
-
if (stats.isTracked) {
|
|
3354
|
-
const { stdout } = await execAsync(`git diff HEAD -- "${stats.path}"`, { cwd: agent.cwd });
|
|
3355
|
-
fileDiff = stdout;
|
|
3356
|
-
}
|
|
3357
|
-
else {
|
|
3358
|
-
const { stdout } = await execAsync(`git diff --no-index /dev/null "${stats.path}" || true`, { cwd: agent.cwd });
|
|
3359
|
-
fileDiff = stdout;
|
|
3360
|
-
}
|
|
3361
|
-
if (fileDiff) {
|
|
3362
|
-
const parsedFiles = await parseAndHighlightDiff(fileDiff, agent.cwd);
|
|
3363
|
-
for (const file of parsedFiles) {
|
|
3364
|
-
allFiles.push({ ...file, status: "ok" });
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
}
|
|
3368
|
-
catch {
|
|
3369
|
-
// If diff fails for this file, add it with empty hunks
|
|
3370
|
-
allFiles.push({
|
|
3371
|
-
path: stats.path,
|
|
3372
|
-
isNew: stats.isNew,
|
|
3373
|
-
isDeleted: stats.isDeleted,
|
|
3374
|
-
additions: stats.additions,
|
|
3375
|
-
deletions: stats.deletions,
|
|
3376
|
-
hunks: [],
|
|
3377
|
-
status: "ok",
|
|
3378
|
-
});
|
|
3379
|
-
}
|
|
3380
|
-
}
|
|
3381
|
-
this.emit({
|
|
3382
|
-
type: "highlighted_diff_response",
|
|
3383
|
-
payload: {
|
|
3384
|
-
agentId,
|
|
3385
|
-
files: allFiles,
|
|
3386
|
-
error: null,
|
|
3387
|
-
requestId,
|
|
3388
|
-
},
|
|
3389
|
-
});
|
|
3390
|
-
this.sessionLogger.debug({ agentId, fileCount: allFiles.length }, `Highlighted diff for agent ${agentId} completed (${allFiles.length} files)`);
|
|
3391
|
-
}
|
|
3392
|
-
catch (error) {
|
|
3393
|
-
this.sessionLogger.error({ err: error, agentId }, `Failed to get highlighted diff for agent ${agentId}`);
|
|
3394
|
-
this.emit({
|
|
3395
|
-
type: "highlighted_diff_response",
|
|
3396
|
-
payload: {
|
|
3397
|
-
agentId,
|
|
3398
|
-
files: [],
|
|
3399
|
-
error: error.message,
|
|
3400
|
-
requestId,
|
|
3401
|
-
},
|
|
3402
|
-
});
|
|
3403
|
-
}
|
|
3404
|
-
}
|
|
3405
3423
|
/**
|
|
3406
3424
|
* Handle read-only file explorer requests scoped to an agent's cwd
|
|
3407
3425
|
*/
|
|
@@ -3737,6 +3755,77 @@ export class Session {
|
|
|
3737
3755
|
payload: { requestId, agent, error: null },
|
|
3738
3756
|
});
|
|
3739
3757
|
}
|
|
3758
|
+
async handleFetchAgentTimelineRequest(msg) {
|
|
3759
|
+
const direction = msg.direction ?? (msg.cursor ? "after" : "tail");
|
|
3760
|
+
const projection = msg.projection ?? "projected";
|
|
3761
|
+
const limit = msg.limit ?? (direction === "after" ? 0 : undefined);
|
|
3762
|
+
const cursor = msg.cursor
|
|
3763
|
+
? {
|
|
3764
|
+
epoch: msg.cursor.epoch,
|
|
3765
|
+
seq: msg.cursor.seq,
|
|
3766
|
+
}
|
|
3767
|
+
: undefined;
|
|
3768
|
+
try {
|
|
3769
|
+
const snapshot = await this.ensureAgentLoaded(msg.agentId);
|
|
3770
|
+
const timeline = this.agentManager.fetchTimeline(msg.agentId, {
|
|
3771
|
+
direction,
|
|
3772
|
+
cursor,
|
|
3773
|
+
limit,
|
|
3774
|
+
});
|
|
3775
|
+
const projected = projectTimelineRows(timeline.rows, snapshot.provider, projection);
|
|
3776
|
+
const firstRow = timeline.rows[0];
|
|
3777
|
+
const lastRow = timeline.rows[timeline.rows.length - 1];
|
|
3778
|
+
const startCursor = firstRow
|
|
3779
|
+
? { epoch: timeline.epoch, seq: firstRow.seq }
|
|
3780
|
+
: null;
|
|
3781
|
+
const endCursor = lastRow
|
|
3782
|
+
? { epoch: timeline.epoch, seq: lastRow.seq }
|
|
3783
|
+
: null;
|
|
3784
|
+
this.emit({
|
|
3785
|
+
type: "fetch_agent_timeline_response",
|
|
3786
|
+
payload: {
|
|
3787
|
+
requestId: msg.requestId,
|
|
3788
|
+
agentId: msg.agentId,
|
|
3789
|
+
direction,
|
|
3790
|
+
projection,
|
|
3791
|
+
epoch: timeline.epoch,
|
|
3792
|
+
reset: timeline.reset,
|
|
3793
|
+
staleCursor: timeline.staleCursor,
|
|
3794
|
+
gap: timeline.gap,
|
|
3795
|
+
window: timeline.window,
|
|
3796
|
+
startCursor,
|
|
3797
|
+
endCursor,
|
|
3798
|
+
hasOlder: timeline.hasOlder,
|
|
3799
|
+
hasNewer: timeline.hasNewer,
|
|
3800
|
+
entries: projected,
|
|
3801
|
+
error: null,
|
|
3802
|
+
},
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3805
|
+
catch (error) {
|
|
3806
|
+
this.sessionLogger.error({ err: error, agentId: msg.agentId }, "Failed to handle fetch_agent_timeline_request");
|
|
3807
|
+
this.emit({
|
|
3808
|
+
type: "fetch_agent_timeline_response",
|
|
3809
|
+
payload: {
|
|
3810
|
+
requestId: msg.requestId,
|
|
3811
|
+
agentId: msg.agentId,
|
|
3812
|
+
direction,
|
|
3813
|
+
projection,
|
|
3814
|
+
epoch: "",
|
|
3815
|
+
reset: false,
|
|
3816
|
+
staleCursor: false,
|
|
3817
|
+
gap: false,
|
|
3818
|
+
window: { minSeq: 0, maxSeq: 0, nextSeq: 0 },
|
|
3819
|
+
startCursor: null,
|
|
3820
|
+
endCursor: null,
|
|
3821
|
+
hasOlder: false,
|
|
3822
|
+
hasNewer: false,
|
|
3823
|
+
entries: [],
|
|
3824
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3825
|
+
},
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3740
3829
|
async handleSendAgentMessageRequest(msg) {
|
|
3741
3830
|
const resolved = await this.resolveAgentIdentifier(msg.agentId);
|
|
3742
3831
|
if (!resolved.ok) {
|
|
@@ -3825,7 +3914,7 @@ export class Session {
|
|
|
3825
3914
|
if (!resolved.ok) {
|
|
3826
3915
|
this.emit({
|
|
3827
3916
|
type: "wait_for_finish_response",
|
|
3828
|
-
payload: { requestId, status: "error", final: null, error: resolved.error },
|
|
3917
|
+
payload: { requestId, status: "error", final: null, error: resolved.error, lastMessage: null },
|
|
3829
3918
|
});
|
|
3830
3919
|
return;
|
|
3831
3920
|
}
|
|
@@ -3841,6 +3930,7 @@ export class Session {
|
|
|
3841
3930
|
status: "error",
|
|
3842
3931
|
final: null,
|
|
3843
3932
|
error: `Agent not found: ${agentId}`,
|
|
3933
|
+
lastMessage: null,
|
|
3844
3934
|
},
|
|
3845
3935
|
});
|
|
3846
3936
|
return;
|
|
@@ -3853,7 +3943,7 @@ export class Session {
|
|
|
3853
3943
|
: "idle";
|
|
3854
3944
|
this.emit({
|
|
3855
3945
|
type: "wait_for_finish_response",
|
|
3856
|
-
payload: { requestId, status, final, error: null },
|
|
3946
|
+
payload: { requestId, status, final, error: null, lastMessage: null },
|
|
3857
3947
|
});
|
|
3858
3948
|
return;
|
|
3859
3949
|
}
|
|
@@ -3877,7 +3967,7 @@ export class Session {
|
|
|
3877
3967
|
: "idle";
|
|
3878
3968
|
this.emit({
|
|
3879
3969
|
type: "wait_for_finish_response",
|
|
3880
|
-
payload: { requestId, status, final, error: null },
|
|
3970
|
+
payload: { requestId, status, final, error: null, lastMessage: result.lastMessage },
|
|
3881
3971
|
});
|
|
3882
3972
|
}
|
|
3883
3973
|
catch (error) {
|
|
@@ -3894,6 +3984,7 @@ export class Session {
|
|
|
3894
3984
|
status: "error",
|
|
3895
3985
|
final,
|
|
3896
3986
|
error: message,
|
|
3987
|
+
lastMessage: null,
|
|
3897
3988
|
},
|
|
3898
3989
|
});
|
|
3899
3990
|
return;
|
|
@@ -3904,37 +3995,13 @@ export class Session {
|
|
|
3904
3995
|
}
|
|
3905
3996
|
this.emit({
|
|
3906
3997
|
type: "wait_for_finish_response",
|
|
3907
|
-
payload: { requestId, status: "timeout", final, error: null },
|
|
3998
|
+
payload: { requestId, status: "timeout", final, error: null, lastMessage: null },
|
|
3908
3999
|
});
|
|
3909
4000
|
}
|
|
3910
4001
|
finally {
|
|
3911
4002
|
clearTimeout(timeoutHandle);
|
|
3912
4003
|
}
|
|
3913
4004
|
}
|
|
3914
|
-
emitAgentTimelineSnapshot(agent) {
|
|
3915
|
-
const timeline = this.agentManager.getTimeline(agent.id);
|
|
3916
|
-
const events = timeline.flatMap((item) => {
|
|
3917
|
-
const serializedEvent = serializeAgentStreamEvent({
|
|
3918
|
-
type: "timeline",
|
|
3919
|
-
provider: agent.provider,
|
|
3920
|
-
item,
|
|
3921
|
-
});
|
|
3922
|
-
if (!serializedEvent) {
|
|
3923
|
-
return [];
|
|
3924
|
-
}
|
|
3925
|
-
return [
|
|
3926
|
-
{
|
|
3927
|
-
event: serializedEvent,
|
|
3928
|
-
timestamp: new Date().toISOString(),
|
|
3929
|
-
},
|
|
3930
|
-
];
|
|
3931
|
-
});
|
|
3932
|
-
this.emit({
|
|
3933
|
-
type: "agent_stream_snapshot",
|
|
3934
|
-
payload: { agentId: agent.id, events },
|
|
3935
|
-
});
|
|
3936
|
-
return timeline.length;
|
|
3937
|
-
}
|
|
3938
4005
|
/**
|
|
3939
4006
|
* Handle audio chunk for buffering and transcription
|
|
3940
4007
|
*/
|
|
@@ -4386,11 +4453,22 @@ export class Session {
|
|
|
4386
4453
|
}
|
|
4387
4454
|
this.onMessage(msg);
|
|
4388
4455
|
}
|
|
4456
|
+
emitBinary(frame) {
|
|
4457
|
+
if (!this.onBinaryMessage) {
|
|
4458
|
+
return;
|
|
4459
|
+
}
|
|
4460
|
+
try {
|
|
4461
|
+
this.onBinaryMessage(frame);
|
|
4462
|
+
}
|
|
4463
|
+
catch (error) {
|
|
4464
|
+
this.sessionLogger.error({ err: error }, "Failed to emit binary frame");
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4389
4467
|
/**
|
|
4390
4468
|
* Clean up session resources
|
|
4391
4469
|
*/
|
|
4392
4470
|
async cleanup() {
|
|
4393
|
-
this.sessionLogger.
|
|
4471
|
+
this.sessionLogger.trace("Cleaning up");
|
|
4394
4472
|
if (this.unsubscribeAgentEvents) {
|
|
4395
4473
|
this.unsubscribeAgentEvents();
|
|
4396
4474
|
this.unsubscribeAgentEvents = null;
|
|
@@ -4428,6 +4506,7 @@ export class Session {
|
|
|
4428
4506
|
unsubscribe();
|
|
4429
4507
|
}
|
|
4430
4508
|
this.terminalSubscriptions.clear();
|
|
4509
|
+
this.detachAllTerminalStreams({ emitExit: false });
|
|
4431
4510
|
for (const target of this.checkoutDiffTargets.values()) {
|
|
4432
4511
|
this.closeCheckoutDiffWatchTarget(target);
|
|
4433
4512
|
}
|
|
@@ -4601,6 +4680,10 @@ export class Session {
|
|
|
4601
4680
|
unsubscribe();
|
|
4602
4681
|
this.terminalSubscriptions.delete(msg.terminalId);
|
|
4603
4682
|
}
|
|
4683
|
+
const streamId = this.terminalStreamByTerminalId.get(msg.terminalId);
|
|
4684
|
+
if (typeof streamId === "number") {
|
|
4685
|
+
this.detachTerminalStream(streamId, { emitExit: true });
|
|
4686
|
+
}
|
|
4604
4687
|
this.terminalManager.killTerminal(msg.terminalId);
|
|
4605
4688
|
this.emit({
|
|
4606
4689
|
type: "kill_terminal_response",
|
|
@@ -4611,5 +4694,213 @@ export class Session {
|
|
|
4611
4694
|
},
|
|
4612
4695
|
});
|
|
4613
4696
|
}
|
|
4697
|
+
async handleAttachTerminalStreamRequest(msg) {
|
|
4698
|
+
if (!this.terminalManager || !this.onBinaryMessage) {
|
|
4699
|
+
this.emit({
|
|
4700
|
+
type: "attach_terminal_stream_response",
|
|
4701
|
+
payload: {
|
|
4702
|
+
terminalId: msg.terminalId,
|
|
4703
|
+
streamId: null,
|
|
4704
|
+
replayedFrom: 0,
|
|
4705
|
+
currentOffset: 0,
|
|
4706
|
+
earliestAvailableOffset: 0,
|
|
4707
|
+
reset: true,
|
|
4708
|
+
error: "Terminal streaming not available",
|
|
4709
|
+
requestId: msg.requestId,
|
|
4710
|
+
},
|
|
4711
|
+
});
|
|
4712
|
+
return;
|
|
4713
|
+
}
|
|
4714
|
+
const session = this.terminalManager.getTerminal(msg.terminalId);
|
|
4715
|
+
if (!session) {
|
|
4716
|
+
this.emit({
|
|
4717
|
+
type: "attach_terminal_stream_response",
|
|
4718
|
+
payload: {
|
|
4719
|
+
terminalId: msg.terminalId,
|
|
4720
|
+
streamId: null,
|
|
4721
|
+
replayedFrom: 0,
|
|
4722
|
+
currentOffset: 0,
|
|
4723
|
+
earliestAvailableOffset: 0,
|
|
4724
|
+
reset: true,
|
|
4725
|
+
error: "Terminal not found",
|
|
4726
|
+
requestId: msg.requestId,
|
|
4727
|
+
},
|
|
4728
|
+
});
|
|
4729
|
+
return;
|
|
4730
|
+
}
|
|
4731
|
+
if (msg.rows || msg.cols) {
|
|
4732
|
+
const state = session.getState();
|
|
4733
|
+
session.send({
|
|
4734
|
+
type: "resize",
|
|
4735
|
+
rows: msg.rows ?? state.rows,
|
|
4736
|
+
cols: msg.cols ?? state.cols,
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4739
|
+
const existingStreamId = this.terminalStreamByTerminalId.get(msg.terminalId);
|
|
4740
|
+
if (typeof existingStreamId === "number") {
|
|
4741
|
+
this.detachTerminalStream(existingStreamId, { emitExit: false });
|
|
4742
|
+
}
|
|
4743
|
+
const streamId = this.allocateTerminalStreamId();
|
|
4744
|
+
const initialOffset = Math.max(0, Math.floor(msg.resumeOffset ?? 0));
|
|
4745
|
+
const binding = {
|
|
4746
|
+
terminalId: msg.terminalId,
|
|
4747
|
+
unsubscribe: () => { },
|
|
4748
|
+
lastOutputOffset: initialOffset,
|
|
4749
|
+
lastAckOffset: initialOffset,
|
|
4750
|
+
pendingChunks: [],
|
|
4751
|
+
pendingBytes: 0,
|
|
4752
|
+
};
|
|
4753
|
+
this.terminalStreams.set(streamId, binding);
|
|
4754
|
+
this.terminalStreamByTerminalId.set(msg.terminalId, streamId);
|
|
4755
|
+
let rawSub;
|
|
4756
|
+
try {
|
|
4757
|
+
rawSub = session.subscribeRaw((chunk) => {
|
|
4758
|
+
const currentBinding = this.terminalStreams.get(streamId);
|
|
4759
|
+
if (!currentBinding) {
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4762
|
+
this.enqueueOrEmitTerminalStreamChunk(streamId, currentBinding, {
|
|
4763
|
+
data: chunk.data,
|
|
4764
|
+
startOffset: chunk.startOffset,
|
|
4765
|
+
endOffset: chunk.endOffset,
|
|
4766
|
+
replay: chunk.replay,
|
|
4767
|
+
});
|
|
4768
|
+
}, { fromOffset: msg.resumeOffset ?? 0 });
|
|
4769
|
+
}
|
|
4770
|
+
catch (error) {
|
|
4771
|
+
this.terminalStreams.delete(streamId);
|
|
4772
|
+
this.terminalStreamByTerminalId.delete(msg.terminalId);
|
|
4773
|
+
throw error;
|
|
4774
|
+
}
|
|
4775
|
+
binding.unsubscribe = rawSub.unsubscribe;
|
|
4776
|
+
binding.lastAckOffset = rawSub.replayedFrom;
|
|
4777
|
+
if (binding.lastOutputOffset < rawSub.replayedFrom) {
|
|
4778
|
+
binding.lastOutputOffset = rawSub.replayedFrom;
|
|
4779
|
+
}
|
|
4780
|
+
this.flushPendingTerminalStreamChunks(streamId, binding);
|
|
4781
|
+
this.emit({
|
|
4782
|
+
type: "attach_terminal_stream_response",
|
|
4783
|
+
payload: {
|
|
4784
|
+
terminalId: msg.terminalId,
|
|
4785
|
+
streamId,
|
|
4786
|
+
replayedFrom: rawSub.replayedFrom,
|
|
4787
|
+
currentOffset: rawSub.currentOffset,
|
|
4788
|
+
earliestAvailableOffset: rawSub.earliestAvailableOffset,
|
|
4789
|
+
reset: rawSub.reset,
|
|
4790
|
+
error: null,
|
|
4791
|
+
requestId: msg.requestId,
|
|
4792
|
+
},
|
|
4793
|
+
});
|
|
4794
|
+
}
|
|
4795
|
+
getTerminalStreamChunkByteLength(chunk) {
|
|
4796
|
+
return Math.max(0, chunk.endOffset - chunk.startOffset);
|
|
4797
|
+
}
|
|
4798
|
+
canEmitTerminalStreamChunk(binding, chunk) {
|
|
4799
|
+
return chunk.startOffset < binding.lastAckOffset + TERMINAL_STREAM_WINDOW_BYTES;
|
|
4800
|
+
}
|
|
4801
|
+
emitTerminalStreamChunk(streamId, binding, chunk) {
|
|
4802
|
+
const payload = new Uint8Array(Buffer.from(chunk.data, "utf8"));
|
|
4803
|
+
this.emitBinary({
|
|
4804
|
+
channel: BinaryMuxChannel.Terminal,
|
|
4805
|
+
messageType: TerminalBinaryMessageType.OutputUtf8,
|
|
4806
|
+
streamId,
|
|
4807
|
+
offset: chunk.startOffset,
|
|
4808
|
+
flags: chunk.replay ? TerminalBinaryFlags.Replay : 0,
|
|
4809
|
+
payload,
|
|
4810
|
+
});
|
|
4811
|
+
binding.lastOutputOffset = chunk.endOffset;
|
|
4812
|
+
}
|
|
4813
|
+
enqueueOrEmitTerminalStreamChunk(streamId, binding, chunk) {
|
|
4814
|
+
const chunkBytes = this.getTerminalStreamChunkByteLength(chunk);
|
|
4815
|
+
if (binding.pendingChunks.length > 0 || !this.canEmitTerminalStreamChunk(binding, chunk)) {
|
|
4816
|
+
if (binding.pendingChunks.length >= TERMINAL_STREAM_MAX_PENDING_CHUNKS ||
|
|
4817
|
+
binding.pendingBytes + chunkBytes > TERMINAL_STREAM_MAX_PENDING_BYTES) {
|
|
4818
|
+
this.sessionLogger.warn({
|
|
4819
|
+
streamId,
|
|
4820
|
+
pendingChunks: binding.pendingChunks.length,
|
|
4821
|
+
pendingBytes: binding.pendingBytes,
|
|
4822
|
+
chunkBytes,
|
|
4823
|
+
}, "Terminal stream pending buffer overflow; closing stream");
|
|
4824
|
+
this.detachTerminalStream(streamId, { emitExit: true });
|
|
4825
|
+
return;
|
|
4826
|
+
}
|
|
4827
|
+
binding.pendingChunks.push(chunk);
|
|
4828
|
+
binding.pendingBytes += chunkBytes;
|
|
4829
|
+
return;
|
|
4830
|
+
}
|
|
4831
|
+
this.emitTerminalStreamChunk(streamId, binding, chunk);
|
|
4832
|
+
}
|
|
4833
|
+
flushPendingTerminalStreamChunks(streamId, binding) {
|
|
4834
|
+
while (binding.pendingChunks.length > 0) {
|
|
4835
|
+
const next = binding.pendingChunks[0];
|
|
4836
|
+
if (!next || !this.canEmitTerminalStreamChunk(binding, next)) {
|
|
4837
|
+
break;
|
|
4838
|
+
}
|
|
4839
|
+
binding.pendingChunks.shift();
|
|
4840
|
+
binding.pendingBytes -= this.getTerminalStreamChunkByteLength(next);
|
|
4841
|
+
if (binding.pendingBytes < 0) {
|
|
4842
|
+
binding.pendingBytes = 0;
|
|
4843
|
+
}
|
|
4844
|
+
this.emitTerminalStreamChunk(streamId, binding, next);
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
handleDetachTerminalStreamRequest(msg) {
|
|
4848
|
+
const success = this.detachTerminalStream(msg.streamId, { emitExit: false });
|
|
4849
|
+
this.emit({
|
|
4850
|
+
type: "detach_terminal_stream_response",
|
|
4851
|
+
payload: {
|
|
4852
|
+
streamId: msg.streamId,
|
|
4853
|
+
success,
|
|
4854
|
+
requestId: msg.requestId,
|
|
4855
|
+
},
|
|
4856
|
+
});
|
|
4857
|
+
}
|
|
4858
|
+
detachAllTerminalStreams(options) {
|
|
4859
|
+
for (const streamId of Array.from(this.terminalStreams.keys())) {
|
|
4860
|
+
this.detachTerminalStream(streamId, options);
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
detachTerminalStream(streamId, options) {
|
|
4864
|
+
const binding = this.terminalStreams.get(streamId);
|
|
4865
|
+
if (!binding) {
|
|
4866
|
+
return false;
|
|
4867
|
+
}
|
|
4868
|
+
try {
|
|
4869
|
+
binding.unsubscribe();
|
|
4870
|
+
}
|
|
4871
|
+
catch (error) {
|
|
4872
|
+
this.sessionLogger.warn({ err: error, streamId }, "Failed to unsubscribe terminal stream");
|
|
4873
|
+
}
|
|
4874
|
+
this.terminalStreams.delete(streamId);
|
|
4875
|
+
if (this.terminalStreamByTerminalId.get(binding.terminalId) === streamId) {
|
|
4876
|
+
this.terminalStreamByTerminalId.delete(binding.terminalId);
|
|
4877
|
+
}
|
|
4878
|
+
if (options?.emitExit) {
|
|
4879
|
+
this.emit({
|
|
4880
|
+
type: "terminal_stream_exit",
|
|
4881
|
+
payload: {
|
|
4882
|
+
streamId,
|
|
4883
|
+
terminalId: binding.terminalId,
|
|
4884
|
+
},
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4887
|
+
return true;
|
|
4888
|
+
}
|
|
4889
|
+
allocateTerminalStreamId() {
|
|
4890
|
+
let attempts = 0;
|
|
4891
|
+
while (attempts < 0xffffffff) {
|
|
4892
|
+
const candidate = this.nextTerminalStreamId >>> 0;
|
|
4893
|
+
this.nextTerminalStreamId = ((this.nextTerminalStreamId + 1) & 0xffffffff) >>> 0;
|
|
4894
|
+
if (candidate === 0) {
|
|
4895
|
+
attempts += 1;
|
|
4896
|
+
continue;
|
|
4897
|
+
}
|
|
4898
|
+
if (!this.terminalStreams.has(candidate)) {
|
|
4899
|
+
return candidate;
|
|
4900
|
+
}
|
|
4901
|
+
attempts += 1;
|
|
4902
|
+
}
|
|
4903
|
+
throw new Error("Unable to allocate terminal stream id");
|
|
4904
|
+
}
|
|
4614
4905
|
}
|
|
4615
4906
|
//# sourceMappingURL=session.js.map
|