@progmruansilva/omp-web 1.202608.0
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/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/cli.js +1015 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/assets/CodeViewer-Da66baHj.js +4 -0
- package/dist/client/assets/TerminalPanel-Bj9an79F.js +126 -0
- package/dist/client/assets/UnifiedDiffViewer-eK_kynTO.js +34 -0
- package/dist/client/assets/index-DytaeWNd.js +3070 -0
- package/dist/client/assets/vendor-editor-core-CL2gOCIg.js +12 -0
- package/dist/client/assets/vendor-editor-languages-zOM9I4iZ.js +26 -0
- package/dist/client/assets/vendor-editor-legacy-B4QLsWF8.js +1 -0
- package/dist/client/assets/vendor-terminal-DDGTF8rc.css +1 -0
- package/dist/client/assets/vendor-terminal-DjQ08hXu.js +16 -0
- package/dist/client/favicon-16x16.png +0 -0
- package/dist/client/favicon-180x180.png +0 -0
- package/dist/client/favicon-192x192.png +0 -0
- package/dist/client/favicon-32x32.png +0 -0
- package/dist/client/favicon-512x512.png +0 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/favicon.svg +11 -0
- package/dist/client/index.html +67 -0
- package/dist/client/manifest.webmanifest +36 -0
- package/dist/config.js +272 -0
- package/dist/config.js.map +1 -0
- package/dist/docker/ompWebDockerCommandPlan.js +211 -0
- package/dist/docker/ompWebDockerCommandPlan.js.map +1 -0
- package/dist/omp-web-plugins/info/omp-web-plugin.js +51 -0
- package/dist/omp-web-plugins/info/package.json +9 -0
- package/dist/omp-web-plugins/schedule-prompts/omp-web-plugin.js +43 -0
- package/dist/omp-web-plugins/schedule-prompts/package.json +9 -0
- package/dist/omp-web-plugins/schedule-prompts/schedulePromptsPanelElement.js +242 -0
- package/dist/omp-web-plugins/schedule-prompts/types.js +2 -0
- package/dist/omp-web-plugins/updates/omp-web-plugin.js +173 -0
- package/dist/omp-web-plugins/updates/package.json +9 -0
- package/dist/omp-web-plugins/updates/updatesLogic.js +97 -0
- package/dist/ompWebVersionReport.js +217 -0
- package/dist/ompWebVersionReport.js.map +1 -0
- package/dist/plugin-api/unstable.d.ts +22 -0
- package/dist/plugin-api.d.ts +197 -0
- package/dist/server/activity/workspaceActivityRoutes.js +4 -0
- package/dist/server/activity/workspaceActivityRoutes.js.map +1 -0
- package/dist/server/activity/workspaceActivityService.js +98 -0
- package/dist/server/activity/workspaceActivityService.js.map +1 -0
- package/dist/server/app.js +162 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/configRoutes.js +270 -0
- package/dist/server/configRoutes.js.map +1 -0
- package/dist/server/diagnostics/nodePtySpawnHelper.js +130 -0
- package/dist/server/diagnostics/nodePtySpawnHelper.js.map +1 -0
- package/dist/server/git/gitEnv.js +15 -0
- package/dist/server/git/gitEnv.js.map +1 -0
- package/dist/server/git/gitService.js +119 -0
- package/dist/server/git/gitService.js.map +1 -0
- package/dist/server/gitRoutes.js +23 -0
- package/dist/server/gitRoutes.js.map +1 -0
- package/dist/server/index.js +7 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/machines/machineClient.js +162 -0
- package/dist/server/machines/machineClient.js.map +1 -0
- package/dist/server/machines/machinePluginProxyRoutes.js +157 -0
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -0
- package/dist/server/machines/machineProxyRoutes.js +127 -0
- package/dist/server/machines/machineProxyRoutes.js.map +1 -0
- package/dist/server/machines/machineRoutes.js +54 -0
- package/dist/server/machines/machineRoutes.js.map +1 -0
- package/dist/server/machines/machineService.js +230 -0
- package/dist/server/machines/machineService.js.map +1 -0
- package/dist/server/machines/machineStore.js +123 -0
- package/dist/server/machines/machineStore.js.map +1 -0
- package/dist/server/machines/proxyUtils.js +30 -0
- package/dist/server/machines/proxyUtils.js.map +1 -0
- package/dist/server/ompWebPluginService.js +279 -0
- package/dist/server/ompWebPluginService.js.map +1 -0
- package/dist/server/ompWebStatus.js +601 -0
- package/dist/server/ompWebStatus.js.map +1 -0
- package/dist/server/ompWebStatusCache.js +31 -0
- package/dist/server/ompWebStatusCache.js.map +1 -0
- package/dist/server/piPackageRoutes.js +82 -0
- package/dist/server/piPackageRoutes.js.map +1 -0
- package/dist/server/piPackageService.js +65 -0
- package/dist/server/piPackageService.js.map +1 -0
- package/dist/server/projects/directorySuggestions.js +37 -0
- package/dist/server/projects/directorySuggestions.js.map +1 -0
- package/dist/server/projects/projectService.js +31 -0
- package/dist/server/projects/projectService.js.map +1 -0
- package/dist/server/push/PushNotificationService.js +137 -0
- package/dist/server/push/PushNotificationService.js.map +1 -0
- package/dist/server/push/pushRoutes.js +55 -0
- package/dist/server/push/pushRoutes.js.map +1 -0
- package/dist/server/realtime/sessionEventHub.js +39 -0
- package/dist/server/realtime/sessionEventHub.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +61 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -0
- package/dist/server/sessiond.js +92 -0
- package/dist/server/sessiond.js.map +1 -0
- package/dist/server/sessions/attachmentService.js +112 -0
- package/dist/server/sessions/attachmentService.js.map +1 -0
- package/dist/server/sessions/authProviderOptions.js +52 -0
- package/dist/server/sessions/authProviderOptions.js.map +1 -0
- package/dist/server/sessions/authRoutes.js +59 -0
- package/dist/server/sessions/authRoutes.js.map +1 -0
- package/dist/server/sessions/authService.js +105 -0
- package/dist/server/sessions/authService.js.map +1 -0
- package/dist/server/sessions/builtinCommands.js +29 -0
- package/dist/server/sessions/builtinCommands.js.map +1 -0
- package/dist/server/sessions/editPreview.js +196 -0
- package/dist/server/sessions/editPreview.js.map +1 -0
- package/dist/server/sessions/messagePaging.js +41 -0
- package/dist/server/sessions/messagePaging.js.map +1 -0
- package/dist/server/sessions/oauthLoginFlowService.js +136 -0
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +97 -0
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +2401 -0
- package/dist/server/sessions/piSessionService.js.map +1 -0
- package/dist/server/sessions/schedulePrompt/schedulePromptService.js +46 -0
- package/dist/server/sessions/schedulePrompt/schedulePromptService.js.map +1 -0
- package/dist/server/sessions/schedulePrompt/scheduler.js +315 -0
- package/dist/server/sessions/schedulePrompt/scheduler.js.map +1 -0
- package/dist/server/sessions/schedulePrompt/storage.js +118 -0
- package/dist/server/sessions/schedulePrompt/storage.js.map +1 -0
- package/dist/server/sessions/schedulePrompt/tool.js +252 -0
- package/dist/server/sessions/schedulePrompt/tool.js.map +1 -0
- package/dist/server/sessions/schedulePrompt/types.js +2 -0
- package/dist/server/sessions/schedulePrompt/types.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +254 -0
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -0
- package/dist/server/sessions/sessionArchiveTree.js +35 -0
- package/dist/server/sessions/sessionArchiveTree.js.map +1 -0
- package/dist/server/sessions/sessionCleanup.js +157 -0
- package/dist/server/sessions/sessionCleanup.js.map +1 -0
- package/dist/server/sessions/sessionCommandService.js +260 -0
- package/dist/server/sessions/sessionCommandService.js.map +1 -0
- package/dist/server/sessions/sessionNameGenerator.js +61 -0
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +344 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -0
- package/dist/server/sessions/sessionRuntimeStore.js +2 -0
- package/dist/server/sessions/sessionRuntimeStore.js.map +1 -0
- package/dist/server/sessions/spawnSessionTool.js +41 -0
- package/dist/server/sessions/spawnSessionTool.js.map +1 -0
- package/dist/server/sessions/spawnSubsessionTool.js +203 -0
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -0
- package/dist/server/sessions/spawnTargetResolver.js +36 -0
- package/dist/server/sessions/spawnTargetResolver.js.map +1 -0
- package/dist/server/sessions/subsessionTranscript.js +233 -0
- package/dist/server/sessions/subsessionTranscript.js.map +1 -0
- package/dist/server/storage/projectStore.js +83 -0
- package/dist/server/storage/projectStore.js.map +1 -0
- package/dist/server/terminalProxyRoutes.js +140 -0
- package/dist/server/terminalProxyRoutes.js.map +1 -0
- package/dist/server/terminals/terminalRoutes.js +153 -0
- package/dist/server/terminals/terminalRoutes.js.map +1 -0
- package/dist/server/terminals/terminalService.js +320 -0
- package/dist/server/terminals/terminalService.js.map +1 -0
- package/dist/server/terminals/terminalSize.js +17 -0
- package/dist/server/terminals/terminalSize.js.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server/utils.js +8 -0
- package/dist/server/utils.js.map +1 -0
- package/dist/server/webSocketBridge.js +32 -0
- package/dist/server/webSocketBridge.js.map +1 -0
- package/dist/server/workingDirectory.js +44 -0
- package/dist/server/workingDirectory.js.map +1 -0
- package/dist/server/workspaceExplorerRoutes.js +109 -0
- package/dist/server/workspaceExplorerRoutes.js.map +1 -0
- package/dist/server/workspaces/effectivePathAccess.js +27 -0
- package/dist/server/workspaces/effectivePathAccess.js.map +1 -0
- package/dist/server/workspaces/fileContentService.js +181 -0
- package/dist/server/workspaces/fileContentService.js.map +1 -0
- package/dist/server/workspaces/fileSuggestions.js +512 -0
- package/dist/server/workspaces/fileSuggestions.js.map +1 -0
- package/dist/server/workspaces/fileTreeService.js +27 -0
- package/dist/server/workspaces/fileTreeService.js.map +1 -0
- package/dist/server/workspaces/gitWorktreeDiscovery.js +34 -0
- package/dist/server/workspaces/gitWorktreeDiscovery.js.map +1 -0
- package/dist/server/workspaces/imagePreviewService.js +40 -0
- package/dist/server/workspaces/imagePreviewService.js.map +1 -0
- package/dist/server/workspaces/pathAccessPolicy.js +87 -0
- package/dist/server/workspaces/pathAccessPolicy.js.map +1 -0
- package/dist/server/workspaces/pathSafety.js +54 -0
- package/dist/server/workspaces/pathSafety.js.map +1 -0
- package/dist/server/workspaces/projectOmpWebConfig.js +56 -0
- package/dist/server/workspaces/projectOmpWebConfig.js.map +1 -0
- package/dist/server/workspaces/workspaceContext.js +8 -0
- package/dist/server/workspaces/workspaceContext.js.map +1 -0
- package/dist/server/workspaces/workspaceDeletionRoutes.js +111 -0
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -0
- package/dist/server/workspaces/workspaceService.js +39 -0
- package/dist/server/workspaces/workspaceService.js.map +1 -0
- package/dist/sessiond/config.js +9 -0
- package/dist/sessiond/config.js.map +1 -0
- package/dist/sessiond/sessionDaemonClient.js +65 -0
- package/dist/sessiond/sessionDaemonClient.js.map +1 -0
- package/dist/shared/activity.js +26 -0
- package/dist/shared/activity.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +688 -0
- package/dist/shared/apiTypes.js +11 -0
- package/dist/shared/apiTypes.js.map +1 -0
- package/dist/shared/capabilities.js +52 -0
- package/dist/shared/capabilities.js.map +1 -0
- package/dist/shared/federatedRoutes.js +79 -0
- package/dist/shared/federatedRoutes.js.map +1 -0
- package/dist/shared/machinePluginIds.js +41 -0
- package/dist/shared/machinePluginIds.js.map +1 -0
- package/dist/shared/ompWebStatusParsing.js +100 -0
- package/dist/shared/ompWebStatusParsing.js.map +1 -0
- package/dist/shared/pluginIds.js +5 -0
- package/dist/shared/pluginIds.js.map +1 -0
- package/dist/shared/promptAttachments.js +90 -0
- package/dist/shared/promptAttachments.js.map +1 -0
- package/dist/shared/thinkingLevels.d.ts +19 -0
- package/dist/shared/thinkingLevels.js +24 -0
- package/dist/shared/thinkingLevels.js.map +1 -0
- package/dist/shared/utils.js +4 -0
- package/dist/shared/utils.js.map +1 -0
- package/dist/shared/workspaceDeletion.js +12 -0
- package/dist/shared/workspaceDeletion.js.map +1 -0
- package/dist/shared/workspaceFiles.js +3 -0
- package/dist/shared/workspaceFiles.js.map +1 -0
- package/docs/assets/favicon.svg +11 -0
- package/docs/assets/omp-web-banner.png +0 -0
- package/docs/assets/omp-web-desktop.png +0 -0
- package/docs/assets/omp-web-mobile.png +0 -0
- package/docs/assets/omp-web-tablet.png +0 -0
- package/docs/config.md +208 -0
- package/docs/plugins.md +916 -0
- package/extensions/omp-web.ts +133 -0
- package/install.sh +5 -0
- package/package.json +141 -0
- package/plugin-api/unstable.d.ts +1 -0
- package/plugin-api.d.ts +1 -0
|
@@ -0,0 +1,2401 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { open, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createAgentSession, getAgentDir, SessionManager, Settings, } from "@oh-my-pi/pi-coding-agent";
|
|
4
|
+
import { EditTool } from "@oh-my-pi/pi-coding-agent/edit";
|
|
5
|
+
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
6
|
+
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
7
|
+
import { SessionCommandService } from "./sessionCommandService.js";
|
|
8
|
+
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
9
|
+
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
10
|
+
import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
11
|
+
import { computeEditPreview } from "./editPreview.js";
|
|
12
|
+
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
|
13
|
+
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
14
|
+
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
15
|
+
import { isKnownThinkingLevel } from "../../shared/thinkingLevels.js";
|
|
16
|
+
import { cwdPathsEqual } from "../workingDirectory.js";
|
|
17
|
+
import { errorMessage, isRecord } from "../utils.js";
|
|
18
|
+
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
19
|
+
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
20
|
+
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
21
|
+
import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
22
|
+
import { SchedulePromptService } from "./schedulePrompt/schedulePromptService.js";
|
|
23
|
+
import { createSchedulePromptToolDefinition } from "./schedulePrompt/tool.js";
|
|
24
|
+
const noopLogger = { info() { } };
|
|
25
|
+
function noop() {
|
|
26
|
+
// Intentionally empty default unsubscribe callback.
|
|
27
|
+
}
|
|
28
|
+
function spawnTargetError(decision) {
|
|
29
|
+
if (decision.reason === "not-registered")
|
|
30
|
+
return new Error("Spawning session is not in a registered project");
|
|
31
|
+
return new Error(`cwd must be a workspace of this project. Allowed: ${decision.allowedCwds.join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
function authLossWarningKey(sessionId, provider, modelId) {
|
|
34
|
+
return `${sessionId}:${provider}/${modelId}`;
|
|
35
|
+
}
|
|
36
|
+
function sessionIdFromLookup(ref) {
|
|
37
|
+
return typeof ref === "string" ? ref : ref.id;
|
|
38
|
+
}
|
|
39
|
+
function isPiSessionRef(ref) {
|
|
40
|
+
return typeof ref !== "string";
|
|
41
|
+
}
|
|
42
|
+
function lookupMatchesActiveSession(ref, active) {
|
|
43
|
+
return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
|
|
44
|
+
}
|
|
45
|
+
function requirePromptText(value) {
|
|
46
|
+
if (typeof value !== "string")
|
|
47
|
+
throw new Error("Prompt text is required");
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function parsePromptStreamingBehavior(value) {
|
|
51
|
+
if (value === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (value === "steer" || value === "followUp")
|
|
54
|
+
return value;
|
|
55
|
+
throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
|
|
56
|
+
}
|
|
57
|
+
class DefaultPiAgentSession {
|
|
58
|
+
constructor(ompSession, piSessionManager) {
|
|
59
|
+
this.ompSession = ompSession;
|
|
60
|
+
this.piSessionManager = piSessionManager;
|
|
61
|
+
this._isBashRunning = false;
|
|
62
|
+
}
|
|
63
|
+
get modelRegistry() { return this.ompSession.modelRegistry; }
|
|
64
|
+
get sessionManager() { return this.piSessionManager; }
|
|
65
|
+
get scopedModels() {
|
|
66
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unnecessary-type-assertion */
|
|
67
|
+
return this.ompSession.scopedModels;
|
|
68
|
+
/* eslint-enable @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unnecessary-type-assertion */
|
|
69
|
+
}
|
|
70
|
+
get sessionId() { return this.ompSession.sessionId; }
|
|
71
|
+
get sessionFile() { return this.ompSession.sessionFile; }
|
|
72
|
+
get sessionName() { return this.ompSession.sessionName; }
|
|
73
|
+
get messages() { return this.ompSession.messages; }
|
|
74
|
+
get model() { return this.ompSession.model; }
|
|
75
|
+
get thinkingLevel() {
|
|
76
|
+
const configured = this.ompSession.configuredThinkingLevel();
|
|
77
|
+
return configured !== undefined && isKnownThinkingLevel(configured) ? configured : "off";
|
|
78
|
+
}
|
|
79
|
+
get isStreaming() { return this.ompSession.isStreaming; }
|
|
80
|
+
get isCompacting() { return this.ompSession.isCompacting; }
|
|
81
|
+
get isBashRunning() { return this._isBashRunning; }
|
|
82
|
+
get pendingMessageCount() { return this.ompSession.queuedMessageCount; }
|
|
83
|
+
get extensionRunner() {
|
|
84
|
+
const runner = this.ompSession.extensionRunner;
|
|
85
|
+
return { getRegisteredCommands: () => runner?.getRegisteredCommands() ?? [] };
|
|
86
|
+
}
|
|
87
|
+
get promptTemplates() { return this.ompSession.promptTemplates; }
|
|
88
|
+
get resourceLoader() {
|
|
89
|
+
return {
|
|
90
|
+
getSkills: () => ({
|
|
91
|
+
skills: this.ompSession.skills.map((s) => ({ name: s.name, description: s.description })),
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
getPlanModeState() {
|
|
96
|
+
const fn = Reflect.get(this.ompSession, "getPlanModeState");
|
|
97
|
+
if (typeof fn !== "function")
|
|
98
|
+
return undefined;
|
|
99
|
+
const res = fn.call(this.ompSession);
|
|
100
|
+
return isRecord(res) && typeof res.enabled === "boolean" && typeof res.planFilePath === "string"
|
|
101
|
+
? { enabled: res.enabled, planFilePath: res.planFilePath }
|
|
102
|
+
: undefined;
|
|
103
|
+
}
|
|
104
|
+
setPlanModeState(state) {
|
|
105
|
+
const fn = Reflect.get(this.ompSession, "setPlanModeState");
|
|
106
|
+
if (typeof fn === "function")
|
|
107
|
+
fn.call(this.ompSession, state);
|
|
108
|
+
}
|
|
109
|
+
toggleAdvisorEnabled() {
|
|
110
|
+
const fn = Reflect.get(this.ompSession, "toggleAdvisorEnabled");
|
|
111
|
+
if (typeof fn !== "function")
|
|
112
|
+
return false;
|
|
113
|
+
const res = fn.call(this.ompSession);
|
|
114
|
+
return typeof res === "boolean" ? res : false;
|
|
115
|
+
}
|
|
116
|
+
setAdvisorEnabled(enabled) {
|
|
117
|
+
const fn = Reflect.get(this.ompSession, "setAdvisorEnabled");
|
|
118
|
+
if (typeof fn !== "function")
|
|
119
|
+
return false;
|
|
120
|
+
const res = fn.call(this.ompSession, enabled);
|
|
121
|
+
return typeof res === "boolean" ? res : false;
|
|
122
|
+
}
|
|
123
|
+
get agent() {
|
|
124
|
+
return { streamFn: this.ompSession.agent.streamFn };
|
|
125
|
+
}
|
|
126
|
+
subscribe(listener) {
|
|
127
|
+
return this.ompSession.subscribe(listener);
|
|
128
|
+
}
|
|
129
|
+
bindExtensions() {
|
|
130
|
+
// Handled via onExtensionError in createAgentSession options
|
|
131
|
+
return Promise.resolve();
|
|
132
|
+
}
|
|
133
|
+
async compact(instructions) {
|
|
134
|
+
const result = await this.ompSession.compact(instructions);
|
|
135
|
+
return { summary: result.summary, tokensBefore: 0 };
|
|
136
|
+
}
|
|
137
|
+
getUserMessagesForForking() {
|
|
138
|
+
return this.ompSession.getUserMessagesForBranching();
|
|
139
|
+
}
|
|
140
|
+
getSessionStats() {
|
|
141
|
+
const stats = this.ompSession.getSessionStats();
|
|
142
|
+
return {
|
|
143
|
+
sessionId: stats.sessionId,
|
|
144
|
+
totalMessages: stats.totalMessages,
|
|
145
|
+
userMessages: stats.userMessages,
|
|
146
|
+
assistantMessages: stats.assistantMessages,
|
|
147
|
+
toolCalls: stats.toolCalls,
|
|
148
|
+
tokens: stats.tokens,
|
|
149
|
+
cost: stats.cost,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async reload() {
|
|
153
|
+
// omp sessions manage persistence internally
|
|
154
|
+
}
|
|
155
|
+
getContextUsage() {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
async prompt(text, options) {
|
|
159
|
+
await this.ompSession.prompt(text, options);
|
|
160
|
+
}
|
|
161
|
+
async sendCustomMessage(message, options) {
|
|
162
|
+
await this.ompSession.sendCustomMessage(message, options);
|
|
163
|
+
}
|
|
164
|
+
async executeBash(command, onChunk, options) {
|
|
165
|
+
this._isBashRunning = true;
|
|
166
|
+
try {
|
|
167
|
+
const result = await this.ompSession.executeBash(command, onChunk, options);
|
|
168
|
+
return {
|
|
169
|
+
output: result.output,
|
|
170
|
+
exitCode: result.exitCode,
|
|
171
|
+
cancelled: result.cancelled,
|
|
172
|
+
truncated: result.truncated,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
this._isBashRunning = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async abort() {
|
|
180
|
+
await this.ompSession.abort();
|
|
181
|
+
}
|
|
182
|
+
clearQueue() {
|
|
183
|
+
const result = this.ompSession.clearQueue();
|
|
184
|
+
const extractText = (m) => {
|
|
185
|
+
if (typeof m === "string")
|
|
186
|
+
return m;
|
|
187
|
+
if (isRecord(m) && typeof m["text"] === "string")
|
|
188
|
+
return m["text"];
|
|
189
|
+
return "";
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
steering: result.steering.map(extractText),
|
|
193
|
+
followUp: result.followUp.map(extractText),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
getSteeringMessages() {
|
|
197
|
+
return this.ompSession.getQueuedMessages().steering;
|
|
198
|
+
}
|
|
199
|
+
getFollowUpMessages() {
|
|
200
|
+
return this.ompSession.getQueuedMessages().followUp;
|
|
201
|
+
}
|
|
202
|
+
async setModel(model) {
|
|
203
|
+
await this.ompSession.setModel(model);
|
|
204
|
+
}
|
|
205
|
+
setThinkingLevel(level) {
|
|
206
|
+
const setLevel = Reflect.get(this.ompSession, "setThinkingLevel");
|
|
207
|
+
if (typeof setLevel === "function") {
|
|
208
|
+
Reflect.apply(setLevel, this.ompSession, [level === "off" ? undefined : level]);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async cycleModel(direction) {
|
|
212
|
+
const result = await this.ompSession.cycleModel(direction);
|
|
213
|
+
if (result === undefined)
|
|
214
|
+
return undefined;
|
|
215
|
+
return { model: result.model };
|
|
216
|
+
}
|
|
217
|
+
getAvailableThinkingLevels() {
|
|
218
|
+
const efforts = this.ompSession.getAvailableThinkingLevels();
|
|
219
|
+
const levels = ["off"];
|
|
220
|
+
for (const effort of efforts) {
|
|
221
|
+
if (isKnownThinkingLevel(effort))
|
|
222
|
+
levels.push(effort);
|
|
223
|
+
}
|
|
224
|
+
return levels;
|
|
225
|
+
}
|
|
226
|
+
cycleThinkingLevel() {
|
|
227
|
+
const levels = this.ompSession.getAvailableThinkingLevels();
|
|
228
|
+
const fullCycle = ["off"];
|
|
229
|
+
for (const level of levels) {
|
|
230
|
+
if (isKnownThinkingLevel(level))
|
|
231
|
+
fullCycle.push(level);
|
|
232
|
+
}
|
|
233
|
+
const current = this.ompSession.configuredThinkingLevel();
|
|
234
|
+
if (current !== undefined && !isKnownThinkingLevel(current))
|
|
235
|
+
return undefined;
|
|
236
|
+
const currentLevel = current !== undefined && isKnownThinkingLevel(current) ? current : "off";
|
|
237
|
+
const currentIndex = fullCycle.indexOf(currentLevel);
|
|
238
|
+
if (currentIndex === -1)
|
|
239
|
+
return undefined;
|
|
240
|
+
const nextIndex = (currentIndex + 1) % fullCycle.length;
|
|
241
|
+
const next = fullCycle[nextIndex];
|
|
242
|
+
if (next === undefined)
|
|
243
|
+
return undefined;
|
|
244
|
+
this.setThinkingLevel(next);
|
|
245
|
+
return next;
|
|
246
|
+
}
|
|
247
|
+
setSessionName(name) {
|
|
248
|
+
void this.ompSession.setSessionName(name);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
class DefaultPiSessionRuntime {
|
|
252
|
+
constructor(session, cwd, result) {
|
|
253
|
+
this.result = result;
|
|
254
|
+
this.rebindSessionCallback = undefined;
|
|
255
|
+
this.session = session;
|
|
256
|
+
this.cwd = cwd;
|
|
257
|
+
this.ompSession = result.session;
|
|
258
|
+
}
|
|
259
|
+
setRebindSession(rebindSession) {
|
|
260
|
+
this.rebindSessionCallback = rebindSession;
|
|
261
|
+
}
|
|
262
|
+
async fork(entryId) {
|
|
263
|
+
return this.ompSession.branch(entryId);
|
|
264
|
+
}
|
|
265
|
+
async dispose() {
|
|
266
|
+
await this.ompSession.dispose();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
270
|
+
if (createRuntime === undefined)
|
|
271
|
+
throw new Error("Runtime factory is required");
|
|
272
|
+
return createRuntime(options);
|
|
273
|
+
}
|
|
274
|
+
function createDefaultRuntimeFactory(authStorage, modelRegistry, schedulePromptService, spawn, subsessions) {
|
|
275
|
+
return async ({ cwd, agentDir, sessionManager, initialModel }) => {
|
|
276
|
+
if (!(sessionManager instanceof SessionManager))
|
|
277
|
+
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
278
|
+
const model = initialModel;
|
|
279
|
+
const sessionId = sessionManager.getSessionId();
|
|
280
|
+
// Deferred refs — populated after piSession is created. The getters are
|
|
281
|
+
// only called at tool-execution time, long after these are assigned.
|
|
282
|
+
const scheduleStorageRef = {};
|
|
283
|
+
const scheduleSchedulerRef = {};
|
|
284
|
+
const scheduleStorage = () => {
|
|
285
|
+
if (scheduleStorageRef.storage === undefined)
|
|
286
|
+
throw new Error("Schedule storage uninitialized");
|
|
287
|
+
return scheduleStorageRef.storage;
|
|
288
|
+
};
|
|
289
|
+
const scheduleScheduler = () => {
|
|
290
|
+
if (scheduleSchedulerRef.scheduler === undefined)
|
|
291
|
+
throw new Error("Schedule scheduler uninitialized");
|
|
292
|
+
return scheduleSchedulerRef.scheduler;
|
|
293
|
+
};
|
|
294
|
+
const customTools = [
|
|
295
|
+
createSchedulePromptToolDefinition(sessionId, scheduleStorage, scheduleScheduler),
|
|
296
|
+
createOmpWebEditToolDefinition(cwd),
|
|
297
|
+
...(spawn === undefined ? [] : [createSpawnSessionToolDefinition(cwd, { spawn })]),
|
|
298
|
+
...(subsessions === undefined ? [] : createSubsessionToolDefinitions(cwd, subsessions)),
|
|
299
|
+
];
|
|
300
|
+
const result = await createAgentSession({
|
|
301
|
+
cwd,
|
|
302
|
+
agentDir,
|
|
303
|
+
authStorage,
|
|
304
|
+
modelRegistry,
|
|
305
|
+
sessionManager,
|
|
306
|
+
hasUI: true,
|
|
307
|
+
customTools,
|
|
308
|
+
...(model === undefined ? {} : { model }),
|
|
309
|
+
});
|
|
310
|
+
const piSession = new DefaultPiAgentSession(result.session, sessionManager);
|
|
311
|
+
const started = schedulePromptService.startForSession(sessionId, cwd, (text) => piSession.prompt(text, { streamingBehavior: "followUp" }));
|
|
312
|
+
scheduleStorageRef.storage = started.storage;
|
|
313
|
+
scheduleSchedulerRef.scheduler = started.scheduler;
|
|
314
|
+
return new DefaultPiSessionRuntime(piSession, cwd, result);
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function isEditReplacement(x) {
|
|
318
|
+
return isRecord(x);
|
|
319
|
+
}
|
|
320
|
+
function isEditParams(params) {
|
|
321
|
+
return isRecord(params) && typeof params["path"] === "string" && Array.isArray(params["edits"]);
|
|
322
|
+
}
|
|
323
|
+
function createOmpWebEditToolDefinition(cwd) {
|
|
324
|
+
const editTool = new EditTool({
|
|
325
|
+
cwd,
|
|
326
|
+
hasUI: true,
|
|
327
|
+
getSessionFile: () => null,
|
|
328
|
+
getSessionSpawns: () => null,
|
|
329
|
+
settings: Settings.isolated({}),
|
|
330
|
+
});
|
|
331
|
+
const def = {
|
|
332
|
+
name: editTool.name,
|
|
333
|
+
label: editTool.label,
|
|
334
|
+
description: editTool.description,
|
|
335
|
+
parameters: editTool.parameters,
|
|
336
|
+
async execute(toolCallId, params, signal, onUpdate) {
|
|
337
|
+
if (isRecord(params) && typeof params["path"] === "string" && params["path"].length > 0) {
|
|
338
|
+
const path = params["path"];
|
|
339
|
+
const rawEdits = Array.isArray(params["edits"]) ? params["edits"] : [];
|
|
340
|
+
const edits = [];
|
|
341
|
+
for (const item of rawEdits) {
|
|
342
|
+
if (isEditReplacement(item))
|
|
343
|
+
edits.push(item);
|
|
344
|
+
}
|
|
345
|
+
const preview = await computeEditPreview(path, edits, cwd);
|
|
346
|
+
if (signal?.aborted !== true) {
|
|
347
|
+
onUpdate?.({ content: [{ type: "text", text: "Edit preview computed." }], details: { preview } });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const execute = Reflect.get(editTool, "execute");
|
|
351
|
+
const executeParams = isEditParams(params) ? params : { path: "", edits: [] };
|
|
352
|
+
const res = execute.call(editTool, toolCallId, executeParams, signal, onUpdate);
|
|
353
|
+
return res;
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
return def;
|
|
357
|
+
}
|
|
358
|
+
export class PiSessionService {
|
|
359
|
+
constructor(events, deps = {}) {
|
|
360
|
+
this.events = events;
|
|
361
|
+
this.active = new Map();
|
|
362
|
+
this.activities = new Map();
|
|
363
|
+
this.compactionPromptQueues = new Map();
|
|
364
|
+
this.schedulePromptService = new SchedulePromptService();
|
|
365
|
+
this.compactionDrainTimers = new Map();
|
|
366
|
+
this.authLossWarnings = new Set();
|
|
367
|
+
/** Tracked subsession id -> the parent session id that spawned it. */
|
|
368
|
+
this.subsessionParents = new Map();
|
|
369
|
+
/** Parent session id -> the set of tracked subsession ids it spawned. */
|
|
370
|
+
this.subsessionChildren = new Map();
|
|
371
|
+
/** Tracked subsession id -> persisted recovery details for the child. */
|
|
372
|
+
this.subsessionLinks = new Map();
|
|
373
|
+
/** Parent id/file identities whose persisted links have already been loaded. */
|
|
374
|
+
this.subsessionHydratedParents = new Set();
|
|
375
|
+
/**
|
|
376
|
+
* Tracked subsession id -> whether a completion notification is armed.
|
|
377
|
+
* Armed when the child starts working; firing on completion disarms it so a
|
|
378
|
+
* child that works again (and stops again) notifies the parent each time.
|
|
379
|
+
*/
|
|
380
|
+
this.subsessionNotifyArmed = new Map();
|
|
381
|
+
/** Session ids for which we've already sent a "work complete" push notification. */
|
|
382
|
+
this.notifiedWorkComplete = new Set();
|
|
383
|
+
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
384
|
+
this.agentDir = deps.agentDir ?? getAgentDir();
|
|
385
|
+
this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
|
|
386
|
+
this.modelRegistry = deps.modelRegistry;
|
|
387
|
+
this.spawnTargets = deps.spawnTargets;
|
|
388
|
+
this.logger = deps.logger ?? noopLogger;
|
|
389
|
+
this.now = deps.now ?? (() => new Date());
|
|
390
|
+
this.pushService = deps.pushService;
|
|
391
|
+
// Subsessions are a beta capability gated behind their own flag, and they
|
|
392
|
+
// also require the spawn capability (they share its project-scope resolver).
|
|
393
|
+
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
394
|
+
this.createRuntime = deps.createRuntime
|
|
395
|
+
?? (this.modelRegistry !== undefined
|
|
396
|
+
? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, this.schedulePromptService, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
397
|
+
spawn: (input) => this.spawnSubsession(input),
|
|
398
|
+
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
399
|
+
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
400
|
+
read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
|
|
401
|
+
})
|
|
402
|
+
: undefined);
|
|
403
|
+
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
404
|
+
this.workspaceActivity = deps.workspaceActivity;
|
|
405
|
+
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
406
|
+
this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, { echoUserMessage: false }), events, {
|
|
407
|
+
onCompactionStart: (session) => {
|
|
408
|
+
this.publishActivity(session, "compacting", "active");
|
|
409
|
+
this.publishStatus(session);
|
|
410
|
+
},
|
|
411
|
+
onCompactionEnd: (session, result, detail) => {
|
|
412
|
+
this.publishActivity(session, result === "success" ? "compaction complete" : "compaction failed", result === "success" ? "idle" : "error", detail);
|
|
413
|
+
this.publishStatus(session);
|
|
414
|
+
},
|
|
415
|
+
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
416
|
+
}, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
|
|
417
|
+
}
|
|
418
|
+
activeCount() {
|
|
419
|
+
return this.active.size;
|
|
420
|
+
}
|
|
421
|
+
async cleanupPreview(request) {
|
|
422
|
+
return previewResponseFromPlan(await this.cleanupPlan(request));
|
|
423
|
+
}
|
|
424
|
+
async cleanup(request) {
|
|
425
|
+
const plan = await this.cleanupPlan(request);
|
|
426
|
+
if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
427
|
+
throw new Error("Archive store does not support deletion");
|
|
428
|
+
const archiveInputs = [];
|
|
429
|
+
const readyArchiveInputs = [];
|
|
430
|
+
const deleteRecords = [];
|
|
431
|
+
const readyDeleteRecords = [];
|
|
432
|
+
const skippedBusySessionIds = new Set(plan.skippedBusySessionIds);
|
|
433
|
+
for (const input of plan.archiveInputs) {
|
|
434
|
+
if (this.activeSessionHasWork(input.sessionId)) {
|
|
435
|
+
skippedBusySessionIds.add(input.sessionId);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
await this.closeActive(input.sessionId);
|
|
439
|
+
readyArchiveInputs.push(input);
|
|
440
|
+
}
|
|
441
|
+
await this.archiveStoreArchiveMany(readyArchiveInputs);
|
|
442
|
+
archiveInputs.push(...readyArchiveInputs);
|
|
443
|
+
for (const record of plan.deleteRecords) {
|
|
444
|
+
if (this.activeSessionHasWork(record.sessionId)) {
|
|
445
|
+
skippedBusySessionIds.add(record.sessionId);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
await this.closeActive(record.sessionId);
|
|
449
|
+
readyDeleteRecords.push(record);
|
|
450
|
+
}
|
|
451
|
+
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
452
|
+
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
453
|
+
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
454
|
+
return summarizeSessionCleanupExecution({
|
|
455
|
+
archiveInputs,
|
|
456
|
+
deleteRecords,
|
|
457
|
+
thresholds: plan.thresholds,
|
|
458
|
+
generatedAt: plan.generatedAt,
|
|
459
|
+
skippedBusySessionIds: [...skippedBusySessionIds],
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async dispose() {
|
|
463
|
+
this.schedulePromptService.dispose();
|
|
464
|
+
clearInterval(this.heartbeat);
|
|
465
|
+
this.clearCompactionDrainTimers();
|
|
466
|
+
const activeSessions = Array.from(new Set(this.active.values()));
|
|
467
|
+
this.active.clear();
|
|
468
|
+
this.activities.clear();
|
|
469
|
+
this.compactionPromptQueues.clear();
|
|
470
|
+
this.authLossWarnings.clear();
|
|
471
|
+
this.subsessionParents.clear();
|
|
472
|
+
this.subsessionChildren.clear();
|
|
473
|
+
this.subsessionLinks.clear();
|
|
474
|
+
this.subsessionHydratedParents.clear();
|
|
475
|
+
this.subsessionNotifyArmed.clear();
|
|
476
|
+
await Promise.all(activeSessions.map(async (active) => {
|
|
477
|
+
active.unsubscribe();
|
|
478
|
+
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
|
|
479
|
+
await active.runtime.session.abort();
|
|
480
|
+
await active.runtime.dispose();
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
async list(cwd) {
|
|
484
|
+
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
|
|
485
|
+
const sessionsById = new Map(sessions.map((session) => [session.id, session]));
|
|
486
|
+
const archivedForCwd = await Promise.all(archivedRecords
|
|
487
|
+
.filter((record) => record.cwd === cwd)
|
|
488
|
+
.map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
|
|
489
|
+
const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
|
|
490
|
+
const unarchivedSessions = sessions.filter((session) => !archivedById.has(session.id)).map(clientSessionFromListEntry);
|
|
491
|
+
this.workspaceActivity?.reconcileSessionActivity(cwd, this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById));
|
|
492
|
+
const archivedSessions = archivedForCwd
|
|
493
|
+
.sort(compareArchivedRecords)
|
|
494
|
+
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
495
|
+
.filter(isDefined);
|
|
496
|
+
return [...unarchivedSessions, ...archivedSessions];
|
|
497
|
+
}
|
|
498
|
+
async start(cwd, options = {}) {
|
|
499
|
+
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, options.initialModel === undefined ? {} : { initialModel: options.initialModel });
|
|
500
|
+
const { session } = active.runtime;
|
|
501
|
+
const created = {
|
|
502
|
+
id: session.sessionId,
|
|
503
|
+
path: session.sessionFile ?? "",
|
|
504
|
+
cwd,
|
|
505
|
+
persisted: sessionFileExists(session.sessionFile),
|
|
506
|
+
created: new Date().toISOString(),
|
|
507
|
+
modified: new Date().toISOString(),
|
|
508
|
+
messageCount: session.messages.length,
|
|
509
|
+
firstMessage: "",
|
|
510
|
+
// Include the parent so listeners can nest the new session in the tree
|
|
511
|
+
// immediately, instead of showing it flat until the next reload.
|
|
512
|
+
...(options.parentSession === undefined ? {} : { parentSessionPath: options.parentSession }),
|
|
513
|
+
};
|
|
514
|
+
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
515
|
+
// session to their list without a manual reload.
|
|
516
|
+
this.events.publishGlobal({ type: "session.created", session: created });
|
|
517
|
+
return created;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Start a new session on behalf of a LLM and deliver an initial prompt to it.
|
|
521
|
+
* The target cwd is constrained to a workspace of the same registered project
|
|
522
|
+
* as the spawning session so the new session is visible in the web UI.
|
|
523
|
+
*/
|
|
524
|
+
async spawnSession(input) {
|
|
525
|
+
if (this.spawnTargets === undefined)
|
|
526
|
+
throw new Error("Spawning sessions is disabled");
|
|
527
|
+
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
528
|
+
if (!decision.allowed)
|
|
529
|
+
throw spawnTargetError(decision);
|
|
530
|
+
const created = await this.start(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
|
|
531
|
+
await this.prompt(created.id, input.prompt);
|
|
532
|
+
this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
|
|
533
|
+
return { sessionId: created.id, cwd: decision.cwd };
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Start a *tracked* child session on behalf of a LLM. Identical to
|
|
537
|
+
* {@link spawnSession} in how the target cwd is resolved, but the child
|
|
538
|
+
* records its parent (so it shows in the session tree) and is registered so
|
|
539
|
+
* the parent is notified when it stops working and can inspect it later.
|
|
540
|
+
*/
|
|
541
|
+
async spawnSubsession(input) {
|
|
542
|
+
if (this.spawnTargets === undefined)
|
|
543
|
+
throw new Error("Spawning sessions is disabled");
|
|
544
|
+
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
545
|
+
if (!decision.allowed)
|
|
546
|
+
throw spawnTargetError(decision);
|
|
547
|
+
const created = await this.start(decision.cwd, {
|
|
548
|
+
...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
|
|
549
|
+
...(input.model === undefined ? {} : { initialModel: input.model }),
|
|
550
|
+
});
|
|
551
|
+
const parentSessionFile = nonEmptyString(input.parentSessionFile);
|
|
552
|
+
const link = {
|
|
553
|
+
parentSessionId: input.parentSessionId,
|
|
554
|
+
childSessionId: created.id,
|
|
555
|
+
...(created.path === "" ? {} : { childSessionFile: created.path }),
|
|
556
|
+
...(parentSessionFile === undefined ? {} : { parentSessionFile }),
|
|
557
|
+
cwd: decision.cwd,
|
|
558
|
+
};
|
|
559
|
+
this.registerVerifiedSubsession(link);
|
|
560
|
+
this.persistSubsessionLink(link);
|
|
561
|
+
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
562
|
+
await this.prompt(created.id, input.prompt);
|
|
563
|
+
this.logger.info({ parentSessionId: input.parentSessionId, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_subsession started a tracked child session");
|
|
564
|
+
return { sessionId: created.id, cwd: decision.cwd };
|
|
565
|
+
}
|
|
566
|
+
/** Summaries of the tracked subsessions spawned by `parentSessionId`. */
|
|
567
|
+
async listSubsessions(parentSessionId, parentSessionFile) {
|
|
568
|
+
const parentFile = nonEmptyString(parentSessionFile);
|
|
569
|
+
await this.hydrateSubsessionsForParent(parentSessionId, parentFile);
|
|
570
|
+
const childIds = this.subsessionChildren.get(parentSessionId);
|
|
571
|
+
if (childIds === undefined)
|
|
572
|
+
return [];
|
|
573
|
+
const authorizedChildIds = [...childIds].filter((childId) => this.subsessionLinkBelongsToParent(parentSessionId, parentFile, childId));
|
|
574
|
+
return Promise.all(authorizedChildIds.map(async (childId) => ({ sessionId: childId, ...(await this.subsessionSummaryFields(childId)) })));
|
|
575
|
+
}
|
|
576
|
+
/** Status and final result of a subsession, scoped to the caller's children. */
|
|
577
|
+
async checkSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
578
|
+
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile);
|
|
579
|
+
const messages = historyMessages(session);
|
|
580
|
+
return {
|
|
581
|
+
sessionId,
|
|
582
|
+
cwd: session.sessionManager.getCwd(),
|
|
583
|
+
status: this.subsessionStatus(session),
|
|
584
|
+
finalText: finalAssistantText(messages),
|
|
585
|
+
messageCount: messages.length,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/** Filtered, paginated transcript of a subsession, scoped to the caller's children. */
|
|
589
|
+
async readSubsession(parentSessionId, sessionId, query, parentSessionFile) {
|
|
590
|
+
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile);
|
|
591
|
+
const view = buildTranscriptView(historyMessages(session), query);
|
|
592
|
+
return {
|
|
593
|
+
sessionId,
|
|
594
|
+
cwd: session.sessionManager.getCwd(),
|
|
595
|
+
status: this.subsessionStatus(session),
|
|
596
|
+
...view,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/** Open a session after verifying it is one of the caller's tracked children. */
|
|
600
|
+
async openSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
601
|
+
const parentFile = nonEmptyString(parentSessionFile);
|
|
602
|
+
await this.hydrateSubsessionsForParent(parentSessionId, parentFile);
|
|
603
|
+
if (this.subsessionParents.get(sessionId) !== parentSessionId || !this.subsessionLinkBelongsToParent(parentSessionId, parentFile, sessionId)) {
|
|
604
|
+
throw new Error(`Session ${sessionId} is not one of your subsessions`);
|
|
605
|
+
}
|
|
606
|
+
return this.getOrOpenTrackedSubsession(sessionId);
|
|
607
|
+
}
|
|
608
|
+
subsessionLinkBelongsToParent(parentSessionId, parentSessionFile, childSessionId) {
|
|
609
|
+
const link = this.subsessionLinks.get(childSessionId);
|
|
610
|
+
if (link?.parentSessionId !== parentSessionId)
|
|
611
|
+
return false;
|
|
612
|
+
return parentSessionFile === undefined || trackedLinkParentFileMatches(link, parentSessionFile);
|
|
613
|
+
}
|
|
614
|
+
activeChildForSubsessionLink(link) {
|
|
615
|
+
const active = this.active.get(link.childSessionId);
|
|
616
|
+
if (active === undefined)
|
|
617
|
+
return undefined;
|
|
618
|
+
return activeSessionFileMatches(active, link.childSessionFile) ? active : undefined;
|
|
619
|
+
}
|
|
620
|
+
activeParentForSubsessionLink(link) {
|
|
621
|
+
const active = this.active.get(link.parentSessionId);
|
|
622
|
+
if (active === undefined)
|
|
623
|
+
return undefined;
|
|
624
|
+
return activeSessionFileMatches(active, link.parentSessionFile) ? active : undefined;
|
|
625
|
+
}
|
|
626
|
+
subsessionLinkForActiveChild(session) {
|
|
627
|
+
const childId = session.sessionId;
|
|
628
|
+
const parentId = this.subsessionParents.get(childId);
|
|
629
|
+
const link = this.subsessionLinks.get(childId);
|
|
630
|
+
if (parentId === undefined || link?.parentSessionId !== parentId)
|
|
631
|
+
return undefined;
|
|
632
|
+
return sessionFileMatches(session, link.childSessionFile) ? link : undefined;
|
|
633
|
+
}
|
|
634
|
+
registerVerifiedSubsession(link) {
|
|
635
|
+
const { childSessionId, parentSessionId } = link;
|
|
636
|
+
const previousParentId = this.subsessionParents.get(childSessionId);
|
|
637
|
+
if (previousParentId !== undefined && previousParentId !== parentSessionId) {
|
|
638
|
+
const previousChildren = this.subsessionChildren.get(previousParentId);
|
|
639
|
+
previousChildren?.delete(childSessionId);
|
|
640
|
+
if (previousChildren?.size === 0)
|
|
641
|
+
this.subsessionChildren.delete(previousParentId);
|
|
642
|
+
}
|
|
643
|
+
this.subsessionParents.set(childSessionId, parentSessionId);
|
|
644
|
+
const children = this.subsessionChildren.get(parentSessionId) ?? new Set();
|
|
645
|
+
children.add(childSessionId);
|
|
646
|
+
this.subsessionChildren.set(parentSessionId, children);
|
|
647
|
+
this.subsessionLinks.set(childSessionId, link);
|
|
648
|
+
if (!this.subsessionNotifyArmed.has(childSessionId))
|
|
649
|
+
this.subsessionNotifyArmed.set(childSessionId, false);
|
|
650
|
+
}
|
|
651
|
+
unregisterSubsession(childSessionId) {
|
|
652
|
+
const parentSessionId = this.subsessionParents.get(childSessionId);
|
|
653
|
+
this.subsessionParents.delete(childSessionId);
|
|
654
|
+
this.subsessionLinks.delete(childSessionId);
|
|
655
|
+
this.subsessionNotifyArmed.delete(childSessionId);
|
|
656
|
+
if (parentSessionId === undefined)
|
|
657
|
+
return;
|
|
658
|
+
const children = this.subsessionChildren.get(parentSessionId);
|
|
659
|
+
children?.delete(childSessionId);
|
|
660
|
+
if (children?.size === 0)
|
|
661
|
+
this.subsessionChildren.delete(parentSessionId);
|
|
662
|
+
}
|
|
663
|
+
persistSubsessionLink(link) {
|
|
664
|
+
const parent = this.activeParentForSubsessionLink(link)?.runtime.session;
|
|
665
|
+
if (parent === undefined)
|
|
666
|
+
return;
|
|
667
|
+
if (parent.sessionManager.appendCustomEntry === undefined)
|
|
668
|
+
return;
|
|
669
|
+
try {
|
|
670
|
+
parent.sessionManager.appendCustomEntry(SUBSESSION_LINK_CUSTOM_TYPE, persistedParentSubsessionLinkData(link));
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
this.logger.info({ parentSessionId: link.parentSessionId, sessionId: link.childSessionId, error: error instanceof Error ? error.message : String(error) }, "failed to persist subsession link");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
persistSubsessionChildMarker(parentSessionId, childSessionId) {
|
|
677
|
+
const child = this.active.get(childSessionId)?.runtime.session;
|
|
678
|
+
if (child === undefined)
|
|
679
|
+
return;
|
|
680
|
+
if (child.sessionManager.appendCustomEntry === undefined)
|
|
681
|
+
return;
|
|
682
|
+
try {
|
|
683
|
+
child.sessionManager.appendCustomEntry(SUBSESSION_CHILD_LINK_CUSTOM_TYPE, persistedChildSubsessionLinkData(parentSessionId, childSessionId));
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
this.logger.info({ parentSessionId, sessionId: childSessionId, error: error instanceof Error ? error.message : String(error) }, "failed to persist subsession child marker");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async hydrateSubsessionsForParent(parentSessionId, parentSessionFile) {
|
|
690
|
+
const hydrationKey = subsessionHydratedParentKey(parentSessionId, parentSessionFile);
|
|
691
|
+
if (this.subsessionHydratedParents.has(hydrationKey))
|
|
692
|
+
return;
|
|
693
|
+
const activeParent = this.active.get(parentSessionId);
|
|
694
|
+
if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
|
|
695
|
+
const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
|
|
696
|
+
await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
697
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (parentSessionFile === undefined)
|
|
701
|
+
return;
|
|
702
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId) {
|
|
703
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
let parentManager;
|
|
707
|
+
try {
|
|
708
|
+
parentManager = await this.sessionManager.open(parentSessionFile);
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
|
|
715
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
716
|
+
}
|
|
717
|
+
async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile) {
|
|
718
|
+
// Parent custom links are the authoritative recovery record: verify the
|
|
719
|
+
// exact live child file/header before tracking.
|
|
720
|
+
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
721
|
+
for (const entry of entries) {
|
|
722
|
+
const link = parsePersistedParentSubsessionLink(entry);
|
|
723
|
+
if (link === undefined)
|
|
724
|
+
continue;
|
|
725
|
+
const verified = await this.verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link);
|
|
726
|
+
if (verified === undefined)
|
|
727
|
+
continue;
|
|
728
|
+
this.registerVerifiedSubsession(verified);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link) {
|
|
732
|
+
if (parentSessionFile === undefined)
|
|
733
|
+
return undefined;
|
|
734
|
+
if (link.spawnedBySessionId !== parentSessionId)
|
|
735
|
+
return undefined;
|
|
736
|
+
if (!(await this.parentLinkHasValidChildTarget(parentSessionFile, link)))
|
|
737
|
+
return undefined;
|
|
738
|
+
return trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile);
|
|
739
|
+
}
|
|
740
|
+
async parentLinkHasValidChildTarget(parentSessionFile, link) {
|
|
741
|
+
return link.spawnedSessionFile !== undefined
|
|
742
|
+
&& await sessionFileHeaderMatches(link.spawnedSessionFile, { sessionId: link.spawnedSessionId, parentSessionFile });
|
|
743
|
+
}
|
|
744
|
+
async recoverSubsessionTrackingForOpenedSession(session) {
|
|
745
|
+
const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
|
|
746
|
+
if (link === undefined)
|
|
747
|
+
return;
|
|
748
|
+
this.registerVerifiedSubsession(link);
|
|
749
|
+
}
|
|
750
|
+
async verifiedSubsessionLinkFromOpenedChild(session) {
|
|
751
|
+
// Child markers are only hints; the current child header and reciprocal
|
|
752
|
+
// parent custom link must agree on the exact ids and files before relinking.
|
|
753
|
+
const entries = session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch();
|
|
754
|
+
let marker;
|
|
755
|
+
for (const entry of entries) {
|
|
756
|
+
const parsed = parsePersistedChildSubsessionLink(entry);
|
|
757
|
+
if (parsed?.spawnedSessionId === session.sessionId)
|
|
758
|
+
marker = parsed;
|
|
759
|
+
}
|
|
760
|
+
if (marker === undefined)
|
|
761
|
+
return undefined;
|
|
762
|
+
const childSessionFile = nonEmptyString(session.sessionFile);
|
|
763
|
+
if (childSessionFile === undefined)
|
|
764
|
+
return undefined;
|
|
765
|
+
const childHeader = await readSessionHeaderSummary(childSessionFile);
|
|
766
|
+
if (childHeader?.id !== session.sessionId)
|
|
767
|
+
return undefined;
|
|
768
|
+
const parentSessionFile = nonEmptyString(childHeader.parentSession);
|
|
769
|
+
if (parentSessionFile === undefined)
|
|
770
|
+
return undefined;
|
|
771
|
+
const parentHeader = await readSessionHeaderSummary(parentSessionFile);
|
|
772
|
+
if (parentHeader?.id !== marker.spawnedBySessionId)
|
|
773
|
+
return undefined;
|
|
774
|
+
const parentLink = await this.findReciprocalParentSubsessionLink(parentSessionFile, marker.spawnedBySessionId, session.sessionId, childSessionFile);
|
|
775
|
+
if (parentLink === undefined)
|
|
776
|
+
return undefined;
|
|
777
|
+
return {
|
|
778
|
+
parentSessionId: marker.spawnedBySessionId,
|
|
779
|
+
childSessionId: session.sessionId,
|
|
780
|
+
childSessionFile,
|
|
781
|
+
parentSessionFile,
|
|
782
|
+
cwd: parentLink.cwd ?? session.sessionManager.getCwd(),
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
async findReciprocalParentSubsessionLink(parentSessionFile, parentSessionId, childSessionId, childSessionFile) {
|
|
786
|
+
let parentManager;
|
|
787
|
+
try {
|
|
788
|
+
parentManager = await this.sessionManager.open(parentSessionFile);
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
794
|
+
for (const entry of entries) {
|
|
795
|
+
const link = parsePersistedParentSubsessionLink(entry);
|
|
796
|
+
if (link === undefined)
|
|
797
|
+
continue;
|
|
798
|
+
if (link.spawnedBySessionId !== parentSessionId || link.spawnedSessionId !== childSessionId)
|
|
799
|
+
continue;
|
|
800
|
+
if (link.spawnedSessionFile === undefined || !sessionPathsEqual(link.spawnedSessionFile, childSessionFile))
|
|
801
|
+
continue;
|
|
802
|
+
return link;
|
|
803
|
+
}
|
|
804
|
+
return undefined;
|
|
805
|
+
}
|
|
806
|
+
async getOrOpenTrackedSubsession(sessionId) {
|
|
807
|
+
const link = this.subsessionLinks.get(sessionId);
|
|
808
|
+
if (link === undefined)
|
|
809
|
+
throw new Error("Session not found");
|
|
810
|
+
const active = this.activeChildForSubsessionLink(link);
|
|
811
|
+
if (active !== undefined)
|
|
812
|
+
return active.runtime.session;
|
|
813
|
+
if (link.childSessionFile !== undefined) {
|
|
814
|
+
if (!(await sessionFileHeaderMatches(link.childSessionFile, { sessionId, parentSessionFile: link.parentSessionFile })))
|
|
815
|
+
throw new Error("Session not found");
|
|
816
|
+
const sessionManager = await this.sessionManager.open(link.childSessionFile);
|
|
817
|
+
return (await this.create(sessionManager, link.cwd ?? sessionManager.getCwd())).runtime.session;
|
|
818
|
+
}
|
|
819
|
+
throw new Error("Session not found");
|
|
820
|
+
}
|
|
821
|
+
async subsessionSummaryFields(childSessionId) {
|
|
822
|
+
const link = this.subsessionLinks.get(childSessionId);
|
|
823
|
+
const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
|
|
824
|
+
if (active !== undefined) {
|
|
825
|
+
return { cwd: active.runtime.cwd, status: this.subsessionStatus(active.runtime.session) };
|
|
826
|
+
}
|
|
827
|
+
if (link?.childSessionFile !== undefined && (await sessionFileHeaderMatches(link.childSessionFile, { sessionId: childSessionId, parentSessionFile: link.parentSessionFile }))) {
|
|
828
|
+
return { cwd: link.cwd ?? "", status: "idle" };
|
|
829
|
+
}
|
|
830
|
+
if (link?.cwd !== undefined)
|
|
831
|
+
return { cwd: link.cwd, status: "unknown" };
|
|
832
|
+
return { cwd: "", status: "unknown" };
|
|
833
|
+
}
|
|
834
|
+
subsessionStatus(session) {
|
|
835
|
+
if (this.hasActiveWork(session))
|
|
836
|
+
return "working";
|
|
837
|
+
if (this.activities.get(session.sessionId)?.phase === "error")
|
|
838
|
+
return "error";
|
|
839
|
+
return "idle";
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Drive parent notifications from a tracked child's status. Arms a pending
|
|
843
|
+
* notification while the child is working, and when it stops fires a single
|
|
844
|
+
* follow-up message to the parent via {@link prompt} (which queues if the
|
|
845
|
+
* parent is busy and delivers immediately when it is idle).
|
|
846
|
+
*/
|
|
847
|
+
updateSubsessionTracking(session) {
|
|
848
|
+
const link = this.subsessionLinkForActiveChild(session);
|
|
849
|
+
if (link === undefined)
|
|
850
|
+
return;
|
|
851
|
+
const childId = link.childSessionId;
|
|
852
|
+
if (this.hasActiveWork(session)) {
|
|
853
|
+
this.subsessionNotifyArmed.set(childId, true);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (this.subsessionNotifyArmed.get(childId) !== true)
|
|
857
|
+
return;
|
|
858
|
+
this.subsessionNotifyArmed.set(childId, false);
|
|
859
|
+
const status = this.activities.get(childId)?.phase === "error" ? "error" : "idle";
|
|
860
|
+
const finalText = finalAssistantText(historyMessages(session));
|
|
861
|
+
const preview = finalText === "" ? "(no output)" : truncateForNotification(finalText);
|
|
862
|
+
const text = `Subsession ${childId} stopped working (status: ${status}). Latest output:\n\n${preview}\n\nUse check_subsession with sessionId "${childId}" for its status and latest output, or read_subsession to look through its full transcript.`;
|
|
863
|
+
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
|
864
|
+
}
|
|
865
|
+
async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
|
|
866
|
+
const link = this.subsessionLinks.get(childSessionId);
|
|
867
|
+
if (link?.parentSessionId !== parentSessionId)
|
|
868
|
+
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
869
|
+
const active = this.activeParentForSubsessionLink(link);
|
|
870
|
+
if (active !== undefined)
|
|
871
|
+
return active.runtime.session;
|
|
872
|
+
const parentSessionFile = link.parentSessionFile;
|
|
873
|
+
if (parentSessionFile === undefined)
|
|
874
|
+
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
875
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId) {
|
|
876
|
+
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
877
|
+
}
|
|
878
|
+
const sessionManager = await this.sessionManager.open(parentSessionFile);
|
|
879
|
+
return (await this.create(sessionManager, sessionManager.getCwd())).runtime.session;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Deliver a subsession-completion notice to the parent as a system-authored
|
|
883
|
+
* custom message rather than a user message, so it is not attributed to the
|
|
884
|
+
* human in the transcript. It still wakes an idle parent (`triggerTurn`) and
|
|
885
|
+
* queues behind in-flight work (`deliverAs: "followUp"`), preserving the
|
|
886
|
+
* established "queue if busy, send and act if idle" behavior.
|
|
887
|
+
*/
|
|
888
|
+
async notifyParentOfSubsession(parentId, childId, text) {
|
|
889
|
+
try {
|
|
890
|
+
const session = await this.getOrOpenParentForSubsession(parentId, childId);
|
|
891
|
+
await session.sendCustomMessage({ customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, content: text, display: true, details: { sessionId: childId } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
892
|
+
this.publishStatus(session);
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
this.logger.info({ parentSessionId: parentId, sessionId: childId, error: error instanceof Error ? error.message : String(error) }, "failed to notify parent of subsession completion");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async messages(ref, page) {
|
|
899
|
+
const session = await this.getOrOpen(ref);
|
|
900
|
+
return pageMessagesAtSafeBoundary(historyMessages(session), page);
|
|
901
|
+
}
|
|
902
|
+
async status(ref) {
|
|
903
|
+
return this.statusFromSession(await this.getOrOpen(ref));
|
|
904
|
+
}
|
|
905
|
+
async availableModels(ref) {
|
|
906
|
+
const session = await this.getOrOpen(ref);
|
|
907
|
+
await session.modelRegistry.refresh();
|
|
908
|
+
const models = session.scopedModels.length > 0
|
|
909
|
+
? session.scopedModels.map((scoped) => scoped.model)
|
|
910
|
+
: session.modelRegistry.getAvailable();
|
|
911
|
+
return models.map(modelToClientModel);
|
|
912
|
+
}
|
|
913
|
+
async setModel(ref, provider, modelId) {
|
|
914
|
+
await this.assertWritable(ref);
|
|
915
|
+
const session = await this.getOrOpen(ref);
|
|
916
|
+
await session.modelRegistry.refresh();
|
|
917
|
+
const candidates = session.scopedModels.length > 0
|
|
918
|
+
? session.scopedModels.map((scoped) => scoped.model)
|
|
919
|
+
: session.modelRegistry.getAvailable();
|
|
920
|
+
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
|
|
921
|
+
?? session.modelRegistry.find(provider, modelId);
|
|
922
|
+
if (model === undefined)
|
|
923
|
+
throw new Error(`Model not found: ${provider}/${modelId}`);
|
|
924
|
+
await session.setModel(model);
|
|
925
|
+
this.publishActivity(session, `model: ${model.id}`, "idle", model.provider);
|
|
926
|
+
this.publishStatus(session);
|
|
927
|
+
return this.statusFromSession(session);
|
|
928
|
+
}
|
|
929
|
+
async cycleModel(ref, direction) {
|
|
930
|
+
await this.assertWritable(ref);
|
|
931
|
+
const session = await this.getOrOpen(ref);
|
|
932
|
+
const result = await session.cycleModel(direction);
|
|
933
|
+
if (result === undefined)
|
|
934
|
+
throw new Error(session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available");
|
|
935
|
+
this.publishActivity(session, `model: ${result.model.id}`, "idle", result.model.provider);
|
|
936
|
+
this.publishStatus(session);
|
|
937
|
+
return this.statusFromSession(session);
|
|
938
|
+
}
|
|
939
|
+
async availableThinkingLevels(ref) {
|
|
940
|
+
const session = await this.getOrOpen(ref);
|
|
941
|
+
return session.getAvailableThinkingLevels();
|
|
942
|
+
}
|
|
943
|
+
async setThinkingLevel(ref, level) {
|
|
944
|
+
await this.assertWritable(ref);
|
|
945
|
+
const session = await this.getOrOpen(ref);
|
|
946
|
+
// pi owns the valid set; validate against the session's live levels rather
|
|
947
|
+
// than a hardcoded union so this stays correct if pi changes the set.
|
|
948
|
+
const available = session.getAvailableThinkingLevels();
|
|
949
|
+
const match = available.find((candidate) => candidate === level);
|
|
950
|
+
if (match === undefined)
|
|
951
|
+
throw new Error(`Invalid thinking level: ${level}`);
|
|
952
|
+
session.setThinkingLevel(match);
|
|
953
|
+
this.publishActivity(session, `thinking: ${session.thinkingLevel}`, "idle");
|
|
954
|
+
this.publishStatus(session);
|
|
955
|
+
return this.statusFromSession(session);
|
|
956
|
+
}
|
|
957
|
+
async cycleThinkingLevel(ref) {
|
|
958
|
+
await this.assertWritable(ref);
|
|
959
|
+
const session = await this.getOrOpen(ref);
|
|
960
|
+
const level = session.cycleThinkingLevel();
|
|
961
|
+
if (level === undefined)
|
|
962
|
+
throw new Error("Current model does not support thinking");
|
|
963
|
+
this.publishActivity(session, `thinking: ${level}`, "idle");
|
|
964
|
+
this.publishStatus(session);
|
|
965
|
+
return this.statusFromSession(session);
|
|
966
|
+
}
|
|
967
|
+
async commands(ref) {
|
|
968
|
+
const session = await this.getOrOpen(ref);
|
|
969
|
+
const commands = [...BUILTIN_COMMANDS];
|
|
970
|
+
for (const command of session.extensionRunner.getRegisteredCommands()) {
|
|
971
|
+
commands.push({ name: command.name, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
|
|
972
|
+
}
|
|
973
|
+
for (const template of session.promptTemplates) {
|
|
974
|
+
commands.push({ name: template.name, ...(template.description === undefined ? {} : { description: template.description }), source: "prompt" });
|
|
975
|
+
}
|
|
976
|
+
for (const skill of session.resourceLoader.getSkills().skills) {
|
|
977
|
+
commands.push({ name: `skill:${skill.name}`, ...(skill.description === undefined ? {} : { description: skill.description }), source: "skill" });
|
|
978
|
+
}
|
|
979
|
+
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
980
|
+
}
|
|
981
|
+
async prompt(ref, text, streamingBehavior, attachments, options) {
|
|
982
|
+
const promptText = requirePromptText(text);
|
|
983
|
+
// Command-forwarded prompts (e.g. /skill:*) are expanded by the agent, which
|
|
984
|
+
// streams the canonical message back. The client doesn't render the raw
|
|
985
|
+
// command text, so the server must not echo it either, or it would show up
|
|
986
|
+
// as a transient line that vanishes on reload.
|
|
987
|
+
const echoUserMessage = options?.echoUserMessage !== false;
|
|
988
|
+
const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
|
|
989
|
+
const parsedAttachments = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false });
|
|
990
|
+
const images = attachmentsToInlineImages(parsedAttachments).map((entry) => entry.image);
|
|
991
|
+
await this.assertWritable(ref);
|
|
992
|
+
const session = await this.getOrOpen(ref);
|
|
993
|
+
this.maybeGenerateSessionName(session, promptText);
|
|
994
|
+
const isQueued = session.isStreaming || session.isCompacting;
|
|
995
|
+
const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
|
|
996
|
+
if (isQueued && images.length === 0 && this.hasQueuedMessageText(session, promptText)) {
|
|
997
|
+
this.publishActivity(session, "duplicate queued message ignored", "active");
|
|
998
|
+
this.publishStatus(session);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (session.isCompacting) {
|
|
1002
|
+
this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp", images, echoUserMessage);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
|
|
1006
|
+
}
|
|
1007
|
+
submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1008
|
+
this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
|
|
1009
|
+
if (behavior === undefined && echoUserMessage)
|
|
1010
|
+
this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) });
|
|
1011
|
+
const promptOptions = buildPromptOptions(behavior, images);
|
|
1012
|
+
const promptPromise = session.prompt(text, promptOptions).catch((error) => {
|
|
1013
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1014
|
+
this.publishActivity(session, "error", "error", message);
|
|
1015
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1016
|
+
});
|
|
1017
|
+
void promptPromise;
|
|
1018
|
+
return promptPromise;
|
|
1019
|
+
}
|
|
1020
|
+
enqueuePromptDuringCompaction(session, text, kind, images = [], echoUserMessage = true) {
|
|
1021
|
+
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
1022
|
+
queue.push({ kind, text, ...(images.length > 0 ? { images } : {}), ...(echoUserMessage ? {} : { echoUserMessage: false }) });
|
|
1023
|
+
this.compactionPromptQueues.set(session.sessionId, queue);
|
|
1024
|
+
this.publishActivity(session, "message queued during compaction", "active");
|
|
1025
|
+
this.publishStatus(session);
|
|
1026
|
+
}
|
|
1027
|
+
async saveAttachments(ref, attachments, folder) {
|
|
1028
|
+
const parsed = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false, allowFileAttachments: true });
|
|
1029
|
+
if (parsed.length === 0)
|
|
1030
|
+
return [];
|
|
1031
|
+
await this.assertWritable(ref);
|
|
1032
|
+
const active = await this.getActive(ref);
|
|
1033
|
+
return saveAttachmentsToWorkspace(active.runtime.cwd, parsed, folder === undefined ? {} : { folder });
|
|
1034
|
+
}
|
|
1035
|
+
async shell(ref, text) {
|
|
1036
|
+
await this.assertWritable(ref);
|
|
1037
|
+
const active = await this.getActive(ref);
|
|
1038
|
+
const { session } = active.runtime;
|
|
1039
|
+
const isExcluded = text.startsWith("!!");
|
|
1040
|
+
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
|
1041
|
+
if (!command)
|
|
1042
|
+
throw new Error("Usage: !<shell command>");
|
|
1043
|
+
if (session.isBashRunning)
|
|
1044
|
+
throw new Error("A bash command is already running");
|
|
1045
|
+
this.publishActivity(session, "running bash", "active", command);
|
|
1046
|
+
this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded });
|
|
1047
|
+
void session.executeBash(command, (chunk) => {
|
|
1048
|
+
this.events.publish(session.sessionId, { type: "shell.chunk", chunk });
|
|
1049
|
+
this.publishActivity(session, "running bash", "active", command);
|
|
1050
|
+
this.publishStatus(session);
|
|
1051
|
+
}, { excludeFromContext: isExcluded }).then((result) => {
|
|
1052
|
+
this.events.publish(session.sessionId, {
|
|
1053
|
+
type: "shell.end",
|
|
1054
|
+
output: result.output,
|
|
1055
|
+
...(result.exitCode === undefined ? {} : { exitCode: result.exitCode }),
|
|
1056
|
+
cancelled: result.cancelled,
|
|
1057
|
+
truncated: result.truncated,
|
|
1058
|
+
});
|
|
1059
|
+
this.publishActivity(session, "bash complete", result.exitCode === 0 ? "idle" : "error", command);
|
|
1060
|
+
this.publishStatus(session);
|
|
1061
|
+
}).catch((error) => {
|
|
1062
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1063
|
+
this.events.publish(session.sessionId, { type: "shell.end", output: message, isError: true });
|
|
1064
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1065
|
+
this.publishActivity(session, "bash failed", "error", message);
|
|
1066
|
+
this.publishStatus(session);
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
async runCommand(ref, text) {
|
|
1070
|
+
await this.assertWritable(ref);
|
|
1071
|
+
const active = await this.getActive(ref);
|
|
1072
|
+
return this.commandService.run(active.runtime.session.sessionId, text);
|
|
1073
|
+
}
|
|
1074
|
+
async respondToCommand(ref, requestId, value) {
|
|
1075
|
+
await this.assertWritable(ref);
|
|
1076
|
+
const active = await this.getActive(ref);
|
|
1077
|
+
return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
|
|
1078
|
+
}
|
|
1079
|
+
async reloadSessionRuntime(session) {
|
|
1080
|
+
if (this.hasActiveWork(session))
|
|
1081
|
+
throw new Error("Stop current session activity before reloading");
|
|
1082
|
+
this.publishActivity(session, "reloading resources", "active");
|
|
1083
|
+
try {
|
|
1084
|
+
await session.reload();
|
|
1085
|
+
this.publishActivity(session, "resources reloaded", "idle");
|
|
1086
|
+
this.publishStatus(session);
|
|
1087
|
+
}
|
|
1088
|
+
catch (error) {
|
|
1089
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1090
|
+
this.publishActivity(session, "reload failed", "error", message);
|
|
1091
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1092
|
+
this.publishStatus(session);
|
|
1093
|
+
throw error;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
async archive(ref) {
|
|
1097
|
+
const session = await this.getOrOpen(ref);
|
|
1098
|
+
await this.gracefulStopAndClose(session);
|
|
1099
|
+
const archiveInput = await this.archiveInputForSession(session);
|
|
1100
|
+
await this.archiveStore.archive(archiveInput);
|
|
1101
|
+
}
|
|
1102
|
+
async archiveMany(refs) {
|
|
1103
|
+
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
1104
|
+
const [archivedRecords, sessionContext] = await Promise.all([
|
|
1105
|
+
this.archiveStore.list(),
|
|
1106
|
+
this.bulkSessionLookupContext(uniqueRefs),
|
|
1107
|
+
]);
|
|
1108
|
+
const failures = [];
|
|
1109
|
+
const alreadyArchivedSessionIds = [];
|
|
1110
|
+
const planItems = [];
|
|
1111
|
+
for (const ref of uniqueRefs) {
|
|
1112
|
+
const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
1113
|
+
if (archived !== undefined) {
|
|
1114
|
+
alreadyArchivedSessionIds.push(archived.sessionId);
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
const active = this.activeForLookup(bulkRefToLookup(ref));
|
|
1118
|
+
const listed = findListedSessionForBulkRef(sessionContext, ref);
|
|
1119
|
+
const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
|
|
1120
|
+
try {
|
|
1121
|
+
if (listed !== undefined) {
|
|
1122
|
+
planItems.push({ input: archiveInputFromListEntry(listed) });
|
|
1123
|
+
}
|
|
1124
|
+
else if (active !== undefined) {
|
|
1125
|
+
planItems.push({ input: archiveInputFromActiveSession(active.runtime.session), activeSession: active.runtime.session });
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
failures.push({ sessionId: ref.id, error: "Session not found" });
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
failures.push({ sessionId: resolvedSessionId, error: errorMessage(error) });
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const readyInputs = [];
|
|
1136
|
+
for (const item of planItems) {
|
|
1137
|
+
try {
|
|
1138
|
+
if (item.activeSession) {
|
|
1139
|
+
await this.gracefulStopAndClose(item.activeSession);
|
|
1140
|
+
}
|
|
1141
|
+
readyInputs.push(item.input);
|
|
1142
|
+
}
|
|
1143
|
+
catch (error) {
|
|
1144
|
+
failures.push({ sessionId: item.input.sessionId, error: errorMessage(error) });
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const archivedSessionIds = [...alreadyArchivedSessionIds];
|
|
1148
|
+
try {
|
|
1149
|
+
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
1150
|
+
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
1151
|
+
}
|
|
1152
|
+
catch (error) {
|
|
1153
|
+
for (const input of readyInputs)
|
|
1154
|
+
failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
|
|
1155
|
+
}
|
|
1156
|
+
return {
|
|
1157
|
+
archived: true,
|
|
1158
|
+
archivedSessionIds,
|
|
1159
|
+
failures,
|
|
1160
|
+
generatedAt: new Date().toISOString(),
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
async archiveTree(ref) {
|
|
1164
|
+
const session = await this.getOrOpen(ref);
|
|
1165
|
+
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
1166
|
+
const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false);
|
|
1167
|
+
const plan = planSessionArchiveTree(root, catalog);
|
|
1168
|
+
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
1169
|
+
for (const target of plan.unarchivedTargets) {
|
|
1170
|
+
if (target.activeSession) {
|
|
1171
|
+
await this.gracefulStopAndClose(target.activeSession);
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
await this.closeActive(target.id);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
await this.archiveStoreArchiveMany(archiveInputs);
|
|
1178
|
+
return {
|
|
1179
|
+
archived: true,
|
|
1180
|
+
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
1181
|
+
archivedCount: archiveInputs.length,
|
|
1182
|
+
skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
async restore(ref) {
|
|
1186
|
+
const archived = await this.getArchived(ref);
|
|
1187
|
+
if (archived === undefined)
|
|
1188
|
+
throw new Error("Session not found");
|
|
1189
|
+
await this.closeActive(archived.sessionId);
|
|
1190
|
+
await this.archiveStore.restore(archived.sessionId);
|
|
1191
|
+
}
|
|
1192
|
+
async deleteArchived(ref) {
|
|
1193
|
+
const record = await this.getArchived(ref);
|
|
1194
|
+
if (record === undefined)
|
|
1195
|
+
throw new Error("Archived session not found");
|
|
1196
|
+
if (this.archiveStore.deleteArchived === undefined)
|
|
1197
|
+
throw new Error("Archive store does not support deletion");
|
|
1198
|
+
await this.closeActive(record.sessionId);
|
|
1199
|
+
if (record.archivePath === undefined)
|
|
1200
|
+
await this.ensureArchivedRecordMoved(record);
|
|
1201
|
+
await this.archiveStore.deleteArchived(record.sessionId);
|
|
1202
|
+
}
|
|
1203
|
+
async deleteArchivedMany(refs) {
|
|
1204
|
+
if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
1205
|
+
throw new Error("Archive store does not support deletion");
|
|
1206
|
+
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
1207
|
+
const archivedRecords = await this.archiveStore.list();
|
|
1208
|
+
const failures = [];
|
|
1209
|
+
const planItems = [];
|
|
1210
|
+
for (const ref of uniqueRefs) {
|
|
1211
|
+
const record = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
1212
|
+
if (record === undefined) {
|
|
1213
|
+
failures.push({ sessionId: ref.id, error: "Archived session not found" });
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
const active = this.activeForLookup({ id: record.sessionId, cwd: record.cwd });
|
|
1217
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1218
|
+
failures.push({ sessionId: record.sessionId, error: "Stop current session activity before deleting archived session" });
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
planItems.push({ record });
|
|
1222
|
+
}
|
|
1223
|
+
const readyRecords = [];
|
|
1224
|
+
for (const item of planItems) {
|
|
1225
|
+
try {
|
|
1226
|
+
await this.closeActive(item.record.sessionId);
|
|
1227
|
+
readyRecords.push(item.record);
|
|
1228
|
+
}
|
|
1229
|
+
catch (error) {
|
|
1230
|
+
failures.push({ sessionId: item.record.sessionId, error: errorMessage(error) });
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const moveFailures = await this.moveLegacyArchivedRecordsForDelete(readyRecords);
|
|
1234
|
+
failures.push(...moveFailures);
|
|
1235
|
+
const moveFailureIds = new Set(moveFailures.map((failure) => failure.sessionId));
|
|
1236
|
+
const deleteIds = readyRecords
|
|
1237
|
+
.map((record) => record.sessionId)
|
|
1238
|
+
.filter((sessionId) => !moveFailureIds.has(sessionId));
|
|
1239
|
+
let deletedSessionIds = [];
|
|
1240
|
+
try {
|
|
1241
|
+
deletedSessionIds = await this.archiveStoreDeleteArchivedMany(deleteIds);
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
for (const sessionId of deleteIds)
|
|
1245
|
+
failures.push({ sessionId, error: errorMessage(error) });
|
|
1246
|
+
}
|
|
1247
|
+
return {
|
|
1248
|
+
deleted: true,
|
|
1249
|
+
deletedSessionIds,
|
|
1250
|
+
failures,
|
|
1251
|
+
generatedAt: new Date().toISOString(),
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
async reload(ref) {
|
|
1255
|
+
await this.assertWritable(ref);
|
|
1256
|
+
const session = await this.getOrOpen(ref);
|
|
1257
|
+
if (this.hasActiveWork(session))
|
|
1258
|
+
throw new Error("Stop current session activity before reloading");
|
|
1259
|
+
await this.closeActive(session.sessionId);
|
|
1260
|
+
const reopened = await this.getActive(ref);
|
|
1261
|
+
this.publishStatus(reopened.runtime.session);
|
|
1262
|
+
}
|
|
1263
|
+
async detachParent(ref) {
|
|
1264
|
+
const session = await this.getOrOpen(ref);
|
|
1265
|
+
const sessionFile = session.sessionFile;
|
|
1266
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
1267
|
+
throw new Error("Session is not persisted");
|
|
1268
|
+
await clearParentSession(sessionFile);
|
|
1269
|
+
clearParentSessionHeader(session.sessionManager);
|
|
1270
|
+
this.unregisterSubsession(session.sessionId);
|
|
1271
|
+
}
|
|
1272
|
+
async abort(ref) {
|
|
1273
|
+
const active = this.activeForLookup(ref);
|
|
1274
|
+
if (active === undefined)
|
|
1275
|
+
return;
|
|
1276
|
+
const sessionId = active.runtime.session.sessionId;
|
|
1277
|
+
this.clearCompactionPromptQueue(sessionId);
|
|
1278
|
+
clearSessionQueue(active.runtime.session);
|
|
1279
|
+
await active.runtime.session.abort();
|
|
1280
|
+
this.publishActivity(active.runtime.session, "stopped", "idle");
|
|
1281
|
+
this.publishStatus(active.runtime.session);
|
|
1282
|
+
}
|
|
1283
|
+
stop(ref) {
|
|
1284
|
+
const active = this.activeForLookup(ref);
|
|
1285
|
+
if (active === undefined)
|
|
1286
|
+
return;
|
|
1287
|
+
void this.closeActive(active.runtime.session.sessionId).catch(() => {
|
|
1288
|
+
// Best-effort shutdown; callers that need errors await closeActive directly.
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
async bulkSessionLookupContext(refs) {
|
|
1292
|
+
const cwdSet = new Set();
|
|
1293
|
+
let needsAllSessions = false;
|
|
1294
|
+
for (const ref of refs) {
|
|
1295
|
+
if (ref.cwd === undefined)
|
|
1296
|
+
needsAllSessions = true;
|
|
1297
|
+
else
|
|
1298
|
+
cwdSet.add(ref.cwd);
|
|
1299
|
+
}
|
|
1300
|
+
const [sessionsByCwd, allSessions] = await Promise.all([
|
|
1301
|
+
this.listSessionsByCwd([...cwdSet]),
|
|
1302
|
+
needsAllSessions ? this.sessionManager.listAll?.() ?? Promise.resolve([]) : Promise.resolve(undefined),
|
|
1303
|
+
]);
|
|
1304
|
+
return allSessions === undefined ? { sessionsByCwd } : { sessionsByCwd, allSessions };
|
|
1305
|
+
}
|
|
1306
|
+
async listSessionsByCwd(cwds) {
|
|
1307
|
+
const uniqueCwds = uniqueStrings(cwds);
|
|
1308
|
+
const entries = await Promise.all(uniqueCwds.map(async (cwd) => [cwd, await this.sessionManager.list(cwd)]));
|
|
1309
|
+
return new Map(entries);
|
|
1310
|
+
}
|
|
1311
|
+
async archiveStoreArchiveMany(inputs) {
|
|
1312
|
+
if (inputs.length === 0)
|
|
1313
|
+
return [];
|
|
1314
|
+
if (this.archiveStore.archiveMany !== undefined)
|
|
1315
|
+
return this.archiveStore.archiveMany(inputs);
|
|
1316
|
+
const records = [];
|
|
1317
|
+
for (const input of inputs)
|
|
1318
|
+
records.push(await this.archiveStore.archive(input));
|
|
1319
|
+
return records;
|
|
1320
|
+
}
|
|
1321
|
+
async archiveStoreDeleteArchivedMany(sessionIds) {
|
|
1322
|
+
if (sessionIds.length === 0)
|
|
1323
|
+
return [];
|
|
1324
|
+
if (this.archiveStore.deleteArchivedMany !== undefined)
|
|
1325
|
+
return this.archiveStore.deleteArchivedMany(sessionIds);
|
|
1326
|
+
if (this.archiveStore.deleteArchived === undefined)
|
|
1327
|
+
throw new Error("Archive store does not support deletion");
|
|
1328
|
+
for (const sessionId of sessionIds)
|
|
1329
|
+
await this.archiveStore.deleteArchived(sessionId);
|
|
1330
|
+
return [...sessionIds];
|
|
1331
|
+
}
|
|
1332
|
+
async moveLegacyArchivedRecordsForDelete(records) {
|
|
1333
|
+
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
1334
|
+
if (legacyRecords.length === 0)
|
|
1335
|
+
return [];
|
|
1336
|
+
let sessionsByCwd;
|
|
1337
|
+
try {
|
|
1338
|
+
sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1339
|
+
}
|
|
1340
|
+
catch (error) {
|
|
1341
|
+
return legacyRecords.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
1342
|
+
}
|
|
1343
|
+
const moveInputs = legacyRecords
|
|
1344
|
+
.map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
|
|
1345
|
+
.filter(isDefined)
|
|
1346
|
+
.map(archiveInputFromListEntry);
|
|
1347
|
+
if (moveInputs.length === 0)
|
|
1348
|
+
return [];
|
|
1349
|
+
try {
|
|
1350
|
+
await this.archiveStoreArchiveMany(moveInputs);
|
|
1351
|
+
return [];
|
|
1352
|
+
}
|
|
1353
|
+
catch (error) {
|
|
1354
|
+
const failedIds = new Set(moveInputs.map((input) => input.sessionId));
|
|
1355
|
+
return legacyRecords
|
|
1356
|
+
.filter((record) => failedIds.has(record.sessionId))
|
|
1357
|
+
.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
async cleanupPlan(request) {
|
|
1361
|
+
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll?.() ?? [], this.archiveStore.list()]);
|
|
1362
|
+
return planSessionCleanup({
|
|
1363
|
+
sessions,
|
|
1364
|
+
archivedRecords,
|
|
1365
|
+
activeSessions: this.cleanupActiveSessionStatuses(),
|
|
1366
|
+
thresholds: request.thresholds,
|
|
1367
|
+
...(request.projectCwds === undefined ? {} : { projectCwds: request.projectCwds }),
|
|
1368
|
+
now: this.now(),
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
cleanupActiveSessionStatuses() {
|
|
1372
|
+
return [...new Set(this.active.values())].map((active) => ({
|
|
1373
|
+
sessionId: active.runtime.session.sessionId,
|
|
1374
|
+
hasActiveWork: this.hasActiveWork(active.runtime.session),
|
|
1375
|
+
}));
|
|
1376
|
+
}
|
|
1377
|
+
activeSessionHasWork(sessionId) {
|
|
1378
|
+
const active = this.active.get(sessionId);
|
|
1379
|
+
return active !== undefined && this.hasActiveWork(active.runtime.session);
|
|
1380
|
+
}
|
|
1381
|
+
reconcilableSessionIds(cwd, listedSessionIds, archivedById) {
|
|
1382
|
+
const sessionIds = new Set(listedSessionIds);
|
|
1383
|
+
for (const active of new Set(this.active.values())) {
|
|
1384
|
+
const session = active.runtime.session;
|
|
1385
|
+
if (session.sessionManager.getCwd() === cwd && !archivedById.has(session.sessionId))
|
|
1386
|
+
sessionIds.add(session.sessionId);
|
|
1387
|
+
}
|
|
1388
|
+
return [...sessionIds];
|
|
1389
|
+
}
|
|
1390
|
+
async ensureArchivedSessionMoved(record, session) {
|
|
1391
|
+
if (session === undefined || this.active.has(record.sessionId))
|
|
1392
|
+
return record;
|
|
1393
|
+
try {
|
|
1394
|
+
return await this.archiveStore.archive(archiveInputFromListEntry(session));
|
|
1395
|
+
}
|
|
1396
|
+
catch {
|
|
1397
|
+
return record;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
async ensureArchivedRecordMoved(record) {
|
|
1401
|
+
const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
|
|
1402
|
+
if (session === undefined)
|
|
1403
|
+
return record;
|
|
1404
|
+
const [moved] = await this.archiveStoreArchiveMany([archiveInputFromListEntry(session)]);
|
|
1405
|
+
return moved ?? record;
|
|
1406
|
+
}
|
|
1407
|
+
async ensureArchivedRecordsMoved(records) {
|
|
1408
|
+
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
1409
|
+
if (legacyRecords.length === 0)
|
|
1410
|
+
return;
|
|
1411
|
+
const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1412
|
+
const moveInputs = legacyRecords
|
|
1413
|
+
.map((record) => sessionsByCwd.get(record.cwd)?.find((candidate) => candidate.id === record.sessionId))
|
|
1414
|
+
.filter(isDefined)
|
|
1415
|
+
.map(archiveInputFromListEntry);
|
|
1416
|
+
await this.archiveStoreArchiveMany(moveInputs);
|
|
1417
|
+
}
|
|
1418
|
+
async archiveInputForSession(session) {
|
|
1419
|
+
const cwd = session.sessionManager.getCwd();
|
|
1420
|
+
const sessionFile = session.sessionFile;
|
|
1421
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
1422
|
+
throw new Error("Session is not persisted");
|
|
1423
|
+
const listed = (await this.sessionManager.list(cwd)).find((candidate) => candidate.id === session.sessionId);
|
|
1424
|
+
if (listed !== undefined)
|
|
1425
|
+
return archiveInputFromListEntry(listed);
|
|
1426
|
+
return archiveInputFromActiveSession(session);
|
|
1427
|
+
}
|
|
1428
|
+
async workspaceArchiveCandidates(cwd) {
|
|
1429
|
+
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
|
|
1430
|
+
const candidates = new Map();
|
|
1431
|
+
const archivedById = new Map();
|
|
1432
|
+
for (const record of archivedRecords) {
|
|
1433
|
+
if (record.cwd === cwd)
|
|
1434
|
+
archivedById.set(record.sessionId, record);
|
|
1435
|
+
}
|
|
1436
|
+
for (const session of sessions) {
|
|
1437
|
+
const archived = archivedById.get(session.id);
|
|
1438
|
+
if (archived === undefined)
|
|
1439
|
+
candidates.set(session.id, archiveCandidateFromListEntry(session));
|
|
1440
|
+
else {
|
|
1441
|
+
const candidate = archiveCandidateFromArchivedRecord(archived, session);
|
|
1442
|
+
if (candidate !== undefined)
|
|
1443
|
+
candidates.set(candidate.id, candidate);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
for (const record of archivedById.values()) {
|
|
1447
|
+
if (candidates.has(record.sessionId))
|
|
1448
|
+
continue;
|
|
1449
|
+
const candidate = archiveCandidateFromArchivedRecord(record, undefined);
|
|
1450
|
+
if (candidate !== undefined)
|
|
1451
|
+
candidates.set(candidate.id, candidate);
|
|
1452
|
+
}
|
|
1453
|
+
for (const active of new Set(this.active.values())) {
|
|
1454
|
+
const session = active.runtime.session;
|
|
1455
|
+
if (session.sessionManager.getCwd() !== cwd || archivedById.has(session.sessionId))
|
|
1456
|
+
continue;
|
|
1457
|
+
const existing = candidates.get(session.sessionId);
|
|
1458
|
+
candidates.set(session.sessionId, { ...(existing ?? archiveCandidateFromActiveSession(session, false)), activeSession: session });
|
|
1459
|
+
}
|
|
1460
|
+
return [...candidates.values()];
|
|
1461
|
+
}
|
|
1462
|
+
async listSessionNames(cwd) {
|
|
1463
|
+
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
|
|
1464
|
+
const names = new Set();
|
|
1465
|
+
for (const session of sessions)
|
|
1466
|
+
addSessionName(names, session.name);
|
|
1467
|
+
for (const record of archivedRecords) {
|
|
1468
|
+
if (record.cwd === cwd)
|
|
1469
|
+
addSessionName(names, record.name);
|
|
1470
|
+
}
|
|
1471
|
+
for (const active of new Set(this.active.values())) {
|
|
1472
|
+
const session = active.runtime.session;
|
|
1473
|
+
if (session.sessionManager.getCwd() === cwd)
|
|
1474
|
+
addSessionName(names, session.sessionName);
|
|
1475
|
+
}
|
|
1476
|
+
return [...names];
|
|
1477
|
+
}
|
|
1478
|
+
async closeActive(sessionId) {
|
|
1479
|
+
const active = this.active.get(sessionId);
|
|
1480
|
+
if (!active)
|
|
1481
|
+
return;
|
|
1482
|
+
this.active.delete(sessionId);
|
|
1483
|
+
this.schedulePromptService.stopForSession(sessionId);
|
|
1484
|
+
this.activities.delete(sessionId);
|
|
1485
|
+
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
|
|
1486
|
+
this.clearAuthLossWarningsForSession(sessionId);
|
|
1487
|
+
this.clearCompactionPromptQueue(sessionId);
|
|
1488
|
+
// Disarm subsession notification before teardown so the abort below cannot
|
|
1489
|
+
// emit a "stopped working" event that notifies the parent (e.g. on archive).
|
|
1490
|
+
// The parent/children link is kept so the parent can still see the child.
|
|
1491
|
+
if (this.subsessionLinkForActiveChild(active.runtime.session) !== undefined)
|
|
1492
|
+
this.subsessionNotifyArmed.delete(sessionId);
|
|
1493
|
+
clearSessionQueue(active.runtime.session);
|
|
1494
|
+
active.unsubscribe();
|
|
1495
|
+
try {
|
|
1496
|
+
await active.runtime.session.abort();
|
|
1497
|
+
}
|
|
1498
|
+
finally {
|
|
1499
|
+
await active.runtime.dispose();
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async gracefulStopAndClose(session, timeoutMs = 5000) {
|
|
1503
|
+
if (this.hasActiveWork(session)) {
|
|
1504
|
+
this.logger.info({ sessionId: session.sessionId }, "Session has active work, requesting abort for graceful shutdown...");
|
|
1505
|
+
// Signal an abort to stop the stream/work
|
|
1506
|
+
await session.abort().catch(() => undefined);
|
|
1507
|
+
// Wait for the session to settle and fire its hooks
|
|
1508
|
+
const start = Date.now();
|
|
1509
|
+
while (this.hasActiveWork(session)) {
|
|
1510
|
+
if (Date.now() - start > timeoutMs) {
|
|
1511
|
+
this.logger.info({ sessionId: session.sessionId, timeoutMs }, "Session did not settle within timeout, forcing hard kill.");
|
|
1512
|
+
break;
|
|
1513
|
+
}
|
|
1514
|
+
// Small delay to yield the event loop
|
|
1515
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
// Give any fire-and-forget hooks (like ai-memory's stop.sh) a brief window to read the file before we move it
|
|
1519
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1520
|
+
await this.closeActive(session.sessionId);
|
|
1521
|
+
}
|
|
1522
|
+
async assertWritable(ref) {
|
|
1523
|
+
if (await this.getArchived(ref) !== undefined)
|
|
1524
|
+
throw new Error("Archived sessions are read-only. Restore the session to continue.");
|
|
1525
|
+
}
|
|
1526
|
+
async getOrOpen(ref) {
|
|
1527
|
+
return (await this.getActive(ref)).runtime.session;
|
|
1528
|
+
}
|
|
1529
|
+
async getActive(ref) {
|
|
1530
|
+
const active = this.activeForLookup(ref);
|
|
1531
|
+
if (active !== undefined)
|
|
1532
|
+
return active;
|
|
1533
|
+
const archived = await this.getArchived(ref);
|
|
1534
|
+
if (archived?.archivePath !== undefined)
|
|
1535
|
+
return this.create(await this.sessionManager.open(archived.archivePath), archived.cwd);
|
|
1536
|
+
const match = isPiSessionRef(ref)
|
|
1537
|
+
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
1538
|
+
: (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
1539
|
+
if (!match)
|
|
1540
|
+
throw new Error("Session not found");
|
|
1541
|
+
return this.create(await this.sessionManager.open(match.path), match.cwd);
|
|
1542
|
+
}
|
|
1543
|
+
async getArchived(ref) {
|
|
1544
|
+
const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
|
|
1545
|
+
if (archived === undefined)
|
|
1546
|
+
return undefined;
|
|
1547
|
+
if (isPiSessionRef(ref) && archived.cwd !== ref.cwd)
|
|
1548
|
+
return undefined;
|
|
1549
|
+
return archived;
|
|
1550
|
+
}
|
|
1551
|
+
activeForLookup(ref) {
|
|
1552
|
+
const sessionId = sessionIdFromLookup(ref);
|
|
1553
|
+
const exact = this.active.get(sessionId);
|
|
1554
|
+
if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
|
|
1555
|
+
return exact;
|
|
1556
|
+
for (const [candidateId, active] of this.active.entries()) {
|
|
1557
|
+
if (candidateId.startsWith(sessionId) && lookupMatchesActiveSession(ref, active))
|
|
1558
|
+
return active;
|
|
1559
|
+
}
|
|
1560
|
+
return undefined;
|
|
1561
|
+
}
|
|
1562
|
+
async create(sessionManager, cwd, options = {}) {
|
|
1563
|
+
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
1564
|
+
cwd,
|
|
1565
|
+
agentDir: this.agentDir,
|
|
1566
|
+
sessionManager,
|
|
1567
|
+
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
1568
|
+
});
|
|
1569
|
+
const active = { runtime, unsubscribe: noop };
|
|
1570
|
+
await this.bindSessionExtensions(runtime.session);
|
|
1571
|
+
this.bindRuntime(active);
|
|
1572
|
+
runtime.setRebindSession(async (session) => {
|
|
1573
|
+
await this.bindSessionExtensions(session);
|
|
1574
|
+
this.bindRuntime(active);
|
|
1575
|
+
await this.recoverSubsessionTrackingForOpenedSession(session);
|
|
1576
|
+
});
|
|
1577
|
+
this.active.set(runtime.session.sessionId, active);
|
|
1578
|
+
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
1579
|
+
this.publishStatus(runtime.session);
|
|
1580
|
+
return active;
|
|
1581
|
+
}
|
|
1582
|
+
async bindSessionExtensions(session) {
|
|
1583
|
+
await session.bindExtensions({
|
|
1584
|
+
onError: (error) => {
|
|
1585
|
+
const message = `${error.extensionPath}: ${error.error}`;
|
|
1586
|
+
this.publishActivity(session, "extension error", "error", message);
|
|
1587
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1588
|
+
},
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
bindRuntime(active) {
|
|
1592
|
+
active.unsubscribe();
|
|
1593
|
+
const { session } = active.runtime;
|
|
1594
|
+
for (const [sessionId, candidate] of this.active.entries()) {
|
|
1595
|
+
if (candidate === active) {
|
|
1596
|
+
this.active.delete(sessionId);
|
|
1597
|
+
if (sessionId !== session.sessionId)
|
|
1598
|
+
this.clearCompactionPromptQueue(sessionId);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
active.unsubscribe = session.subscribe((event) => {
|
|
1602
|
+
this.events.publish(session.sessionId, toClientEvent(event));
|
|
1603
|
+
this.publishActivityForEvent(session, event);
|
|
1604
|
+
const eventType = getString(event, "type");
|
|
1605
|
+
if (eventType === "compaction_end")
|
|
1606
|
+
this.scheduleCompactionQueueDrain(session.sessionId);
|
|
1607
|
+
if (eventType === "agent_start" || eventType === "agent_end")
|
|
1608
|
+
this.scheduleCompactionQueueDrain(session.sessionId);
|
|
1609
|
+
this.publishStatus(session);
|
|
1610
|
+
this.updateSubsessionTracking(session);
|
|
1611
|
+
});
|
|
1612
|
+
this.active.set(session.sessionId, active);
|
|
1613
|
+
}
|
|
1614
|
+
scheduleCompactionQueueDrain(sessionId, delayMs = 0) {
|
|
1615
|
+
if (!this.compactionPromptQueues.has(sessionId) || this.compactionDrainTimers.has(sessionId))
|
|
1616
|
+
return;
|
|
1617
|
+
const timer = setTimeout(() => {
|
|
1618
|
+
this.compactionDrainTimers.delete(sessionId);
|
|
1619
|
+
this.drainCompactionPromptQueue(sessionId);
|
|
1620
|
+
}, delayMs);
|
|
1621
|
+
this.compactionDrainTimers.set(sessionId, timer);
|
|
1622
|
+
}
|
|
1623
|
+
drainCompactionPromptQueue(sessionId) {
|
|
1624
|
+
const active = this.active.get(sessionId);
|
|
1625
|
+
if (active === undefined)
|
|
1626
|
+
return;
|
|
1627
|
+
const { session } = active.runtime;
|
|
1628
|
+
if (session.isCompacting) {
|
|
1629
|
+
this.scheduleCompactionQueueDrain(sessionId, 100);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
if (session.isStreaming) {
|
|
1633
|
+
const queued = this.takeCompactionPromptQueue(sessionId);
|
|
1634
|
+
if (queued.length === 0)
|
|
1635
|
+
return;
|
|
1636
|
+
this.publishStatus(session);
|
|
1637
|
+
for (const prompt of queued)
|
|
1638
|
+
void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images, prompt.echoUserMessage ?? true);
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
const prompt = this.shiftCompactionPrompt(sessionId);
|
|
1642
|
+
if (prompt === undefined)
|
|
1643
|
+
return;
|
|
1644
|
+
this.publishStatus(session);
|
|
1645
|
+
const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images, prompt.echoUserMessage ?? true);
|
|
1646
|
+
void submitted.finally(() => { this.scheduleCompactionQueueDrain(sessionId); });
|
|
1647
|
+
}
|
|
1648
|
+
takeCompactionPromptQueue(sessionId) {
|
|
1649
|
+
const queued = this.compactionPromptQueues.get(sessionId) ?? [];
|
|
1650
|
+
this.compactionPromptQueues.delete(sessionId);
|
|
1651
|
+
return queued;
|
|
1652
|
+
}
|
|
1653
|
+
shiftCompactionPrompt(sessionId) {
|
|
1654
|
+
const queue = this.compactionPromptQueues.get(sessionId);
|
|
1655
|
+
const prompt = queue?.shift();
|
|
1656
|
+
if (queue === undefined || queue.length === 0)
|
|
1657
|
+
this.compactionPromptQueues.delete(sessionId);
|
|
1658
|
+
return prompt;
|
|
1659
|
+
}
|
|
1660
|
+
clearCompactionPromptQueue(sessionId) {
|
|
1661
|
+
this.compactionPromptQueues.delete(sessionId);
|
|
1662
|
+
const timer = this.compactionDrainTimers.get(sessionId);
|
|
1663
|
+
if (timer !== undefined) {
|
|
1664
|
+
clearTimeout(timer);
|
|
1665
|
+
this.compactionDrainTimers.delete(sessionId);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
clearCompactionDrainTimers() {
|
|
1669
|
+
for (const timer of this.compactionDrainTimers.values())
|
|
1670
|
+
clearTimeout(timer);
|
|
1671
|
+
this.compactionDrainTimers.clear();
|
|
1672
|
+
}
|
|
1673
|
+
maybeGenerateSessionName(session, firstMessage) {
|
|
1674
|
+
if (session.sessionName !== undefined || session.messages.length !== 0 || session.isStreaming || session.isCompacting)
|
|
1675
|
+
return;
|
|
1676
|
+
const model = session.model;
|
|
1677
|
+
if (model === undefined)
|
|
1678
|
+
return;
|
|
1679
|
+
void generateShortSessionName(session.agent.streamFn, model, firstMessage).then((name) => {
|
|
1680
|
+
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
1681
|
+
}).catch(() => {
|
|
1682
|
+
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
applyGeneratedSessionName(session, name) {
|
|
1686
|
+
if (name === undefined || session.sessionName !== undefined)
|
|
1687
|
+
return;
|
|
1688
|
+
session.setSessionName(name);
|
|
1689
|
+
this.publishSessionName(session);
|
|
1690
|
+
}
|
|
1691
|
+
applyAuthChange(change = {}) {
|
|
1692
|
+
void this.modelRegistry?.refresh();
|
|
1693
|
+
for (const active of this.active.values()) {
|
|
1694
|
+
const { session } = active.runtime;
|
|
1695
|
+
void session.modelRegistry.refresh();
|
|
1696
|
+
this.syncCurrentModelAuthWarning(session, change.removedProviderId);
|
|
1697
|
+
this.publishStatus(session);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
syncCurrentModelAuthWarning(session, removedProviderId) {
|
|
1701
|
+
const model = session.model;
|
|
1702
|
+
if (model === undefined)
|
|
1703
|
+
return;
|
|
1704
|
+
if (model.provider === "unknown" && model.id === "unknown")
|
|
1705
|
+
return;
|
|
1706
|
+
const warningKey = authLossWarningKey(session.sessionId, model.provider, model.id);
|
|
1707
|
+
const registered = session.modelRegistry.find(model.provider, model.id);
|
|
1708
|
+
if (registered === undefined)
|
|
1709
|
+
return;
|
|
1710
|
+
if (session.modelRegistry.hasConfiguredAuth(registered)) {
|
|
1711
|
+
this.authLossWarnings.delete(warningKey);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
if (removedProviderId === undefined || model.provider !== removedProviderId || this.authLossWarnings.has(warningKey))
|
|
1715
|
+
return;
|
|
1716
|
+
this.authLossWarnings.add(warningKey);
|
|
1717
|
+
this.events.publish(session.sessionId, {
|
|
1718
|
+
type: "command.output",
|
|
1719
|
+
level: "error",
|
|
1720
|
+
message: `Authentication for ${model.provider}/${model.id} was removed. Use /model to select another model.`,
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
clearAuthLossWarningsForSession(sessionId) {
|
|
1724
|
+
const prefix = `${sessionId}:`;
|
|
1725
|
+
for (const key of this.authLossWarnings) {
|
|
1726
|
+
if (key.startsWith(prefix))
|
|
1727
|
+
this.authLossWarnings.delete(key);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
publishSessionName(session) {
|
|
1731
|
+
const event = session.sessionName === undefined
|
|
1732
|
+
? { type: "session.name", sessionId: session.sessionId }
|
|
1733
|
+
: { type: "session.name", sessionId: session.sessionId, name: session.sessionName };
|
|
1734
|
+
this.events.publish(session.sessionId, event);
|
|
1735
|
+
this.events.publishGlobal(event);
|
|
1736
|
+
}
|
|
1737
|
+
publishHeartbeats() {
|
|
1738
|
+
for (const active of this.active.values()) {
|
|
1739
|
+
const { session } = active.runtime;
|
|
1740
|
+
// Re-evaluate subsession completion here too: agent_end can arrive while
|
|
1741
|
+
// the session still reports active work transiently, so the event-driven
|
|
1742
|
+
// latch may not fire. The heartbeat re-checks once the session settles.
|
|
1743
|
+
this.updateSubsessionTracking(session);
|
|
1744
|
+
const activity = this.activities.get(session.sessionId);
|
|
1745
|
+
if (!this.hasActiveWork(session)) {
|
|
1746
|
+
if (activity?.phase === "active")
|
|
1747
|
+
this.publishStatus(session);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
this.publishStatus(session);
|
|
1751
|
+
if (activity?.phase === "active")
|
|
1752
|
+
this.publishActivity(session, activity.label, "active", activity.detail);
|
|
1753
|
+
else
|
|
1754
|
+
this.publishActivity(session, this.activityLabelFromStatus(session), "active");
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
activityLabelFromStatus(session) {
|
|
1758
|
+
if (session.isCompacting)
|
|
1759
|
+
return "compacting";
|
|
1760
|
+
if (session.isBashRunning)
|
|
1761
|
+
return "running bash";
|
|
1762
|
+
if (session.isStreaming)
|
|
1763
|
+
return "agent running";
|
|
1764
|
+
if (this.pendingMessageCount(session) > 0)
|
|
1765
|
+
return "queued";
|
|
1766
|
+
return "active";
|
|
1767
|
+
}
|
|
1768
|
+
hasActiveWork(session) {
|
|
1769
|
+
return sessionHasActiveWork(session, this.compactionQueuedMessages(session.sessionId).length);
|
|
1770
|
+
}
|
|
1771
|
+
publishActivityForEvent(session, event) {
|
|
1772
|
+
const eventType = getString(event, "type");
|
|
1773
|
+
if (eventType === undefined)
|
|
1774
|
+
return;
|
|
1775
|
+
if (eventType === "agent_start") {
|
|
1776
|
+
this.publishActivity(session, "agent running", "active");
|
|
1777
|
+
this.notifiedWorkComplete.delete(session.sessionId);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (eventType === "agent_end") {
|
|
1781
|
+
this.publishActivity(session, "idle", "idle");
|
|
1782
|
+
setTimeout(() => {
|
|
1783
|
+
this.publishActivity(session, "idle", "idle");
|
|
1784
|
+
this.publishStatus(session);
|
|
1785
|
+
if (this.pushService !== undefined && !this.notifiedWorkComplete.has(session.sessionId) && !sessionHasActiveWork(session)) {
|
|
1786
|
+
this.notifiedWorkComplete.add(session.sessionId);
|
|
1787
|
+
const messages = session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch();
|
|
1788
|
+
void this.pushService.notifySessionComplete(session.sessionId, session.sessionName, messages);
|
|
1789
|
+
}
|
|
1790
|
+
}, 250);
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
if (eventType === "turn_end") {
|
|
1794
|
+
this.publishActivity(session, "turn complete", "idle");
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
if (eventType === "message_start") {
|
|
1798
|
+
this.publishActivity(session, "message started", "active");
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if (eventType === "message_end") {
|
|
1802
|
+
this.publishActivity(session, "message complete", "idle");
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (eventType === "message_update") {
|
|
1806
|
+
this.publishActivity(session, "receiving response", "active");
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
if (eventType === "tool_execution_start") {
|
|
1810
|
+
this.publishActivity(session, "running tool", "active", getString(event, "toolName"));
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (eventType === "tool_execution_end") {
|
|
1814
|
+
const isError = getBoolean(event, "isError") === true;
|
|
1815
|
+
this.publishActivity(session, isError ? "tool failed" : "tool complete", isError ? "error" : "idle", getString(event, "toolName"));
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
if (eventType === "bash_execution_start") {
|
|
1819
|
+
this.publishActivity(session, "running bash", "active");
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
if (eventType === "bash_execution_end") {
|
|
1823
|
+
this.publishActivity(session, "bash complete", "idle");
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
if (this.hasActiveWork(session))
|
|
1827
|
+
this.publishActivity(session, eventType.replaceAll("_", " "), "active");
|
|
1828
|
+
}
|
|
1829
|
+
publishActivity(session, label, phase, detail) {
|
|
1830
|
+
const at = new Date().toISOString();
|
|
1831
|
+
const stored = detail === undefined ? { phase, label, at } : { phase, label, detail, at };
|
|
1832
|
+
this.activities.set(session.sessionId, stored);
|
|
1833
|
+
const activity = detail === undefined ? { sessionId: session.sessionId, phase, label, at } : { sessionId: session.sessionId, phase, label, detail, at };
|
|
1834
|
+
this.workspaceActivity?.applySessionActivity(session.sessionManager.getCwd(), activity);
|
|
1835
|
+
this.events.publish(session.sessionId, { type: "activity.update", activity });
|
|
1836
|
+
this.events.publishGlobal({ type: "activity.update", activity });
|
|
1837
|
+
}
|
|
1838
|
+
publishStatus(session) {
|
|
1839
|
+
const status = this.statusFromSession(session);
|
|
1840
|
+
this.clearStaleActiveActivity(session);
|
|
1841
|
+
this.workspaceActivity?.applySessionStatus(session.sessionManager.getCwd(), status);
|
|
1842
|
+
this.events.publish(session.sessionId, { type: "status.update", status });
|
|
1843
|
+
this.events.publishGlobal({ type: "status.update", status });
|
|
1844
|
+
}
|
|
1845
|
+
clearStaleActiveActivity(session) {
|
|
1846
|
+
const current = this.activities.get(session.sessionId);
|
|
1847
|
+
if (current?.phase !== "active" || this.hasActiveWork(session))
|
|
1848
|
+
return;
|
|
1849
|
+
const at = new Date().toISOString();
|
|
1850
|
+
const stored = { phase: "idle", label: "idle", at };
|
|
1851
|
+
this.activities.set(session.sessionId, stored);
|
|
1852
|
+
const activity = { sessionId: session.sessionId, ...stored };
|
|
1853
|
+
this.events.publish(session.sessionId, { type: "activity.update", activity });
|
|
1854
|
+
this.events.publishGlobal({ type: "activity.update", activity });
|
|
1855
|
+
}
|
|
1856
|
+
statusFromSession(session) {
|
|
1857
|
+
const stats = session.getSessionStats();
|
|
1858
|
+
const model = session.model === undefined ? undefined : modelToClientModel(session.model);
|
|
1859
|
+
const contextUsage = session.getContextUsage();
|
|
1860
|
+
return {
|
|
1861
|
+
sessionId: session.sessionId,
|
|
1862
|
+
persisted: sessionFileExists(session.sessionFile),
|
|
1863
|
+
...(model === undefined ? {} : { model }),
|
|
1864
|
+
thinkingLevel: session.thinkingLevel,
|
|
1865
|
+
isStreaming: session.isStreaming,
|
|
1866
|
+
isCompacting: session.isCompacting,
|
|
1867
|
+
isBashRunning: session.isBashRunning,
|
|
1868
|
+
pendingMessageCount: this.pendingMessageCount(session),
|
|
1869
|
+
queuedMessages: queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)),
|
|
1870
|
+
messageCount: session.messages.length,
|
|
1871
|
+
tokens: stats.tokens,
|
|
1872
|
+
cost: stats.cost,
|
|
1873
|
+
...(contextUsage === undefined ? {} : { contextUsage }),
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
pendingMessageCount(session) {
|
|
1877
|
+
return session.pendingMessageCount + this.compactionQueuedMessages(session.sessionId).length;
|
|
1878
|
+
}
|
|
1879
|
+
compactionQueuedMessages(sessionId) {
|
|
1880
|
+
return this.compactionPromptQueues.get(sessionId) ?? [];
|
|
1881
|
+
}
|
|
1882
|
+
hasQueuedMessageText(session, text) {
|
|
1883
|
+
return queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)).some((message) => message.text === text);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
function previewResponseFromPlan(plan) {
|
|
1887
|
+
return {
|
|
1888
|
+
generatedAt: plan.generatedAt,
|
|
1889
|
+
thresholds: plan.thresholds,
|
|
1890
|
+
projects: plan.projects,
|
|
1891
|
+
totals: plan.totals,
|
|
1892
|
+
...(plan.skippedBusySessionIds.length === 0 ? {} : { skippedBusySessionIds: plan.skippedBusySessionIds }),
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
function uniqueBulkSessionRefs(refs) {
|
|
1896
|
+
const seen = new Set();
|
|
1897
|
+
const unique = [];
|
|
1898
|
+
for (const ref of refs) {
|
|
1899
|
+
const key = `${ref.cwd ?? ""}\0${ref.id}`;
|
|
1900
|
+
if (seen.has(key))
|
|
1901
|
+
continue;
|
|
1902
|
+
seen.add(key);
|
|
1903
|
+
unique.push(ref);
|
|
1904
|
+
}
|
|
1905
|
+
return unique;
|
|
1906
|
+
}
|
|
1907
|
+
function bulkRefToLookup(ref) {
|
|
1908
|
+
return ref.cwd === undefined ? ref.id : { id: ref.id, cwd: ref.cwd };
|
|
1909
|
+
}
|
|
1910
|
+
function findArchivedRecordForBulkRef(records, ref) {
|
|
1911
|
+
return records.find((record) => (ref.cwd === undefined || record.cwd === ref.cwd) && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
|
|
1912
|
+
}
|
|
1913
|
+
function findListedSessionForBulkRef(context, ref) {
|
|
1914
|
+
if (ref.cwd !== undefined)
|
|
1915
|
+
return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
1916
|
+
return context.allSessions === undefined ? undefined : findSessionByIdOrPrefix(context.allSessions, ref.id);
|
|
1917
|
+
}
|
|
1918
|
+
function findSessionByIdOrPrefix(sessions, sessionId) {
|
|
1919
|
+
return sessions.find((session) => session.id === sessionId) ?? sessions.find((session) => session.id.startsWith(sessionId));
|
|
1920
|
+
}
|
|
1921
|
+
function uniqueStrings(values) {
|
|
1922
|
+
return [...new Set(values)];
|
|
1923
|
+
}
|
|
1924
|
+
function modelToClientModel(model) {
|
|
1925
|
+
if (model === undefined)
|
|
1926
|
+
return {};
|
|
1927
|
+
const name = getString(model, "name");
|
|
1928
|
+
const reasoning = getProperty(model, "reasoning");
|
|
1929
|
+
return {
|
|
1930
|
+
provider: model.provider,
|
|
1931
|
+
id: model.id,
|
|
1932
|
+
...(name === undefined ? {} : { name }),
|
|
1933
|
+
...(model.contextWindow === null ? {} : { contextWindow: model.contextWindow }),
|
|
1934
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
function clientSessionFromListEntry(session) {
|
|
1938
|
+
return {
|
|
1939
|
+
id: session.id,
|
|
1940
|
+
path: session.path,
|
|
1941
|
+
cwd: session.cwd,
|
|
1942
|
+
persisted: true,
|
|
1943
|
+
...(session.name === undefined ? {} : { name: session.name }),
|
|
1944
|
+
created: session.created.toISOString(),
|
|
1945
|
+
modified: session.modified.toISOString(),
|
|
1946
|
+
messageCount: session.messageCount,
|
|
1947
|
+
firstMessage: session.firstMessage,
|
|
1948
|
+
...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
function archiveInputFromListEntry(session) {
|
|
1952
|
+
return {
|
|
1953
|
+
sessionId: session.id,
|
|
1954
|
+
cwd: session.cwd,
|
|
1955
|
+
path: session.path,
|
|
1956
|
+
created: session.created.toISOString(),
|
|
1957
|
+
modified: session.modified.toISOString(),
|
|
1958
|
+
messageCount: session.messageCount,
|
|
1959
|
+
firstMessage: session.firstMessage,
|
|
1960
|
+
...(session.name === undefined ? {} : { name: session.name }),
|
|
1961
|
+
...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
function archiveInputFromActiveSession(session) {
|
|
1965
|
+
const sessionFile = session.sessionFile;
|
|
1966
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
1967
|
+
throw new Error("Session is not persisted");
|
|
1968
|
+
const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
|
|
1969
|
+
return {
|
|
1970
|
+
sessionId: session.sessionId,
|
|
1971
|
+
cwd: session.sessionManager.getCwd(),
|
|
1972
|
+
path: sessionFile,
|
|
1973
|
+
created: new Date().toISOString(),
|
|
1974
|
+
modified: new Date().toISOString(),
|
|
1975
|
+
messageCount: session.messages.length,
|
|
1976
|
+
firstMessage: "",
|
|
1977
|
+
...(session.sessionName === undefined ? {} : { name: session.sessionName }),
|
|
1978
|
+
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
function archiveCandidateFromListEntry(session) {
|
|
1982
|
+
return {
|
|
1983
|
+
id: session.id,
|
|
1984
|
+
path: session.path,
|
|
1985
|
+
cwd: session.cwd,
|
|
1986
|
+
archived: false,
|
|
1987
|
+
listEntry: session,
|
|
1988
|
+
...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
function archiveCandidateFromArchivedRecord(record, fallback) {
|
|
1992
|
+
const path = record.originalPath ?? fallback?.path;
|
|
1993
|
+
if (path === undefined)
|
|
1994
|
+
return undefined;
|
|
1995
|
+
const parentSessionPath = record.parentSessionPath ?? fallback?.parentSessionPath;
|
|
1996
|
+
return {
|
|
1997
|
+
id: record.sessionId,
|
|
1998
|
+
path,
|
|
1999
|
+
cwd: record.cwd,
|
|
2000
|
+
archived: true,
|
|
2001
|
+
...(fallback === undefined ? {} : { listEntry: fallback }),
|
|
2002
|
+
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
function archiveCandidateFromActiveSession(session, archived) {
|
|
2006
|
+
const sessionFile = session.sessionFile;
|
|
2007
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
2008
|
+
throw new Error("Session is not persisted");
|
|
2009
|
+
const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
|
|
2010
|
+
return {
|
|
2011
|
+
id: session.sessionId,
|
|
2012
|
+
path: sessionFile,
|
|
2013
|
+
cwd: session.sessionManager.getCwd(),
|
|
2014
|
+
archived,
|
|
2015
|
+
activeSession: session,
|
|
2016
|
+
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function archiveInputFromCandidate(candidate) {
|
|
2020
|
+
if (candidate.listEntry !== undefined)
|
|
2021
|
+
return archiveInputFromListEntry(candidate.listEntry);
|
|
2022
|
+
if (candidate.activeSession !== undefined)
|
|
2023
|
+
return archiveInputFromActiveSession(candidate.activeSession);
|
|
2024
|
+
throw new Error(`Session is not available for archiving: ${candidate.id}`);
|
|
2025
|
+
}
|
|
2026
|
+
function sessionHasActiveWork(session, extraQueuedMessageCount = 0) {
|
|
2027
|
+
return session.isStreaming || session.isCompacting || session.isBashRunning || session.pendingMessageCount + extraQueuedMessageCount > 0;
|
|
2028
|
+
}
|
|
2029
|
+
function clientSessionFromArchivedRecord(record, fallback) {
|
|
2030
|
+
const path = record.originalPath ?? fallback?.path;
|
|
2031
|
+
const created = record.created ?? fallback?.created.toISOString();
|
|
2032
|
+
const modified = record.modified ?? fallback?.modified.toISOString();
|
|
2033
|
+
const messageCount = record.messageCount ?? fallback?.messageCount;
|
|
2034
|
+
const firstMessage = record.firstMessage ?? fallback?.firstMessage;
|
|
2035
|
+
if (path === undefined || created === undefined || modified === undefined || messageCount === undefined || firstMessage === undefined)
|
|
2036
|
+
return undefined;
|
|
2037
|
+
const name = record.name ?? fallback?.name;
|
|
2038
|
+
const parentSessionPath = record.parentSessionPath ?? fallback?.parentSessionPath;
|
|
2039
|
+
return {
|
|
2040
|
+
id: record.sessionId,
|
|
2041
|
+
path,
|
|
2042
|
+
cwd: record.cwd,
|
|
2043
|
+
...(name === undefined ? {} : { name }),
|
|
2044
|
+
created,
|
|
2045
|
+
modified,
|
|
2046
|
+
messageCount,
|
|
2047
|
+
firstMessage,
|
|
2048
|
+
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
2049
|
+
archived: true,
|
|
2050
|
+
archivedAt: record.archivedAt,
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
function addSessionName(names, name) {
|
|
2054
|
+
const trimmed = name?.replace(/\s+/g, " ").trim();
|
|
2055
|
+
if (trimmed !== undefined && trimmed !== "")
|
|
2056
|
+
names.add(trimmed);
|
|
2057
|
+
}
|
|
2058
|
+
function compareArchivedRecords(a, b) {
|
|
2059
|
+
return archivedTimestamp(b) - archivedTimestamp(a);
|
|
2060
|
+
}
|
|
2061
|
+
function archivedTimestamp(record) {
|
|
2062
|
+
const time = Date.parse(record.archivedAt);
|
|
2063
|
+
return Number.isNaN(time) ? 0 : time;
|
|
2064
|
+
}
|
|
2065
|
+
function isDefined(value) {
|
|
2066
|
+
return value !== undefined;
|
|
2067
|
+
}
|
|
2068
|
+
function trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile) {
|
|
2069
|
+
return {
|
|
2070
|
+
parentSessionId,
|
|
2071
|
+
childSessionId: link.spawnedSessionId,
|
|
2072
|
+
...(link.spawnedSessionFile === undefined ? {} : { childSessionFile: link.spawnedSessionFile }),
|
|
2073
|
+
parentSessionFile,
|
|
2074
|
+
...(link.cwd === undefined ? {} : { cwd: link.cwd }),
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
function persistedParentSubsessionLinkData(link) {
|
|
2078
|
+
return {
|
|
2079
|
+
version: 1,
|
|
2080
|
+
spawnedBySessionId: link.parentSessionId,
|
|
2081
|
+
spawnedSessionId: link.childSessionId,
|
|
2082
|
+
...(link.childSessionFile === undefined ? {} : { spawnedSessionFile: link.childSessionFile }),
|
|
2083
|
+
...(link.cwd === undefined ? {} : { cwd: link.cwd }),
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
function persistedChildSubsessionLinkData(parentSessionId, childSessionId) {
|
|
2087
|
+
return {
|
|
2088
|
+
version: 1,
|
|
2089
|
+
spawnedBySessionId: parentSessionId,
|
|
2090
|
+
spawnedSessionId: childSessionId,
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
function parsePersistedParentSubsessionLink(entry) {
|
|
2094
|
+
if (!isRecord(entry) || entry["type"] !== "custom" || entry["customType"] !== SUBSESSION_LINK_CUSTOM_TYPE)
|
|
2095
|
+
return undefined;
|
|
2096
|
+
const data = entry["data"];
|
|
2097
|
+
if (!isRecord(data))
|
|
2098
|
+
return undefined;
|
|
2099
|
+
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
2100
|
+
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
2101
|
+
if (spawnedBySessionId === undefined || spawnedBySessionId === "" || spawnedSessionId === undefined || spawnedSessionId === "")
|
|
2102
|
+
return undefined;
|
|
2103
|
+
const spawnedSessionFile = getString(data, "spawnedSessionFile");
|
|
2104
|
+
const cwd = getString(data, "cwd");
|
|
2105
|
+
return {
|
|
2106
|
+
spawnedBySessionId,
|
|
2107
|
+
spawnedSessionId,
|
|
2108
|
+
...(spawnedSessionFile === undefined || spawnedSessionFile === "" ? {} : { spawnedSessionFile }),
|
|
2109
|
+
...(cwd === undefined || cwd === "" ? {} : { cwd }),
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
function parsePersistedChildSubsessionLink(entry) {
|
|
2113
|
+
if (!isRecord(entry) || entry["type"] !== "custom" || entry["customType"] !== SUBSESSION_CHILD_LINK_CUSTOM_TYPE)
|
|
2114
|
+
return undefined;
|
|
2115
|
+
const data = entry["data"];
|
|
2116
|
+
if (!isRecord(data))
|
|
2117
|
+
return undefined;
|
|
2118
|
+
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
2119
|
+
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
2120
|
+
if (spawnedBySessionId === undefined || spawnedBySessionId === "" || spawnedSessionId === undefined || spawnedSessionId === "")
|
|
2121
|
+
return undefined;
|
|
2122
|
+
return { spawnedBySessionId, spawnedSessionId };
|
|
2123
|
+
}
|
|
2124
|
+
function nonEmptyString(value) {
|
|
2125
|
+
return value === undefined || value === "" ? undefined : value;
|
|
2126
|
+
}
|
|
2127
|
+
function subsessionHydratedParentKey(parentSessionId, parentSessionFile) {
|
|
2128
|
+
return `${parentSessionId}\0${parentSessionFile ?? ""}`;
|
|
2129
|
+
}
|
|
2130
|
+
function sessionPathsEqual(a, b) {
|
|
2131
|
+
return cwdPathsEqual(a, b);
|
|
2132
|
+
}
|
|
2133
|
+
function sessionFileExists(sessionFile) {
|
|
2134
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
2135
|
+
return false;
|
|
2136
|
+
try {
|
|
2137
|
+
return statSync(sessionFile).isFile();
|
|
2138
|
+
}
|
|
2139
|
+
catch {
|
|
2140
|
+
return false;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
function sessionFileMatches(session, expectedSessionFile) {
|
|
2144
|
+
const sessionFile = nonEmptyString(session.sessionFile);
|
|
2145
|
+
return sessionFile !== undefined && expectedSessionFile !== undefined && sessionPathsEqual(sessionFile, expectedSessionFile);
|
|
2146
|
+
}
|
|
2147
|
+
function activeSessionFileMatches(active, expectedSessionFile) {
|
|
2148
|
+
return sessionFileMatches(active.runtime.session, expectedSessionFile);
|
|
2149
|
+
}
|
|
2150
|
+
function trackedLinkParentFileMatches(link, parentSessionFile) {
|
|
2151
|
+
return link.parentSessionFile !== undefined && sessionPathsEqual(link.parentSessionFile, parentSessionFile);
|
|
2152
|
+
}
|
|
2153
|
+
async function readSessionHeaderSummary(sessionFile) {
|
|
2154
|
+
let file;
|
|
2155
|
+
try {
|
|
2156
|
+
file = await open(sessionFile, "r");
|
|
2157
|
+
const buffer = Buffer.alloc(4096);
|
|
2158
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
2159
|
+
const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n", 1)[0];
|
|
2160
|
+
if (firstLine === undefined || firstLine === "")
|
|
2161
|
+
return undefined;
|
|
2162
|
+
const header = JSON.parse(firstLine);
|
|
2163
|
+
if (!isRecord(header) || header["type"] !== "session" || typeof header["id"] !== "string")
|
|
2164
|
+
return undefined;
|
|
2165
|
+
const parentSession = getString(header, "parentSession");
|
|
2166
|
+
return { id: header["id"], ...(parentSession === undefined ? {} : { parentSession }) };
|
|
2167
|
+
}
|
|
2168
|
+
catch {
|
|
2169
|
+
return undefined;
|
|
2170
|
+
}
|
|
2171
|
+
finally {
|
|
2172
|
+
await file?.close().catch(() => undefined);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
async function sessionFileHeaderMatches(sessionFile, expected) {
|
|
2176
|
+
const header = await readSessionHeaderSummary(sessionFile);
|
|
2177
|
+
if (header?.id !== expected.sessionId)
|
|
2178
|
+
return false;
|
|
2179
|
+
if (expected.parentSessionFile === undefined)
|
|
2180
|
+
return true;
|
|
2181
|
+
return header.parentSession !== undefined && sessionPathsEqual(header.parentSession, expected.parentSessionFile);
|
|
2182
|
+
}
|
|
2183
|
+
async function clearParentSession(sessionFile) {
|
|
2184
|
+
const content = await readFile(sessionFile, "utf8");
|
|
2185
|
+
const newlineIndex = content.indexOf("\n");
|
|
2186
|
+
const firstLine = newlineIndex === -1 ? content : content.slice(0, newlineIndex);
|
|
2187
|
+
const rest = newlineIndex === -1 ? "" : content.slice(newlineIndex);
|
|
2188
|
+
const header = JSON.parse(firstLine);
|
|
2189
|
+
if (!isRecord(header) || header["type"] !== "session")
|
|
2190
|
+
throw new Error("Invalid session file header");
|
|
2191
|
+
if (header["parentSession"] === undefined)
|
|
2192
|
+
return;
|
|
2193
|
+
delete header["parentSession"];
|
|
2194
|
+
await writeFile(sessionFile, `${JSON.stringify(header)}${rest}`, "utf8");
|
|
2195
|
+
}
|
|
2196
|
+
function clearParentSessionHeader(sessionManager) {
|
|
2197
|
+
const header = sessionManager.getHeader?.();
|
|
2198
|
+
if (header !== undefined && header !== null)
|
|
2199
|
+
delete header.parentSession;
|
|
2200
|
+
}
|
|
2201
|
+
function clearSessionQueue(session) {
|
|
2202
|
+
session.clearQueue();
|
|
2203
|
+
}
|
|
2204
|
+
function queuedMessagesFromSession(session, extraQueuedMessages = []) {
|
|
2205
|
+
return [
|
|
2206
|
+
...session.getSteeringMessages().map((text) => ({ kind: "steer", text })),
|
|
2207
|
+
...session.getFollowUpMessages().map((text) => ({ kind: "followUp", text })),
|
|
2208
|
+
...extraQueuedMessages,
|
|
2209
|
+
];
|
|
2210
|
+
}
|
|
2211
|
+
function userTextMessage(text) {
|
|
2212
|
+
return { role: "user", content: text };
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Build the optimistic user message echoed to clients. When images are present
|
|
2216
|
+
* we mirror pi's content-array shape (`[{type:"text"}, {type:"image"}, ...]`) so
|
|
2217
|
+
* the local echo matches what pi persists in the session branch.
|
|
2218
|
+
*/
|
|
2219
|
+
function userMessage(text, images) {
|
|
2220
|
+
if (images.length === 0)
|
|
2221
|
+
return userTextMessage(text);
|
|
2222
|
+
const content = [];
|
|
2223
|
+
if (text !== "")
|
|
2224
|
+
content.push({ type: "text", text });
|
|
2225
|
+
content.push(...images);
|
|
2226
|
+
return { role: "user", content };
|
|
2227
|
+
}
|
|
2228
|
+
function buildPromptOptions(behavior, images) {
|
|
2229
|
+
const options = {};
|
|
2230
|
+
if (behavior !== undefined)
|
|
2231
|
+
options.streamingBehavior = behavior;
|
|
2232
|
+
if (images.length > 0)
|
|
2233
|
+
options.images = images;
|
|
2234
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
2235
|
+
}
|
|
2236
|
+
function stringValue(value) {
|
|
2237
|
+
return typeof value === "string" ? value : "";
|
|
2238
|
+
}
|
|
2239
|
+
function historyMessages(session) {
|
|
2240
|
+
const messages = [];
|
|
2241
|
+
for (const entry of session.sessionManager.getBranch()) {
|
|
2242
|
+
if (!isRecord(entry))
|
|
2243
|
+
continue;
|
|
2244
|
+
if (entry["type"] === "message")
|
|
2245
|
+
messages.push(entry["message"]);
|
|
2246
|
+
else if (entry["type"] === "custom_message" && entry["display"] === true)
|
|
2247
|
+
messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
|
|
2248
|
+
else if (entry["type"] === "compaction")
|
|
2249
|
+
messages.push({ role: "system", source: "compaction", content: `Compacted history:\n\n${stringValue(entry["summary"])}` });
|
|
2250
|
+
else if (entry["type"] === "branch_summary")
|
|
2251
|
+
messages.push({ role: "system", source: "branch_summary", content: `Branch summary:\n\n${stringValue(entry["summary"])}` });
|
|
2252
|
+
}
|
|
2253
|
+
return messages;
|
|
2254
|
+
}
|
|
2255
|
+
/** custom entry type used to persist parent -> child subsession links outside LLM context. */
|
|
2256
|
+
const SUBSESSION_LINK_CUSTOM_TYPE = "omp-web.subsession.link";
|
|
2257
|
+
/** custom entry type used to mark a child as created by spawn_subsession. */
|
|
2258
|
+
const SUBSESSION_CHILD_LINK_CUSTOM_TYPE = "omp-web.subsession.spawned";
|
|
2259
|
+
/** customType marking a parent-facing subsession-completion notice. */
|
|
2260
|
+
const SUBSESSION_NOTIFICATION_CUSTOM_TYPE = "subsession.completion";
|
|
2261
|
+
const SUBSESSION_NOTIFICATION_PREVIEW_CHARS = 2000;
|
|
2262
|
+
function truncateForNotification(text) {
|
|
2263
|
+
if (text.length <= SUBSESSION_NOTIFICATION_PREVIEW_CHARS)
|
|
2264
|
+
return text;
|
|
2265
|
+
return `${text.slice(0, SUBSESSION_NOTIFICATION_PREVIEW_CHARS)}…`;
|
|
2266
|
+
}
|
|
2267
|
+
/** Most recent assistant text from a history message list, or "" if none. */
|
|
2268
|
+
function finalAssistantText(messages) {
|
|
2269
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2270
|
+
const message = messages[i];
|
|
2271
|
+
if (!isRecord(message) || message["role"] !== "assistant")
|
|
2272
|
+
continue;
|
|
2273
|
+
const content = message["content"];
|
|
2274
|
+
if (typeof content === "string")
|
|
2275
|
+
return content;
|
|
2276
|
+
if (!Array.isArray(content))
|
|
2277
|
+
continue;
|
|
2278
|
+
const texts = [];
|
|
2279
|
+
for (const part of content) {
|
|
2280
|
+
if (isRecord(part) && part["type"] === "text" && typeof part["text"] === "string")
|
|
2281
|
+
texts.push(part["text"]);
|
|
2282
|
+
}
|
|
2283
|
+
if (texts.length > 0)
|
|
2284
|
+
return texts.join("\n").trim();
|
|
2285
|
+
}
|
|
2286
|
+
return "";
|
|
2287
|
+
}
|
|
2288
|
+
function toClientEvent(event) {
|
|
2289
|
+
const eventType = getString(event, "type");
|
|
2290
|
+
const assistantMessageEvent = getProperty(event, "assistantMessageEvent");
|
|
2291
|
+
if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "text_delta") {
|
|
2292
|
+
return { type: "assistant.delta", text: getString(assistantMessageEvent, "delta") ?? "" };
|
|
2293
|
+
}
|
|
2294
|
+
if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "thinking_delta") {
|
|
2295
|
+
return { type: "assistant.thinking.delta", text: getString(assistantMessageEvent, "delta") ?? "" };
|
|
2296
|
+
}
|
|
2297
|
+
if (eventType === "tool_execution_start") {
|
|
2298
|
+
const args = getProperty(event, "args");
|
|
2299
|
+
return { type: "tool.start", toolName: getString(event, "toolName") ?? "", toolCallId: getString(event, "toolCallId") ?? "", summary: summarizeToolArgs(args), args };
|
|
2300
|
+
}
|
|
2301
|
+
if (eventType === "tool_execution_update") {
|
|
2302
|
+
const partialResult = getProperty(event, "partialResult");
|
|
2303
|
+
return { type: "tool.update", toolName: getString(event, "toolName") ?? "", toolCallId: getString(event, "toolCallId") ?? "", text: stringifyToolResult(partialResult), content: toolResultContent(partialResult), details: toolResultDetails(partialResult) };
|
|
2304
|
+
}
|
|
2305
|
+
if (eventType === "tool_execution_end") {
|
|
2306
|
+
const result = getProperty(event, "result");
|
|
2307
|
+
return { type: "tool.end", toolName: getString(event, "toolName") ?? "", toolCallId: getString(event, "toolCallId") ?? "", text: stringifyToolResult(result), content: toolResultContent(result), details: toolResultDetails(result), isError: getBoolean(event, "isError") === true };
|
|
2308
|
+
}
|
|
2309
|
+
if (eventType === "agent_start")
|
|
2310
|
+
return { type: "agent.start" };
|
|
2311
|
+
if (eventType === "agent_end")
|
|
2312
|
+
return { type: "agent.end" };
|
|
2313
|
+
if (eventType === "message_end") {
|
|
2314
|
+
const message = getProperty(event, "message");
|
|
2315
|
+
return message === undefined ? { type: "message.end" } : { type: "message.end", message };
|
|
2316
|
+
}
|
|
2317
|
+
return { type: "pi.event", eventType: eventType ?? "unknown" };
|
|
2318
|
+
}
|
|
2319
|
+
function summarizeToolArgs(args) {
|
|
2320
|
+
if (!isRecord(args))
|
|
2321
|
+
return stringifyPrimitive(args);
|
|
2322
|
+
const command = getString(args, "command");
|
|
2323
|
+
if (command !== undefined)
|
|
2324
|
+
return command;
|
|
2325
|
+
const path = getString(args, "path");
|
|
2326
|
+
if (path !== undefined)
|
|
2327
|
+
return path;
|
|
2328
|
+
if (typeof args["oldText"] === "string" && typeof args["newText"] === "string")
|
|
2329
|
+
return "edit text replacement";
|
|
2330
|
+
const edits = args["edits"];
|
|
2331
|
+
if (Array.isArray(edits))
|
|
2332
|
+
return `${String(edits.length)} edit${edits.length === 1 ? "" : "s"}`;
|
|
2333
|
+
const entries = Object.entries(args).filter(([, value]) => value != null).slice(0, 3);
|
|
2334
|
+
return entries.map(([key, value]) => `${key}: ${shortToolValue(value)}`).join(" · ");
|
|
2335
|
+
}
|
|
2336
|
+
function shortToolValue(value) {
|
|
2337
|
+
if (typeof value === "string")
|
|
2338
|
+
return value.length > 80 ? `${value.slice(0, 77)}…` : value;
|
|
2339
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
2340
|
+
return String(value);
|
|
2341
|
+
if (Array.isArray(value))
|
|
2342
|
+
return `${String(value.length)} item${value.length === 1 ? "" : "s"}`;
|
|
2343
|
+
if (typeof value === "object" && value !== null)
|
|
2344
|
+
return "object";
|
|
2345
|
+
return "";
|
|
2346
|
+
}
|
|
2347
|
+
function toolResultContent(result) {
|
|
2348
|
+
if (isRecord(result)) {
|
|
2349
|
+
const content = getProperty(result, "content");
|
|
2350
|
+
if (content !== undefined)
|
|
2351
|
+
return content;
|
|
2352
|
+
const text = getString(result, "text") ?? getString(result, "output");
|
|
2353
|
+
if (text !== undefined)
|
|
2354
|
+
return [{ type: "text", text }];
|
|
2355
|
+
}
|
|
2356
|
+
if (typeof result === "string")
|
|
2357
|
+
return [{ type: "text", text: result }];
|
|
2358
|
+
return result;
|
|
2359
|
+
}
|
|
2360
|
+
function toolResultDetails(result) {
|
|
2361
|
+
return isRecord(result) ? getProperty(result, "details") : undefined;
|
|
2362
|
+
}
|
|
2363
|
+
function stringifyToolResult(result) {
|
|
2364
|
+
if (typeof result === "string")
|
|
2365
|
+
return result;
|
|
2366
|
+
if (Array.isArray(result))
|
|
2367
|
+
return result.map(stringifyToolResult).filter((text) => text !== "").join("\n");
|
|
2368
|
+
if (isRecord(result)) {
|
|
2369
|
+
if (getString(result, "type") === "image")
|
|
2370
|
+
return "[image]";
|
|
2371
|
+
const text = getString(result, "text") ?? getString(result, "content") ?? getString(result, "output");
|
|
2372
|
+
if (text !== undefined)
|
|
2373
|
+
return text;
|
|
2374
|
+
const content = getProperty(result, "content");
|
|
2375
|
+
if (Array.isArray(content))
|
|
2376
|
+
return stringifyToolResult(content);
|
|
2377
|
+
return JSON.stringify(result, null, 2);
|
|
2378
|
+
}
|
|
2379
|
+
return stringifyPrimitive(result);
|
|
2380
|
+
}
|
|
2381
|
+
function getProperty(value, key) {
|
|
2382
|
+
return isRecord(value) ? value[key] : undefined;
|
|
2383
|
+
}
|
|
2384
|
+
function getString(value, key) {
|
|
2385
|
+
const property = getProperty(value, key);
|
|
2386
|
+
return typeof property === "string" ? property : undefined;
|
|
2387
|
+
}
|
|
2388
|
+
function getBoolean(value, key) {
|
|
2389
|
+
const property = getProperty(value, key);
|
|
2390
|
+
return typeof property === "boolean" ? property : undefined;
|
|
2391
|
+
}
|
|
2392
|
+
function stringifyPrimitive(value) {
|
|
2393
|
+
if (value == null)
|
|
2394
|
+
return "";
|
|
2395
|
+
if (typeof value === "string")
|
|
2396
|
+
return value;
|
|
2397
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
|
|
2398
|
+
return String(value);
|
|
2399
|
+
return "";
|
|
2400
|
+
}
|
|
2401
|
+
//# sourceMappingURL=piSessionService.js.map
|