@getpaseo/server 0.1.3 → 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 +44 -32
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +463 -774
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +45 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +197 -9
- 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-mapper.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js +120 -6
- 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 +83 -9
- 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 +25 -0
- package/dist/server/server/agent/providers/codex-app-server-agent.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 -3
- 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 -18
- 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 +81 -14
- package/dist/server/server/dictation/dictation-stream-manager.js.map +1 -1
- package/dist/server/server/exports.d.ts +1 -1
- package/dist/server/server/exports.d.ts.map +1 -1
- package/dist/server/server/persisted-config.d.ts +4 -4
- 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 +21 -5
- package/dist/server/server/relay-transport.js.map +1 -1
- package/dist/server/server/session.d.ts +32 -8
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +499 -79
- 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/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 +4 -3
- 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 +3 -66
- package/dist/server/server/speech/providers/local/sherpa/model-downloader.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 +466 -112
- package/dist/server/server/speech/speech-runtime.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 -64
- 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 +4655 -3615
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +184 -26
- 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 +4 -0
- package/dist/server/utils/checkout-git.d.ts.map +1 -1
- package/dist/server/utils/checkout-git.js +92 -0
- 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,6 +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 { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from "../shared/binary-mux.js";
|
|
9
10
|
import { TTSManager } from "./agent/tts-manager.js";
|
|
10
11
|
import { STTManager } from "./agent/stt-manager.js";
|
|
11
12
|
import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
|
|
@@ -16,13 +17,14 @@ import { experimental_createMCPClient } from "ai";
|
|
|
16
17
|
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
|
17
18
|
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
|
|
18
19
|
import { toAgentPayload } from "./agent/agent-projections.js";
|
|
20
|
+
import { projectTimelineRows } from "./agent/timeline-projection.js";
|
|
19
21
|
import { StructuredAgentResponseError, generateStructuredAgentResponse, } from "./agent/agent-response-loop.js";
|
|
20
22
|
import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
|
|
21
23
|
import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
|
|
22
24
|
import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
|
|
23
25
|
import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
|
|
24
26
|
import { createWorktree, runWorktreeSetupCommands, WorktreeSetupError, slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
|
|
25
|
-
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";
|
|
26
28
|
import { getProjectIcon } from "../utils/project-icon.js";
|
|
27
29
|
import { expandTilde } from "../utils/path.js";
|
|
28
30
|
import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from "./speech/providers/local/models.js";
|
|
@@ -39,6 +41,9 @@ const PROJECT_PLACEMENT_CACHE_TTL_MS = 10000;
|
|
|
39
41
|
const MAX_AGENTS_PER_PROJECT = 5;
|
|
40
42
|
const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
|
|
41
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;
|
|
42
47
|
/**
|
|
43
48
|
* Default model used for auto-generating commit messages and PR descriptions.
|
|
44
49
|
* Uses Claude Haiku for speed and cost efficiency.
|
|
@@ -116,6 +121,15 @@ const VOICE_INTERNAL_DICTATION_ID_PREFIX = "__voice_turn__:";
|
|
|
116
121
|
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
|
|
117
122
|
const AgentIdSchema = z.string().uuid();
|
|
118
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
|
+
}
|
|
119
133
|
function convertPCMToWavBuffer(pcmBuffer, sampleRate, channels, bitsPerSample) {
|
|
120
134
|
const headerSize = 44;
|
|
121
135
|
const wavBuffer = Buffer.alloc(headerSize + pcmBuffer.length);
|
|
@@ -199,14 +213,18 @@ export class Session {
|
|
|
199
213
|
this.clientActivity = null;
|
|
200
214
|
this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
|
|
201
215
|
this.terminalSubscriptions = new Map();
|
|
216
|
+
this.terminalStreams = new Map();
|
|
217
|
+
this.terminalStreamByTerminalId = new Map();
|
|
218
|
+
this.nextTerminalStreamId = 1;
|
|
202
219
|
this.checkoutDiffSubscriptions = new Map();
|
|
203
220
|
this.checkoutDiffTargets = new Map();
|
|
204
221
|
this.voiceModeAgentId = null;
|
|
205
222
|
this.voiceModeBaseConfig = null;
|
|
206
|
-
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;
|
|
207
224
|
this.clientId = clientId;
|
|
208
225
|
this.sessionId = uuidv4();
|
|
209
226
|
this.onMessage = onMessage;
|
|
227
|
+
this.onBinaryMessage = onBinaryMessage ?? null;
|
|
210
228
|
this.downloadTokenStore = downloadTokenStore;
|
|
211
229
|
this.pushTokenStore = pushTokenStore;
|
|
212
230
|
this.paseoHome = paseoHome;
|
|
@@ -230,6 +248,7 @@ export class Session {
|
|
|
230
248
|
this.unregisterVoiceCallerContext = voiceBridge?.unregisterVoiceCallerContext;
|
|
231
249
|
this.ensureVoiceMcpSocketForAgent = voiceBridge?.ensureVoiceMcpSocketForAgent;
|
|
232
250
|
this.removeVoiceMcpSocketForAgent = voiceBridge?.removeVoiceMcpSocketForAgent;
|
|
251
|
+
this.getSpeechReadiness = dictation?.getSpeechReadiness;
|
|
233
252
|
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
|
234
253
|
this.abortController = new AbortController();
|
|
235
254
|
this.sessionLogger = logger.child({
|
|
@@ -415,9 +434,7 @@ export class Session {
|
|
|
415
434
|
}
|
|
416
435
|
// Reduce bandwidth/CPU on mobile: only forward high-frequency agent stream events
|
|
417
436
|
// for the focused agent, with a short grace window while backgrounded.
|
|
418
|
-
//
|
|
419
|
-
// History catch-up is handled via explicit `initialize_agent_request` which emits a
|
|
420
|
-
// batched `agent_stream_snapshot`.
|
|
437
|
+
// History catch-up is handled via pull-based `fetch_agent_timeline_request`.
|
|
421
438
|
const activity = this.clientActivity;
|
|
422
439
|
if (activity?.deviceType === "mobile") {
|
|
423
440
|
if (!activity.focusedAgentId) {
|
|
@@ -441,6 +458,8 @@ export class Session {
|
|
|
441
458
|
agentId: event.agentId,
|
|
442
459
|
event: serializedEvent,
|
|
443
460
|
timestamp: new Date().toISOString(),
|
|
461
|
+
...(typeof event.seq === "number" ? { seq: event.seq } : {}),
|
|
462
|
+
...(typeof event.epoch === "string" ? { epoch: event.epoch } : {}),
|
|
444
463
|
};
|
|
445
464
|
this.emit({
|
|
446
465
|
type: "agent_stream",
|
|
@@ -699,6 +718,22 @@ export class Session {
|
|
|
699
718
|
await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
|
|
700
719
|
break;
|
|
701
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
|
+
}
|
|
702
737
|
await this.dictationStreamManager.handleStart(msg.dictationId, msg.format);
|
|
703
738
|
break;
|
|
704
739
|
case "dictation_stream_chunk":
|
|
@@ -730,8 +765,8 @@ export class Session {
|
|
|
730
765
|
case "restart_server_request":
|
|
731
766
|
await this.handleRestartServerRequest(msg.requestId, msg.reason);
|
|
732
767
|
break;
|
|
733
|
-
case "
|
|
734
|
-
await this.
|
|
768
|
+
case "fetch_agent_timeline_request":
|
|
769
|
+
await this.handleFetchAgentTimelineRequest(msg);
|
|
735
770
|
break;
|
|
736
771
|
case "set_agent_mode_request":
|
|
737
772
|
await this.handleSetAgentModeRequest(msg.agentId, msg.modeId, msg.requestId);
|
|
@@ -751,6 +786,9 @@ export class Session {
|
|
|
751
786
|
case "validate_branch_request":
|
|
752
787
|
await this.handleValidateBranchRequest(msg);
|
|
753
788
|
break;
|
|
789
|
+
case "branch_suggestions_request":
|
|
790
|
+
await this.handleBranchSuggestionsRequest(msg);
|
|
791
|
+
break;
|
|
754
792
|
case "subscribe_checkout_diff_request":
|
|
755
793
|
await this.handleSubscribeCheckoutDiffRequest(msg);
|
|
756
794
|
break;
|
|
@@ -848,6 +886,12 @@ export class Session {
|
|
|
848
886
|
case "kill_terminal_request":
|
|
849
887
|
await this.handleKillTerminalRequest(msg);
|
|
850
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;
|
|
851
895
|
}
|
|
852
896
|
}
|
|
853
897
|
catch (error) {
|
|
@@ -881,6 +925,58 @@ export class Session {
|
|
|
881
925
|
});
|
|
882
926
|
}
|
|
883
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
|
+
}
|
|
884
980
|
async handleRestartServerRequest(requestId, reason) {
|
|
885
981
|
if (restartRequested) {
|
|
886
982
|
this.sessionLogger.debug("Restart already requested, ignoring duplicate");
|
|
@@ -1032,12 +1128,57 @@ export class Session {
|
|
|
1032
1128
|
});
|
|
1033
1129
|
}
|
|
1034
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
|
+
}
|
|
1035
1172
|
/**
|
|
1036
1173
|
* Handle voice mode toggle
|
|
1037
1174
|
*/
|
|
1038
1175
|
async handleSetVoiceMode(enabled, agentId, requestId) {
|
|
1039
1176
|
try {
|
|
1040
1177
|
if (enabled) {
|
|
1178
|
+
const unavailable = this.resolveVoiceFeatureUnavailableContext("voice_mode");
|
|
1179
|
+
if (unavailable) {
|
|
1180
|
+
throw new VoiceFeatureUnavailableError(unavailable);
|
|
1181
|
+
}
|
|
1041
1182
|
const normalizedAgentId = this.parseVoiceTargetAgentId(agentId ?? "", "set_voice_mode");
|
|
1042
1183
|
if (this.isVoiceMode &&
|
|
1043
1184
|
this.voiceModeAgentId &&
|
|
@@ -1084,6 +1225,7 @@ export class Session {
|
|
|
1084
1225
|
}
|
|
1085
1226
|
catch (error) {
|
|
1086
1227
|
const errorMessage = error instanceof Error ? error.message : "Failed to set voice mode";
|
|
1228
|
+
const unavailable = this.getVoiceFeatureUnavailableResponseMetadata(error);
|
|
1087
1229
|
this.sessionLogger.error({
|
|
1088
1230
|
err: error,
|
|
1089
1231
|
enabled,
|
|
@@ -1098,6 +1240,7 @@ export class Session {
|
|
|
1098
1240
|
agentId: this.voiceModeAgentId,
|
|
1099
1241
|
accepted: false,
|
|
1100
1242
|
error: errorMessage,
|
|
1243
|
+
...unavailable,
|
|
1101
1244
|
},
|
|
1102
1245
|
});
|
|
1103
1246
|
return;
|
|
@@ -1410,37 +1553,6 @@ export class Session {
|
|
|
1410
1553
|
}
|
|
1411
1554
|
this.startAgentStream(agentId, prompt, runOptions);
|
|
1412
1555
|
}
|
|
1413
|
-
/**
|
|
1414
|
-
* Handle on-demand agent initialization request from client
|
|
1415
|
-
*/
|
|
1416
|
-
async handleInitializeAgentRequest(agentId, requestId) {
|
|
1417
|
-
try {
|
|
1418
|
-
const snapshot = await this.ensureAgentLoaded(agentId);
|
|
1419
|
-
await this.forwardAgentUpdate(snapshot);
|
|
1420
|
-
// Send timeline snapshot after hydration (if any)
|
|
1421
|
-
const timelineSize = this.emitAgentTimelineSnapshot(snapshot);
|
|
1422
|
-
this.emit({
|
|
1423
|
-
type: "initialize_agent_request",
|
|
1424
|
-
payload: {
|
|
1425
|
-
agentId,
|
|
1426
|
-
agentStatus: snapshot.lifecycle,
|
|
1427
|
-
timelineSize,
|
|
1428
|
-
requestId,
|
|
1429
|
-
},
|
|
1430
|
-
});
|
|
1431
|
-
}
|
|
1432
|
-
catch (error) {
|
|
1433
|
-
this.sessionLogger.error({ err: error, agentId }, `Failed to initialize agent ${agentId}`);
|
|
1434
|
-
this.emit({
|
|
1435
|
-
type: "initialize_agent_request",
|
|
1436
|
-
payload: {
|
|
1437
|
-
agentId,
|
|
1438
|
-
requestId,
|
|
1439
|
-
error: error?.message ?? "Failed to initialize agent",
|
|
1440
|
-
},
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
1556
|
/**
|
|
1445
1557
|
* Handle create agent request
|
|
1446
1558
|
*/
|
|
@@ -1541,7 +1653,7 @@ export class Session {
|
|
|
1541
1653
|
const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
|
|
1542
1654
|
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
|
|
1543
1655
|
await this.forwardAgentUpdate(snapshot);
|
|
1544
|
-
const timelineSize = this.
|
|
1656
|
+
const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
|
|
1545
1657
|
if (requestId) {
|
|
1546
1658
|
const agentPayload = await this.getAgentPayloadById(snapshot.id);
|
|
1547
1659
|
if (!agentPayload) {
|
|
@@ -1600,7 +1712,7 @@ export class Session {
|
|
|
1600
1712
|
}
|
|
1601
1713
|
await this.agentManager.hydrateTimelineFromProvider(agentId);
|
|
1602
1714
|
await this.forwardAgentUpdate(snapshot);
|
|
1603
|
-
const timelineSize = this.
|
|
1715
|
+
const timelineSize = this.agentManager.getTimeline(agentId).length;
|
|
1604
1716
|
if (requestId) {
|
|
1605
1717
|
this.emit({
|
|
1606
1718
|
type: "status",
|
|
@@ -2627,6 +2739,31 @@ export class Session {
|
|
|
2627
2739
|
});
|
|
2628
2740
|
}
|
|
2629
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
|
+
}
|
|
2630
2767
|
normalizeCheckoutDiffCompare(compare) {
|
|
2631
2768
|
if (compare.mode === "uncommitted") {
|
|
2632
2769
|
return { mode: "uncommitted" };
|
|
@@ -2798,30 +2935,42 @@ export class Session {
|
|
|
2798
2935
|
latestPayload: null,
|
|
2799
2936
|
latestFingerprint: null,
|
|
2800
2937
|
};
|
|
2801
|
-
const
|
|
2802
|
-
|
|
2803
|
-
watchPaths.add(watchRoot);
|
|
2804
|
-
}
|
|
2938
|
+
const repoWatchPath = watchRoot ?? cwd;
|
|
2939
|
+
const watchPaths = new Set([repoWatchPath]);
|
|
2805
2940
|
const gitDir = await this.resolveCheckoutGitDir(cwd);
|
|
2806
2941
|
if (gitDir) {
|
|
2807
2942
|
watchPaths.add(gitDir);
|
|
2808
2943
|
}
|
|
2809
|
-
let
|
|
2944
|
+
let hasRecursiveRepoCoverage = false;
|
|
2945
|
+
const allowRecursiveRepoWatch = process.platform !== "linux";
|
|
2810
2946
|
for (const watchPath of watchPaths) {
|
|
2947
|
+
const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
|
|
2811
2948
|
const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
|
|
2812
2949
|
this.scheduleCheckoutDiffTargetRefresh(target);
|
|
2813
2950
|
});
|
|
2814
2951
|
let watcher = null;
|
|
2952
|
+
let watcherIsRecursive = false;
|
|
2815
2953
|
try {
|
|
2816
|
-
|
|
2954
|
+
if (shouldTryRecursive) {
|
|
2955
|
+
watcher = createWatcher(true);
|
|
2956
|
+
watcherIsRecursive = true;
|
|
2957
|
+
}
|
|
2958
|
+
else {
|
|
2959
|
+
watcher = createWatcher(false);
|
|
2960
|
+
}
|
|
2817
2961
|
}
|
|
2818
2962
|
catch (error) {
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
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
|
+
}
|
|
2822
2971
|
}
|
|
2823
|
-
|
|
2824
|
-
this.sessionLogger.warn({ err:
|
|
2972
|
+
else {
|
|
2973
|
+
this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Failed to start checkout diff watcher");
|
|
2825
2974
|
}
|
|
2826
2975
|
}
|
|
2827
2976
|
if (!watcher) {
|
|
@@ -2831,11 +2980,11 @@ export class Session {
|
|
|
2831
2980
|
this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, "Checkout diff watcher error");
|
|
2832
2981
|
});
|
|
2833
2982
|
target.watchers.push(watcher);
|
|
2834
|
-
if (
|
|
2835
|
-
|
|
2983
|
+
if (watchPath === repoWatchPath && watcherIsRecursive) {
|
|
2984
|
+
hasRecursiveRepoCoverage = true;
|
|
2836
2985
|
}
|
|
2837
2986
|
}
|
|
2838
|
-
const missingRepoCoverage =
|
|
2987
|
+
const missingRepoCoverage = !hasRecursiveRepoCoverage;
|
|
2839
2988
|
if (target.watchers.length === 0 || missingRepoCoverage) {
|
|
2840
2989
|
target.fallbackRefreshInterval = setInterval(() => {
|
|
2841
2990
|
this.scheduleCheckoutDiffTargetRefresh(target);
|
|
@@ -2846,7 +2995,7 @@ export class Session {
|
|
|
2846
2995
|
intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
|
|
2847
2996
|
reason: target.watchers.length === 0
|
|
2848
2997
|
? "no_watchers"
|
|
2849
|
-
: "
|
|
2998
|
+
: "missing_recursive_repo_root_coverage",
|
|
2850
2999
|
}, "Checkout diff watchers unavailable; using timed refresh fallback");
|
|
2851
3000
|
}
|
|
2852
3001
|
this.checkoutDiffTargets.set(targetKey, target);
|
|
@@ -3606,6 +3755,77 @@ export class Session {
|
|
|
3606
3755
|
payload: { requestId, agent, error: null },
|
|
3607
3756
|
});
|
|
3608
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
|
+
}
|
|
3609
3829
|
async handleSendAgentMessageRequest(msg) {
|
|
3610
3830
|
const resolved = await this.resolveAgentIdentifier(msg.agentId);
|
|
3611
3831
|
if (!resolved.ok) {
|
|
@@ -3782,30 +4002,6 @@ export class Session {
|
|
|
3782
4002
|
clearTimeout(timeoutHandle);
|
|
3783
4003
|
}
|
|
3784
4004
|
}
|
|
3785
|
-
emitAgentTimelineSnapshot(agent) {
|
|
3786
|
-
const timeline = this.agentManager.getTimeline(agent.id);
|
|
3787
|
-
const events = timeline.flatMap((item) => {
|
|
3788
|
-
const serializedEvent = serializeAgentStreamEvent({
|
|
3789
|
-
type: "timeline",
|
|
3790
|
-
provider: agent.provider,
|
|
3791
|
-
item,
|
|
3792
|
-
});
|
|
3793
|
-
if (!serializedEvent) {
|
|
3794
|
-
return [];
|
|
3795
|
-
}
|
|
3796
|
-
return [
|
|
3797
|
-
{
|
|
3798
|
-
event: serializedEvent,
|
|
3799
|
-
timestamp: new Date().toISOString(),
|
|
3800
|
-
},
|
|
3801
|
-
];
|
|
3802
|
-
});
|
|
3803
|
-
this.emit({
|
|
3804
|
-
type: "agent_stream_snapshot",
|
|
3805
|
-
payload: { agentId: agent.id, events },
|
|
3806
|
-
});
|
|
3807
|
-
return timeline.length;
|
|
3808
|
-
}
|
|
3809
4005
|
/**
|
|
3810
4006
|
* Handle audio chunk for buffering and transcription
|
|
3811
4007
|
*/
|
|
@@ -4257,6 +4453,17 @@ export class Session {
|
|
|
4257
4453
|
}
|
|
4258
4454
|
this.onMessage(msg);
|
|
4259
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
|
+
}
|
|
4260
4467
|
/**
|
|
4261
4468
|
* Clean up session resources
|
|
4262
4469
|
*/
|
|
@@ -4299,6 +4506,7 @@ export class Session {
|
|
|
4299
4506
|
unsubscribe();
|
|
4300
4507
|
}
|
|
4301
4508
|
this.terminalSubscriptions.clear();
|
|
4509
|
+
this.detachAllTerminalStreams({ emitExit: false });
|
|
4302
4510
|
for (const target of this.checkoutDiffTargets.values()) {
|
|
4303
4511
|
this.closeCheckoutDiffWatchTarget(target);
|
|
4304
4512
|
}
|
|
@@ -4472,6 +4680,10 @@ export class Session {
|
|
|
4472
4680
|
unsubscribe();
|
|
4473
4681
|
this.terminalSubscriptions.delete(msg.terminalId);
|
|
4474
4682
|
}
|
|
4683
|
+
const streamId = this.terminalStreamByTerminalId.get(msg.terminalId);
|
|
4684
|
+
if (typeof streamId === "number") {
|
|
4685
|
+
this.detachTerminalStream(streamId, { emitExit: true });
|
|
4686
|
+
}
|
|
4475
4687
|
this.terminalManager.killTerminal(msg.terminalId);
|
|
4476
4688
|
this.emit({
|
|
4477
4689
|
type: "kill_terminal_response",
|
|
@@ -4482,5 +4694,213 @@ export class Session {
|
|
|
4482
4694
|
},
|
|
4483
4695
|
});
|
|
4484
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
|
+
}
|
|
4485
4905
|
}
|
|
4486
4906
|
//# sourceMappingURL=session.js.map
|