@jmfederico/pi-web 1.202606.7 → 1.202607.1
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/README.md +14 -5
- package/dist/cli.js +291 -320
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/CodeViewer-uASb28Y1.js +4 -0
- package/dist/client/assets/TerminalPanel-DfTmykFw.js +187 -0
- package/dist/client/assets/UnifiedDiffViewer-laQN1x7P.js +39 -0
- package/dist/client/assets/index-Dhapi0oR.js +3093 -0
- package/dist/client/assets/vendor-editor-core-vUi74DnD.js +12 -0
- package/dist/client/assets/vendor-editor-languages-DoaXUodB.js +46 -0
- package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +1 -0
- package/dist/client/assets/vendor-terminal-BrP-ENHg.css +1 -0
- package/dist/client/assets/vendor-terminal-D8k4UKM2.js +35 -0
- package/dist/client/index.html +9 -6
- package/dist/client/manifest.webmanifest +4 -4
- package/dist/config.js +144 -2
- package/dist/config.js.map +1 -1
- package/dist/docker/piWebDockerCommandPlan.js +211 -0
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -0
- package/dist/nativeServices/serviceDoctor.js +480 -0
- package/dist/nativeServices/serviceDoctor.js.map +1 -0
- package/dist/nativeServices/serviceInstall.js +35 -0
- package/dist/nativeServices/serviceInstall.js.map +1 -0
- package/dist/nativeServices/servicePlan.js +358 -0
- package/dist/nativeServices/servicePlan.js.map +1 -0
- package/dist/nativeServices/serviceProbe.js +447 -0
- package/dist/nativeServices/serviceProbe.js.map +1 -0
- package/dist/nativeServices/serviceRendering.js +122 -0
- package/dist/nativeServices/serviceRendering.js.map +1 -0
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +27 -5
- package/dist/pi-web-plugins/updates/updatesLogic.js +37 -5
- package/dist/piWebVersionReport.js +4 -0
- package/dist/piWebVersionReport.js.map +1 -1
- package/dist/plugin-api.d.ts +3 -1
- package/dist/server/activeAgentProfileProvider.js +25 -0
- package/dist/server/activeAgentProfileProvider.js.map +1 -0
- package/dist/server/app.js +69 -13
- package/dist/server/app.js.map +1 -1
- package/dist/server/browserMessageProjection.js +56 -0
- package/dist/server/browserMessageProjection.js.map +1 -0
- package/dist/server/configRoutes.js +147 -6
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/machines/machineClient.js +19 -9
- package/dist/server/machines/machineClient.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +7 -4
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +76 -11
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineRoutes.js +1 -1
- package/dist/server/machines/machineRoutes.js.map +1 -1
- package/dist/server/machines/machineService.js +6 -28
- package/dist/server/machines/machineService.js.map +1 -1
- package/dist/server/piPackageRoutes.js +91 -0
- package/dist/server/piPackageRoutes.js.map +1 -0
- package/dist/server/piPackageService.js +101 -0
- package/dist/server/piPackageService.js.map +1 -0
- package/dist/server/piWebPluginService.js +91 -20
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebReleaseLookupCache.js +37 -0
- package/dist/server/piWebReleaseLookupCache.js.map +1 -0
- package/dist/server/piWebStatus.js +90 -37
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/piWebStatusCache.js +19 -6
- package/dist/server/piWebStatusCache.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +34 -7
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessiond/sessionDaemonStartup.js +49 -0
- package/dist/server/sessiond/sessionDaemonStartup.js.map +1 -0
- package/dist/server/sessiond.js +88 -62
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/authProviderOptions.js +30 -32
- package/dist/server/sessions/authProviderOptions.js.map +1 -1
- package/dist/server/sessions/authRoutes.js +14 -4
- package/dist/server/sessions/authRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +89 -29
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/builtinCommands.js +1 -1
- package/dist/server/sessions/builtinCommands.js.map +1 -1
- package/dist/server/sessions/oauthLoginFlowService.js +176 -53
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +22 -21
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +715 -127
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionArchiveMigration.js +590 -0
- package/dist/server/sessions/sessionArchiveMigration.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +58 -26
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +16 -0
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionNameGenerator.js +27 -44
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +65 -3
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionService.js +2 -0
- package/dist/server/sessions/sessionService.js.map +1 -0
- package/dist/server/sessions/spawnSessionTool.js +9 -4
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +79 -32
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/terminals/terminalService.js +10 -4
- package/dist/server/terminals/terminalService.js.map +1 -1
- package/dist/server/workspaces/fileSuggestions.js +7 -3
- package/dist/server/workspaces/fileSuggestions.js.map +1 -1
- package/dist/sessiond/activeAgentProfile.js +31 -0
- package/dist/sessiond/activeAgentProfile.js.map +1 -0
- package/dist/sessiond/sessionDaemonClient.js +38 -0
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/activeAgentProfile.js +76 -0
- package/dist/shared/activeAgentProfile.js.map +1 -0
- package/dist/shared/activity.js +0 -15
- package/dist/shared/activity.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +160 -3
- package/dist/shared/apiTypes.js +6 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +33 -2
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +14 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/piWebStatusParsing.js +26 -8
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/dist/shared/thinkingLevels.d.ts +1 -1
- package/dist/shared/thinkingLevels.js +1 -1
- package/dist/shared/thinkingLevels.js.map +1 -1
- package/docs/config.md +75 -19
- package/docs/plugins.md +46 -18
- package/package.json +36 -32
- package/dist/client/assets/CodeViewer-CQU9mJ74.js +0 -4
- package/dist/client/assets/TerminalPanel-DtfVBsqI.js +0 -126
- package/dist/client/assets/UnifiedDiffViewer-BZQQK99S.js +0 -34
- package/dist/client/assets/index-CbE7df66.js +0 -2742
- package/dist/client/assets/vendor-editor-core-C7aWiQy0.js +0 -12
- package/dist/client/assets/vendor-editor-languages-EW34FfVy.js +0 -26
- package/dist/client/assets/vendor-editor-legacy-B4QLsWF8.js +0 -1
- package/dist/client/assets/vendor-terminal-DDGTF8rc.css +0 -1
- package/dist/client/assets/vendor-terminal-DjQ08hXu.js +0 -16
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { open, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
4
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
3
6
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
4
7
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
5
8
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
6
9
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
7
10
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
8
|
-
import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
11
|
+
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
9
12
|
import { computeEditPreview } from "./editPreview.js";
|
|
10
|
-
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
|
11
13
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
12
14
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
13
|
-
import { cwdPathsEqual } from "../workingDirectory.js";
|
|
15
|
+
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
14
16
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
15
17
|
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
16
18
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
@@ -48,23 +50,154 @@ function parsePromptStreamingBehavior(value) {
|
|
|
48
50
|
return value;
|
|
49
51
|
throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
|
|
50
52
|
}
|
|
53
|
+
function resourceDiagnosticToWarning(diagnostic, source) {
|
|
54
|
+
return {
|
|
55
|
+
severity: diagnostic.type === "error" ? "error" : "warning",
|
|
56
|
+
message: diagnostic.message,
|
|
57
|
+
source,
|
|
58
|
+
...(diagnostic.path === undefined ? {} : { path: diagnostic.path }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function runtimeDiagnosticToWarning(diagnostic) {
|
|
62
|
+
return { severity: diagnostic.type, message: diagnostic.message, source: "runtime" };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Compute the live warnings for a runtime by re-reading its current resource
|
|
66
|
+
* loader diagnostics, extension load errors, and runtime setup diagnostics.
|
|
67
|
+
*
|
|
68
|
+
* This mimics the TUI recomputing warnings on every (re)bind: it reads the
|
|
69
|
+
* runtime's current state rather than a cached snapshot, so a rebuilt runtime
|
|
70
|
+
* yields fresh warnings. Runtimes without SDK services (e.g. test fakes)
|
|
71
|
+
* contribute no warnings.
|
|
72
|
+
*/
|
|
73
|
+
export function collectRuntimeWarnings(runtime) {
|
|
74
|
+
const warnings = [];
|
|
75
|
+
for (const diagnostic of runtime.diagnostics ?? [])
|
|
76
|
+
warnings.push(runtimeDiagnosticToWarning(diagnostic));
|
|
77
|
+
const resourceLoader = runtime.services?.resourceLoader;
|
|
78
|
+
if (resourceLoader !== undefined) {
|
|
79
|
+
for (const diagnostic of resourceLoader.getSkills().diagnostics)
|
|
80
|
+
warnings.push(resourceDiagnosticToWarning(diagnostic, "skill"));
|
|
81
|
+
for (const diagnostic of resourceLoader.getPrompts().diagnostics)
|
|
82
|
+
warnings.push(resourceDiagnosticToWarning(diagnostic, "prompt"));
|
|
83
|
+
for (const diagnostic of resourceLoader.getThemes().diagnostics)
|
|
84
|
+
warnings.push(resourceDiagnosticToWarning(diagnostic, "theme"));
|
|
85
|
+
for (const error of resourceLoader.getExtensions().errors) {
|
|
86
|
+
warnings.push({ severity: "error", message: `${error.path}: ${error.error}`, source: "extension", path: error.path });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return warnings;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Verbatim TUI wording for the Anthropic subscription-auth billing notice. Kept
|
|
93
|
+
* character-for-character in sync with `ANTHROPIC_SUBSCRIPTION_AUTH_WARNING` in
|
|
94
|
+
* the SDK's interactive mode so the browser shows the same message the TUI does.
|
|
95
|
+
*/
|
|
96
|
+
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
|
|
97
|
+
/** Mirror of the SDK TUI `isAnthropicSubscriptionAuthKey` (subscription API keys start with `sk-ant-oat`). */
|
|
98
|
+
function isAnthropicSubscriptionAuthKey(apiKey) {
|
|
99
|
+
return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Dismiss id for the Anthropic subscription-auth billing notice. This is `pi`'s
|
|
103
|
+
* own `WarningSettings` key verbatim (`anthropicExtraUsage`): we carry the
|
|
104
|
+
* coupling `pi` already defines rather than inventing a parallel vocabulary, and
|
|
105
|
+
* {@link dismissSessionWarning} maps it back to `setWarnings`.
|
|
106
|
+
*/
|
|
107
|
+
const ANTHROPIC_EXTRA_USAGE_DISMISS_ID = "anthropicExtraUsage";
|
|
108
|
+
/**
|
|
109
|
+
* Port of the TUI `maybeWarnAboutAnthropicSubscriptionAuth` gate/trigger, computed
|
|
110
|
+
* live from the session's current model, stored Anthropic credential, and warning
|
|
111
|
+
* settings. Returns the billing warning when the active provider is `anthropic`
|
|
112
|
+
* and auth is a subscription credential (stored `oauth`, or an `sk-ant-oat` API
|
|
113
|
+
* key), unless suppressed via `getWarnings().anthropicExtraUsage === false`.
|
|
114
|
+
*
|
|
115
|
+
* The stored credential is read synchronously (matching the TUI's `oauth` branch
|
|
116
|
+
* and the documented `sk-ant-oat` key trigger) so warnings stay part of the
|
|
117
|
+
* synchronous live status computation.
|
|
118
|
+
*/
|
|
119
|
+
export function anthropicSubscriptionWarning(session, authPath) {
|
|
120
|
+
if (session.settingsManager.getWarnings().anthropicExtraUsage === false)
|
|
121
|
+
return undefined;
|
|
122
|
+
if (session.model?.provider !== "anthropic")
|
|
123
|
+
return undefined;
|
|
124
|
+
const credential = readStoredCredential("anthropic", authPath);
|
|
125
|
+
if (credential === undefined)
|
|
126
|
+
return undefined;
|
|
127
|
+
const isSubscriptionAuth = credential.type === "oauth"
|
|
128
|
+
? true
|
|
129
|
+
: isAnthropicSubscriptionAuthKey(credential.key);
|
|
130
|
+
if (!isSubscriptionAuth)
|
|
131
|
+
return undefined;
|
|
132
|
+
return {
|
|
133
|
+
severity: "warning",
|
|
134
|
+
message: ANTHROPIC_SUBSCRIPTION_AUTH_WARNING,
|
|
135
|
+
source: "anthropic",
|
|
136
|
+
dismiss: { id: ANTHROPIC_EXTRA_USAGE_DISMISS_ID },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Durably suppress a dismissable session warning by mapping its opaque dismiss
|
|
141
|
+
* id back to the concrete `pi` suppression it represents. Only known ids are
|
|
142
|
+
* honored; unknown ids throw so a stale/forged client cannot silently no-op.
|
|
143
|
+
*
|
|
144
|
+
* This is the single place provider-specific suppression lives: the wire type,
|
|
145
|
+
* parser, and UI stay agnostic. Adding a future dismissable warning is a
|
|
146
|
+
* server-only change here plus a `dismiss` id on its producer.
|
|
147
|
+
*/
|
|
148
|
+
export function dismissSessionWarning(session, dismissId) {
|
|
149
|
+
if (dismissId !== ANTHROPIC_EXTRA_USAGE_DISMISS_ID) {
|
|
150
|
+
throw new Error(`Unknown session warning dismiss id: ${dismissId}`);
|
|
151
|
+
}
|
|
152
|
+
session.settingsManager.setWarnings({ ...session.settingsManager.getWarnings(), anthropicExtraUsage: false });
|
|
153
|
+
}
|
|
51
154
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
52
155
|
if (!(options.sessionManager instanceof SessionManager))
|
|
53
156
|
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
157
|
+
const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.delegationToolsEnabled);
|
|
158
|
+
return createAgentSessionRuntime(runtimeFactory, {
|
|
159
|
+
cwd: options.cwd,
|
|
160
|
+
agentDir: options.agentDir,
|
|
161
|
+
sessionManager: options.sessionManager,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, delegationToolsEnabled) {
|
|
165
|
+
// These inputs belong only to the session being opened. A later runtime
|
|
166
|
+
// replacement resolves its own model and delegation capability.
|
|
167
|
+
let pendingInitialModel = initialModel;
|
|
168
|
+
let pendingDelegationToolsEnabled = delegationToolsEnabled;
|
|
169
|
+
return async (options) => {
|
|
170
|
+
const model = pendingInitialModel;
|
|
171
|
+
const toolsEnabled = pendingDelegationToolsEnabled;
|
|
172
|
+
pendingInitialModel = undefined;
|
|
173
|
+
pendingDelegationToolsEnabled = undefined;
|
|
174
|
+
return createRuntime({
|
|
175
|
+
...options,
|
|
176
|
+
...(model === undefined ? {} : { initialModel: model }),
|
|
177
|
+
...(toolsEnabled === undefined ? {} : { delegationToolsEnabled: toolsEnabled }),
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions) {
|
|
182
|
+
return [
|
|
183
|
+
createPiWebEditToolDefinition(cwd),
|
|
184
|
+
...(delegationEnabled && spawn !== undefined ? [createSpawnSessionToolDefinition(cwd, { spawn })] : []),
|
|
185
|
+
...(delegationEnabled && subsessions !== undefined ? createSubsessionToolDefinitions(cwd, subsessions) : []),
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions) {
|
|
189
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => {
|
|
190
|
+
const services = await createAgentSessionServices({ cwd, agentDir, modelRuntime });
|
|
191
|
+
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
192
|
+
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
193
|
+
const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions);
|
|
194
|
+
const result = await createAgentSessionFromServices({
|
|
195
|
+
services,
|
|
196
|
+
sessionManager,
|
|
197
|
+
customTools,
|
|
198
|
+
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
199
|
+
...(initialModel === undefined ? {} : { model: initialModel }),
|
|
200
|
+
});
|
|
68
201
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
69
202
|
};
|
|
70
203
|
}
|
|
@@ -90,9 +223,10 @@ function createPiWebEditToolDefinition(cwd) {
|
|
|
90
223
|
});
|
|
91
224
|
}
|
|
92
225
|
export class PiSessionService {
|
|
93
|
-
constructor(events, deps
|
|
226
|
+
constructor(events, deps) {
|
|
94
227
|
this.events = events;
|
|
95
228
|
this.active = new Map();
|
|
229
|
+
this.pendingSessionOpens = new Map();
|
|
96
230
|
this.activities = new Map();
|
|
97
231
|
this.compactionPromptQueues = new Map();
|
|
98
232
|
this.compactionDrainTimers = new Map();
|
|
@@ -112,16 +246,16 @@ export class PiSessionService {
|
|
|
112
246
|
*/
|
|
113
247
|
this.subsessionNotifyArmed = new Map();
|
|
114
248
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
115
|
-
this.agentDir = deps.agentDir
|
|
116
|
-
this.sessionManager = deps.sessionManager
|
|
117
|
-
this.
|
|
249
|
+
this.agentDir = deps.agentDir;
|
|
250
|
+
this.sessionManager = deps.sessionManager;
|
|
251
|
+
this.modelRuntime = deps.modelRuntime;
|
|
118
252
|
this.spawnTargets = deps.spawnTargets;
|
|
119
253
|
this.logger = deps.logger ?? noopLogger;
|
|
120
254
|
this.now = deps.now ?? (() => new Date());
|
|
121
255
|
// Subsessions are a beta capability gated behind their own flag, and they
|
|
122
256
|
// also require the spawn capability (they share its project-scope resolver).
|
|
123
257
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
124
|
-
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.
|
|
258
|
+
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
125
259
|
spawn: (input) => this.spawnSubsession(input),
|
|
126
260
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
127
261
|
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
@@ -139,6 +273,7 @@ export class PiSessionService {
|
|
|
139
273
|
this.publishActivity(session, result === "success" ? "compaction complete" : "compaction failed", result === "success" ? "idle" : "error", detail);
|
|
140
274
|
this.publishStatus(session);
|
|
141
275
|
},
|
|
276
|
+
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
142
277
|
}, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
|
|
143
278
|
}
|
|
144
279
|
activeCount() {
|
|
@@ -149,10 +284,12 @@ export class PiSessionService {
|
|
|
149
284
|
}
|
|
150
285
|
async cleanup(request) {
|
|
151
286
|
const plan = await this.cleanupPlan(request);
|
|
152
|
-
if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined)
|
|
287
|
+
if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
153
288
|
throw new Error("Archive store does not support deletion");
|
|
154
289
|
const archiveInputs = [];
|
|
290
|
+
const readyArchiveInputs = [];
|
|
155
291
|
const deleteRecords = [];
|
|
292
|
+
const readyDeleteRecords = [];
|
|
156
293
|
const skippedBusySessionIds = new Set(plan.skippedBusySessionIds);
|
|
157
294
|
for (const input of plan.archiveInputs) {
|
|
158
295
|
if (this.activeSessionHasWork(input.sessionId)) {
|
|
@@ -160,20 +297,21 @@ export class PiSessionService {
|
|
|
160
297
|
continue;
|
|
161
298
|
}
|
|
162
299
|
await this.closeActive(input.sessionId);
|
|
163
|
-
|
|
164
|
-
archiveInputs.push(input);
|
|
300
|
+
readyArchiveInputs.push(input);
|
|
165
301
|
}
|
|
302
|
+
await this.archiveStoreArchiveMany(readyArchiveInputs);
|
|
303
|
+
archiveInputs.push(...readyArchiveInputs);
|
|
166
304
|
for (const record of plan.deleteRecords) {
|
|
167
305
|
if (this.activeSessionHasWork(record.sessionId)) {
|
|
168
306
|
skippedBusySessionIds.add(record.sessionId);
|
|
169
307
|
continue;
|
|
170
308
|
}
|
|
171
309
|
await this.closeActive(record.sessionId);
|
|
172
|
-
|
|
173
|
-
await this.ensureArchivedRecordMoved(record);
|
|
174
|
-
await this.archiveStore.deleteArchived?.(record.sessionId);
|
|
175
|
-
deleteRecords.push(record);
|
|
310
|
+
readyDeleteRecords.push(record);
|
|
176
311
|
}
|
|
312
|
+
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
313
|
+
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
314
|
+
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
177
315
|
return summarizeSessionCleanupExecution({
|
|
178
316
|
archiveInputs,
|
|
179
317
|
deleteRecords,
|
|
@@ -185,8 +323,12 @@ export class PiSessionService {
|
|
|
185
323
|
async dispose() {
|
|
186
324
|
clearInterval(this.heartbeat);
|
|
187
325
|
this.clearCompactionDrainTimers();
|
|
326
|
+
const pendingOpens = this.pendingSessionOpenPromises();
|
|
327
|
+
if (pendingOpens.length > 0)
|
|
328
|
+
await Promise.allSettled(pendingOpens);
|
|
188
329
|
const activeSessions = Array.from(new Set(this.active.values()));
|
|
189
330
|
this.active.clear();
|
|
331
|
+
this.pendingSessionOpens.clear();
|
|
190
332
|
this.activities.clear();
|
|
191
333
|
this.compactionPromptQueues.clear();
|
|
192
334
|
this.authLossWarnings.clear();
|
|
@@ -198,8 +340,12 @@ export class PiSessionService {
|
|
|
198
340
|
await Promise.all(activeSessions.map(async (active) => {
|
|
199
341
|
active.unsubscribe();
|
|
200
342
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
|
|
201
|
-
|
|
202
|
-
|
|
343
|
+
try {
|
|
344
|
+
await active.runtime.session.abort();
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
await active.runtime.dispose();
|
|
348
|
+
}
|
|
203
349
|
}));
|
|
204
350
|
}
|
|
205
351
|
async list(cwd) {
|
|
@@ -217,20 +363,27 @@ export class PiSessionService {
|
|
|
217
363
|
.filter(isDefined);
|
|
218
364
|
return [...unarchivedSessions, ...archivedSessions];
|
|
219
365
|
}
|
|
220
|
-
async start(cwd,
|
|
221
|
-
|
|
366
|
+
async start(cwd, options = {}) {
|
|
367
|
+
return this.startSession(cwd, options);
|
|
368
|
+
}
|
|
369
|
+
async startSession(cwd, options) {
|
|
370
|
+
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
|
|
371
|
+
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
372
|
+
...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
|
|
373
|
+
});
|
|
222
374
|
const { session } = active.runtime;
|
|
223
375
|
const created = {
|
|
224
376
|
id: session.sessionId,
|
|
225
377
|
path: session.sessionFile ?? "",
|
|
226
378
|
cwd,
|
|
379
|
+
persisted: sessionFileExists(session.sessionFile),
|
|
227
380
|
created: new Date().toISOString(),
|
|
228
381
|
modified: new Date().toISOString(),
|
|
229
382
|
messageCount: session.messages.length,
|
|
230
383
|
firstMessage: "",
|
|
231
384
|
// Include the parent so listeners can nest the new session in the tree
|
|
232
385
|
// immediately, instead of showing it flat until the next reload.
|
|
233
|
-
...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
|
|
386
|
+
...(options.parentSession === undefined ? {} : { parentSessionPath: options.parentSession }),
|
|
234
387
|
};
|
|
235
388
|
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
236
389
|
// session to their list without a manual reload.
|
|
@@ -248,7 +401,7 @@ export class PiSessionService {
|
|
|
248
401
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
249
402
|
if (!decision.allowed)
|
|
250
403
|
throw spawnTargetError(decision);
|
|
251
|
-
const created = await this.start(decision.cwd);
|
|
404
|
+
const created = await this.start(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
|
|
252
405
|
await this.prompt(created.id, input.prompt);
|
|
253
406
|
this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
|
|
254
407
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
@@ -265,7 +418,11 @@ export class PiSessionService {
|
|
|
265
418
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
266
419
|
if (!decision.allowed)
|
|
267
420
|
throw spawnTargetError(decision);
|
|
268
|
-
const created = await this.
|
|
421
|
+
const created = await this.startSession(decision.cwd, {
|
|
422
|
+
...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
|
|
423
|
+
...(input.model === undefined ? {} : { initialModel: input.model }),
|
|
424
|
+
creationProvenance: "tracked-subsession",
|
|
425
|
+
});
|
|
269
426
|
const parentSessionFile = nonEmptyString(input.parentSessionFile);
|
|
270
427
|
const link = {
|
|
271
428
|
parentSessionId: input.parentSessionId,
|
|
@@ -465,61 +622,13 @@ export class PiSessionService {
|
|
|
465
622
|
return;
|
|
466
623
|
this.registerVerifiedSubsession(link);
|
|
467
624
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (parsed?.spawnedSessionId === session.sessionId)
|
|
476
|
-
marker = parsed;
|
|
477
|
-
}
|
|
478
|
-
if (marker === undefined)
|
|
479
|
-
return undefined;
|
|
480
|
-
const childSessionFile = nonEmptyString(session.sessionFile);
|
|
481
|
-
if (childSessionFile === undefined)
|
|
482
|
-
return undefined;
|
|
483
|
-
const childHeader = await readSessionHeaderSummary(childSessionFile);
|
|
484
|
-
if (childHeader?.id !== session.sessionId)
|
|
485
|
-
return undefined;
|
|
486
|
-
const parentSessionFile = nonEmptyString(childHeader.parentSession);
|
|
487
|
-
if (parentSessionFile === undefined)
|
|
488
|
-
return undefined;
|
|
489
|
-
const parentHeader = await readSessionHeaderSummary(parentSessionFile);
|
|
490
|
-
if (parentHeader?.id !== marker.spawnedBySessionId)
|
|
491
|
-
return undefined;
|
|
492
|
-
const parentLink = this.findReciprocalParentSubsessionLink(parentSessionFile, marker.spawnedBySessionId, session.sessionId, childSessionFile);
|
|
493
|
-
if (parentLink === undefined)
|
|
494
|
-
return undefined;
|
|
495
|
-
return {
|
|
496
|
-
parentSessionId: marker.spawnedBySessionId,
|
|
497
|
-
childSessionId: session.sessionId,
|
|
498
|
-
childSessionFile,
|
|
499
|
-
parentSessionFile,
|
|
500
|
-
cwd: parentLink.cwd ?? session.sessionManager.getCwd(),
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
findReciprocalParentSubsessionLink(parentSessionFile, parentSessionId, childSessionId, childSessionFile) {
|
|
504
|
-
let parentManager;
|
|
505
|
-
try {
|
|
506
|
-
parentManager = this.sessionManager.open(parentSessionFile);
|
|
507
|
-
}
|
|
508
|
-
catch {
|
|
509
|
-
return undefined;
|
|
510
|
-
}
|
|
511
|
-
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
512
|
-
for (const entry of entries) {
|
|
513
|
-
const link = parsePersistedParentSubsessionLink(entry);
|
|
514
|
-
if (link === undefined)
|
|
515
|
-
continue;
|
|
516
|
-
if (link.spawnedBySessionId !== parentSessionId || link.spawnedSessionId !== childSessionId)
|
|
517
|
-
continue;
|
|
518
|
-
if (link.spawnedSessionFile === undefined || !sessionPathsEqual(link.spawnedSessionFile, childSessionFile))
|
|
519
|
-
continue;
|
|
520
|
-
return link;
|
|
521
|
-
}
|
|
522
|
-
return undefined;
|
|
625
|
+
verifiedSubsessionLinkFromOpenedChild(session) {
|
|
626
|
+
return verifiedTrackedSubsessionLink(this.sessionManager, {
|
|
627
|
+
sessionId: session.sessionId,
|
|
628
|
+
sessionFile: session.sessionFile,
|
|
629
|
+
sessionManager: session.sessionManager,
|
|
630
|
+
cwd: session.sessionManager.getCwd(),
|
|
631
|
+
});
|
|
523
632
|
}
|
|
524
633
|
async getOrOpenTrackedSubsession(sessionId) {
|
|
525
634
|
const link = this.subsessionLinks.get(sessionId);
|
|
@@ -556,6 +665,16 @@ export class PiSessionService {
|
|
|
556
665
|
return "error";
|
|
557
666
|
return "idle";
|
|
558
667
|
}
|
|
668
|
+
workingSubsessionIds(parentSessionId) {
|
|
669
|
+
const childIds = this.subsessionChildren.get(parentSessionId);
|
|
670
|
+
if (childIds === undefined)
|
|
671
|
+
return [];
|
|
672
|
+
return [...childIds].filter((childId) => {
|
|
673
|
+
const link = this.subsessionLinks.get(childId);
|
|
674
|
+
const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
|
|
675
|
+
return active !== undefined && this.hasActiveWork(active.runtime.session);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
559
678
|
/**
|
|
560
679
|
* Drive parent notifications from a tracked child's status. Arms a pending
|
|
561
680
|
* notification while the child is working, and when it stops fires a single
|
|
@@ -576,8 +695,12 @@ export class PiSessionService {
|
|
|
576
695
|
this.subsessionNotifyArmed.set(childId, false);
|
|
577
696
|
const status = this.activities.get(childId)?.phase === "error" ? "error" : "idle";
|
|
578
697
|
const finalText = finalAssistantText(historyMessages(session));
|
|
579
|
-
const
|
|
580
|
-
const
|
|
698
|
+
const outputSection = formatSubsessionNotificationOutput(childId, finalText);
|
|
699
|
+
const workingIds = this.workingSubsessionIds(link.parentSessionId);
|
|
700
|
+
const next = workingIds.length === 0
|
|
701
|
+
? "No other tracked subsessions are working."
|
|
702
|
+
: `Still working: ${workingIds.join(", ")}. Continue working, or call yield_to_subsessions alone and last at the next join point. Further completion notices arrive automatically; do not poll.`;
|
|
703
|
+
const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n${outputSection}`;
|
|
581
704
|
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
|
582
705
|
}
|
|
583
706
|
async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
|
|
@@ -620,23 +743,42 @@ export class PiSessionService {
|
|
|
620
743
|
async status(ref) {
|
|
621
744
|
return this.statusFromSession(await this.getOrOpen(ref));
|
|
622
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* Join-time snapshot of the in-flight assistant stream. The `seq` watermark and
|
|
748
|
+
* the partial are read together in one synchronous tick (no await between the
|
|
749
|
+
* `currentSeq` read and the `state.streamingMessage` read) so a joining client
|
|
750
|
+
* can seed the partial and then apply only buffered live events with
|
|
751
|
+
* `seq > snapshot.seq`. The partial is browser-projected to strip thinking
|
|
752
|
+
* signatures; it is `null` when no assistant message is mid-stream.
|
|
753
|
+
*/
|
|
754
|
+
async streamSnapshot(ref) {
|
|
755
|
+
const session = await this.getOrOpen(ref);
|
|
756
|
+
// Single consistent tick: capture the watermark and the partial together so
|
|
757
|
+
// the seq matches the partial the client seeds against.
|
|
758
|
+
const seq = this.events.currentSeq(session.sessionId);
|
|
759
|
+
const streamingMessage = session.state.streamingMessage;
|
|
760
|
+
const partial = streamingMessage === undefined || streamingMessage === null
|
|
761
|
+
? null
|
|
762
|
+
: projectBrowserMessage(streamingMessage);
|
|
763
|
+
return { seq, partial };
|
|
764
|
+
}
|
|
623
765
|
async availableModels(ref) {
|
|
624
766
|
const session = await this.getOrOpen(ref);
|
|
625
|
-
session.
|
|
767
|
+
await session.modelRuntime.reloadConfig();
|
|
626
768
|
const models = session.scopedModels.length > 0
|
|
627
769
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
628
|
-
: session.
|
|
770
|
+
: session.modelRuntime.getAvailableSnapshot();
|
|
629
771
|
return models.map(modelToClientModel);
|
|
630
772
|
}
|
|
631
773
|
async setModel(ref, provider, modelId) {
|
|
632
774
|
await this.assertWritable(ref);
|
|
633
775
|
const session = await this.getOrOpen(ref);
|
|
634
|
-
session.
|
|
776
|
+
await session.modelRuntime.reloadConfig();
|
|
635
777
|
const candidates = session.scopedModels.length > 0
|
|
636
778
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
637
|
-
: session.
|
|
779
|
+
: session.modelRuntime.getAvailableSnapshot();
|
|
638
780
|
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
|
|
639
|
-
?? session.
|
|
781
|
+
?? session.modelRuntime.getModel(provider, modelId);
|
|
640
782
|
if (model === undefined)
|
|
641
783
|
throw new Error(`Model not found: ${provider}/${modelId}`);
|
|
642
784
|
await session.setModel(model);
|
|
@@ -795,6 +937,23 @@ export class PiSessionService {
|
|
|
795
937
|
const active = await this.getActive(ref);
|
|
796
938
|
return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
|
|
797
939
|
}
|
|
940
|
+
async reloadSessionRuntime(session) {
|
|
941
|
+
if (this.hasActiveWork(session))
|
|
942
|
+
throw new Error("Stop current session activity before reloading");
|
|
943
|
+
this.publishActivity(session, "reloading resources", "active");
|
|
944
|
+
try {
|
|
945
|
+
await session.reload();
|
|
946
|
+
this.publishActivity(session, "resources reloaded", "idle");
|
|
947
|
+
this.publishStatus(session);
|
|
948
|
+
}
|
|
949
|
+
catch (error) {
|
|
950
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
951
|
+
this.publishActivity(session, "reload failed", "error", message);
|
|
952
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
953
|
+
this.publishStatus(session);
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
798
957
|
async archive(ref) {
|
|
799
958
|
const session = await this.getOrOpen(ref);
|
|
800
959
|
if (this.hasActiveWork(session))
|
|
@@ -803,6 +962,69 @@ export class PiSessionService {
|
|
|
803
962
|
await this.closeActive(session.sessionId);
|
|
804
963
|
await this.archiveStore.archive(archiveInput);
|
|
805
964
|
}
|
|
965
|
+
async archiveMany(refs) {
|
|
966
|
+
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
967
|
+
const [archivedRecords, sessionContext] = await Promise.all([
|
|
968
|
+
this.archiveStore.list(),
|
|
969
|
+
this.bulkSessionLookupContext(uniqueRefs),
|
|
970
|
+
]);
|
|
971
|
+
const failures = [];
|
|
972
|
+
const alreadyArchivedSessionIds = [];
|
|
973
|
+
const planItems = [];
|
|
974
|
+
for (const ref of uniqueRefs) {
|
|
975
|
+
const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
976
|
+
if (archived !== undefined) {
|
|
977
|
+
alreadyArchivedSessionIds.push(archived.sessionId);
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const active = this.activeForLookup(bulkRefToLookup(ref));
|
|
981
|
+
const listed = findListedSessionForBulkRef(sessionContext, ref);
|
|
982
|
+
const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
|
|
983
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
984
|
+
failures.push({ sessionId: resolvedSessionId, error: "Stop current session activity before archiving" });
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
if (listed !== undefined) {
|
|
989
|
+
planItems.push({ input: archiveInputFromListEntry(listed) });
|
|
990
|
+
}
|
|
991
|
+
else if (active !== undefined) {
|
|
992
|
+
planItems.push({ input: archiveInputFromActiveSession(active.runtime.session) });
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
failures.push({ sessionId: ref.id, error: "Session not found" });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
failures.push({ sessionId: resolvedSessionId, error: errorMessage(error) });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const readyInputs = [];
|
|
1003
|
+
for (const item of planItems) {
|
|
1004
|
+
try {
|
|
1005
|
+
await this.closeActive(item.input.sessionId);
|
|
1006
|
+
readyInputs.push(item.input);
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
failures.push({ sessionId: item.input.sessionId, error: errorMessage(error) });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const archivedSessionIds = [...alreadyArchivedSessionIds];
|
|
1013
|
+
try {
|
|
1014
|
+
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
1015
|
+
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
for (const input of readyInputs)
|
|
1019
|
+
failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
|
|
1020
|
+
}
|
|
1021
|
+
return {
|
|
1022
|
+
archived: true,
|
|
1023
|
+
archivedSessionIds: uniqueStrings(archivedSessionIds),
|
|
1024
|
+
failures,
|
|
1025
|
+
generatedAt: new Date().toISOString(),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
806
1028
|
async archiveTree(ref) {
|
|
807
1029
|
const session = await this.getOrOpen(ref);
|
|
808
1030
|
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
@@ -814,8 +1036,7 @@ export class PiSessionService {
|
|
|
814
1036
|
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
815
1037
|
for (const input of archiveInputs)
|
|
816
1038
|
await this.closeActive(input.sessionId);
|
|
817
|
-
|
|
818
|
-
await this.archiveStore.archive(input);
|
|
1039
|
+
await this.archiveStoreArchiveMany(archiveInputs);
|
|
819
1040
|
return {
|
|
820
1041
|
archived: true,
|
|
821
1042
|
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
@@ -841,6 +1062,57 @@ export class PiSessionService {
|
|
|
841
1062
|
await this.ensureArchivedRecordMoved(record);
|
|
842
1063
|
await this.archiveStore.deleteArchived(record.sessionId);
|
|
843
1064
|
}
|
|
1065
|
+
async deleteArchivedMany(refs) {
|
|
1066
|
+
if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
1067
|
+
throw new Error("Archive store does not support deletion");
|
|
1068
|
+
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
1069
|
+
const archivedRecords = await this.archiveStore.list();
|
|
1070
|
+
const failures = [];
|
|
1071
|
+
const planItems = [];
|
|
1072
|
+
for (const ref of uniqueRefs) {
|
|
1073
|
+
const record = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
1074
|
+
if (record === undefined) {
|
|
1075
|
+
failures.push({ sessionId: ref.id, error: "Archived session not found" });
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
const active = this.activeForLookup({ id: record.sessionId, cwd: record.cwd });
|
|
1079
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1080
|
+
failures.push({ sessionId: record.sessionId, error: "Stop current session activity before deleting archived session" });
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
planItems.push({ record });
|
|
1084
|
+
}
|
|
1085
|
+
const readyRecords = [];
|
|
1086
|
+
for (const item of planItems) {
|
|
1087
|
+
try {
|
|
1088
|
+
await this.closeActive(item.record.sessionId);
|
|
1089
|
+
readyRecords.push(item.record);
|
|
1090
|
+
}
|
|
1091
|
+
catch (error) {
|
|
1092
|
+
failures.push({ sessionId: item.record.sessionId, error: errorMessage(error) });
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const moveFailures = await this.moveLegacyArchivedRecordsForDelete(readyRecords);
|
|
1096
|
+
failures.push(...moveFailures);
|
|
1097
|
+
const moveFailureIds = new Set(moveFailures.map((failure) => failure.sessionId));
|
|
1098
|
+
const deleteIds = readyRecords
|
|
1099
|
+
.map((record) => record.sessionId)
|
|
1100
|
+
.filter((sessionId) => !moveFailureIds.has(sessionId));
|
|
1101
|
+
let deletedSessionIds = [];
|
|
1102
|
+
try {
|
|
1103
|
+
deletedSessionIds = await this.archiveStoreDeleteArchivedMany(deleteIds);
|
|
1104
|
+
}
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
for (const sessionId of deleteIds)
|
|
1107
|
+
failures.push({ sessionId, error: errorMessage(error) });
|
|
1108
|
+
}
|
|
1109
|
+
return {
|
|
1110
|
+
deleted: true,
|
|
1111
|
+
deletedSessionIds,
|
|
1112
|
+
failures,
|
|
1113
|
+
generatedAt: new Date().toISOString(),
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
844
1116
|
async reload(ref) {
|
|
845
1117
|
await this.assertWritable(ref);
|
|
846
1118
|
const session = await this.getOrOpen(ref);
|
|
@@ -859,6 +1131,20 @@ export class PiSessionService {
|
|
|
859
1131
|
clearParentSessionHeader(session.sessionManager);
|
|
860
1132
|
this.unregisterSubsession(session.sessionId);
|
|
861
1133
|
}
|
|
1134
|
+
async clearQueue(ref) {
|
|
1135
|
+
await this.assertWritable(ref);
|
|
1136
|
+
const session = await this.getOrOpen(ref);
|
|
1137
|
+
this.clearCompactionPromptQueue(session.sessionId);
|
|
1138
|
+
clearSessionQueue(session);
|
|
1139
|
+
this.publishStatus(session);
|
|
1140
|
+
return this.statusFromSession(session);
|
|
1141
|
+
}
|
|
1142
|
+
async dismissWarning(ref, dismissId) {
|
|
1143
|
+
const session = await this.getOrOpen(ref);
|
|
1144
|
+
dismissSessionWarning(session, dismissId);
|
|
1145
|
+
this.publishStatus(session);
|
|
1146
|
+
return this.statusFromSession(session);
|
|
1147
|
+
}
|
|
862
1148
|
async abort(ref) {
|
|
863
1149
|
const active = this.activeForLookup(ref);
|
|
864
1150
|
if (active === undefined)
|
|
@@ -878,6 +1164,75 @@ export class PiSessionService {
|
|
|
878
1164
|
// Best-effort shutdown; callers that need errors await closeActive directly.
|
|
879
1165
|
});
|
|
880
1166
|
}
|
|
1167
|
+
async bulkSessionLookupContext(refs) {
|
|
1168
|
+
const cwdSet = new Set();
|
|
1169
|
+
let needsAllSessions = false;
|
|
1170
|
+
for (const ref of refs) {
|
|
1171
|
+
if (ref.cwd === undefined)
|
|
1172
|
+
needsAllSessions = true;
|
|
1173
|
+
else
|
|
1174
|
+
cwdSet.add(ref.cwd);
|
|
1175
|
+
}
|
|
1176
|
+
const [sessionsByCwd, allSessions] = await Promise.all([
|
|
1177
|
+
this.listSessionsByCwd([...cwdSet]),
|
|
1178
|
+
needsAllSessions ? this.sessionManager.listAll?.() ?? Promise.resolve([]) : Promise.resolve(undefined),
|
|
1179
|
+
]);
|
|
1180
|
+
return allSessions === undefined ? { sessionsByCwd } : { sessionsByCwd, allSessions };
|
|
1181
|
+
}
|
|
1182
|
+
async listSessionsByCwd(cwds) {
|
|
1183
|
+
const uniqueCwds = uniqueStrings(cwds);
|
|
1184
|
+
const entries = await Promise.all(uniqueCwds.map(async (cwd) => [cwd, await this.sessionManager.list(cwd)]));
|
|
1185
|
+
return new Map(entries);
|
|
1186
|
+
}
|
|
1187
|
+
async archiveStoreArchiveMany(inputs) {
|
|
1188
|
+
if (inputs.length === 0)
|
|
1189
|
+
return [];
|
|
1190
|
+
if (this.archiveStore.archiveMany !== undefined)
|
|
1191
|
+
return this.archiveStore.archiveMany(inputs);
|
|
1192
|
+
const records = [];
|
|
1193
|
+
for (const input of inputs)
|
|
1194
|
+
records.push(await this.archiveStore.archive(input));
|
|
1195
|
+
return records;
|
|
1196
|
+
}
|
|
1197
|
+
async archiveStoreDeleteArchivedMany(sessionIds) {
|
|
1198
|
+
if (sessionIds.length === 0)
|
|
1199
|
+
return [];
|
|
1200
|
+
if (this.archiveStore.deleteArchivedMany !== undefined)
|
|
1201
|
+
return this.archiveStore.deleteArchivedMany(sessionIds);
|
|
1202
|
+
if (this.archiveStore.deleteArchived === undefined)
|
|
1203
|
+
throw new Error("Archive store does not support deletion");
|
|
1204
|
+
for (const sessionId of sessionIds)
|
|
1205
|
+
await this.archiveStore.deleteArchived(sessionId);
|
|
1206
|
+
return [...sessionIds];
|
|
1207
|
+
}
|
|
1208
|
+
async moveLegacyArchivedRecordsForDelete(records) {
|
|
1209
|
+
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
1210
|
+
if (legacyRecords.length === 0)
|
|
1211
|
+
return [];
|
|
1212
|
+
let sessionsByCwd;
|
|
1213
|
+
try {
|
|
1214
|
+
sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
return legacyRecords.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
1218
|
+
}
|
|
1219
|
+
const moveInputs = legacyRecords
|
|
1220
|
+
.map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
|
|
1221
|
+
.filter(isDefined)
|
|
1222
|
+
.map(archiveInputFromListEntry);
|
|
1223
|
+
if (moveInputs.length === 0)
|
|
1224
|
+
return [];
|
|
1225
|
+
try {
|
|
1226
|
+
await this.archiveStoreArchiveMany(moveInputs);
|
|
1227
|
+
return [];
|
|
1228
|
+
}
|
|
1229
|
+
catch (error) {
|
|
1230
|
+
const failedIds = new Set(moveInputs.map((input) => input.sessionId));
|
|
1231
|
+
return legacyRecords
|
|
1232
|
+
.filter((record) => failedIds.has(record.sessionId))
|
|
1233
|
+
.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
881
1236
|
async cleanupPlan(request) {
|
|
882
1237
|
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll?.() ?? [], this.archiveStore.list()]);
|
|
883
1238
|
return planSessionCleanup({
|
|
@@ -922,7 +1277,19 @@ export class PiSessionService {
|
|
|
922
1277
|
const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
|
|
923
1278
|
if (session === undefined)
|
|
924
1279
|
return record;
|
|
925
|
-
|
|
1280
|
+
const [moved] = await this.archiveStoreArchiveMany([archiveInputFromListEntry(session)]);
|
|
1281
|
+
return moved ?? record;
|
|
1282
|
+
}
|
|
1283
|
+
async ensureArchivedRecordsMoved(records) {
|
|
1284
|
+
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
1285
|
+
if (legacyRecords.length === 0)
|
|
1286
|
+
return;
|
|
1287
|
+
const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1288
|
+
const moveInputs = legacyRecords
|
|
1289
|
+
.map((record) => sessionsByCwd.get(record.cwd)?.find((candidate) => candidate.id === record.sessionId))
|
|
1290
|
+
.filter(isDefined)
|
|
1291
|
+
.map(archiveInputFromListEntry);
|
|
1292
|
+
await this.archiveStoreArchiveMany(moveInputs);
|
|
926
1293
|
}
|
|
927
1294
|
async archiveInputForSession(session) {
|
|
928
1295
|
const cwd = session.sessionManager.getCwd();
|
|
@@ -985,6 +1352,9 @@ export class PiSessionService {
|
|
|
985
1352
|
return [...names];
|
|
986
1353
|
}
|
|
987
1354
|
async closeActive(sessionId) {
|
|
1355
|
+
const pendingOpens = this.pendingSessionOpenPromises(sessionId);
|
|
1356
|
+
if (pendingOpens.length > 0)
|
|
1357
|
+
await Promise.allSettled(pendingOpens);
|
|
988
1358
|
const active = this.active.get(sessionId);
|
|
989
1359
|
if (!active)
|
|
990
1360
|
return;
|
|
@@ -1019,14 +1389,40 @@ export class PiSessionService {
|
|
|
1019
1389
|
if (active !== undefined)
|
|
1020
1390
|
return active;
|
|
1021
1391
|
const archived = await this.getArchived(ref);
|
|
1022
|
-
if (archived?.archivePath !== undefined)
|
|
1023
|
-
|
|
1392
|
+
if (archived?.archivePath !== undefined) {
|
|
1393
|
+
const { archivePath } = archived;
|
|
1394
|
+
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath));
|
|
1395
|
+
}
|
|
1024
1396
|
const match = isPiSessionRef(ref)
|
|
1025
1397
|
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
1026
1398
|
: (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
1027
1399
|
if (!match)
|
|
1028
1400
|
throw new Error("Session not found");
|
|
1029
|
-
return this.
|
|
1401
|
+
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path));
|
|
1402
|
+
}
|
|
1403
|
+
openExistingSession(sessionId, cwd, openSessionManager) {
|
|
1404
|
+
const active = this.activeForLookup({ id: sessionId, cwd });
|
|
1405
|
+
if (active !== undefined)
|
|
1406
|
+
return Promise.resolve(active);
|
|
1407
|
+
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
|
|
1408
|
+
const existing = this.pendingSessionOpens.get(key);
|
|
1409
|
+
if (existing !== undefined)
|
|
1410
|
+
return existing.promise;
|
|
1411
|
+
const pending = {
|
|
1412
|
+
sessionId,
|
|
1413
|
+
promise: this.create(openSessionManager(), cwd),
|
|
1414
|
+
};
|
|
1415
|
+
pending.promise = pending.promise.finally(() => {
|
|
1416
|
+
if (this.pendingSessionOpens.get(key) === pending)
|
|
1417
|
+
this.pendingSessionOpens.delete(key);
|
|
1418
|
+
});
|
|
1419
|
+
this.pendingSessionOpens.set(key, pending);
|
|
1420
|
+
return pending.promise;
|
|
1421
|
+
}
|
|
1422
|
+
pendingSessionOpenPromises(sessionId) {
|
|
1423
|
+
return [...this.pendingSessionOpens.values()]
|
|
1424
|
+
.filter((pending) => sessionId === undefined || pending.sessionId === sessionId)
|
|
1425
|
+
.map((pending) => pending.promise);
|
|
1030
1426
|
}
|
|
1031
1427
|
async getArchived(ref) {
|
|
1032
1428
|
const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
|
|
@@ -1047,23 +1443,77 @@ export class PiSessionService {
|
|
|
1047
1443
|
}
|
|
1048
1444
|
return undefined;
|
|
1049
1445
|
}
|
|
1050
|
-
async create(sessionManager, cwd) {
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1446
|
+
async create(sessionManager, cwd, options = {}) {
|
|
1447
|
+
const delegationToolsEnabled = options.creationProvenance !== "tracked-subsession"
|
|
1448
|
+
&& await sessionAllowsDelegationTools(sessionManager, this.sessionManager);
|
|
1449
|
+
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
1450
|
+
cwd,
|
|
1451
|
+
agentDir: this.agentDir,
|
|
1452
|
+
sessionManager,
|
|
1453
|
+
delegationToolsEnabled,
|
|
1454
|
+
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
1455
|
+
});
|
|
1053
1456
|
const active = { runtime, unsubscribe: noop };
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
await this.bindSessionExtensions(session);
|
|
1457
|
+
try {
|
|
1458
|
+
await this.bindSessionExtensions(runtime.session);
|
|
1057
1459
|
this.bindRuntime(active);
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1460
|
+
runtime.setRebindSession(async (session) => {
|
|
1461
|
+
await this.bindSessionExtensions(session);
|
|
1462
|
+
this.bindRuntime(active);
|
|
1463
|
+
await this.recoverSubsessionTrackingForOpenedSession(session);
|
|
1464
|
+
});
|
|
1465
|
+
this.active.set(runtime.session.sessionId, active);
|
|
1466
|
+
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
1467
|
+
this.publishStatus(runtime.session);
|
|
1468
|
+
return active;
|
|
1469
|
+
}
|
|
1470
|
+
catch (error) {
|
|
1471
|
+
active.unsubscribe();
|
|
1472
|
+
let removedActive = false;
|
|
1473
|
+
for (const [sessionId, candidate] of this.active.entries()) {
|
|
1474
|
+
if (candidate !== active)
|
|
1475
|
+
continue;
|
|
1476
|
+
this.active.delete(sessionId);
|
|
1477
|
+
this.activities.delete(sessionId);
|
|
1478
|
+
this.clearAuthLossWarningsForSession(sessionId);
|
|
1479
|
+
this.clearCompactionPromptQueue(sessionId);
|
|
1480
|
+
removedActive = true;
|
|
1481
|
+
}
|
|
1482
|
+
if (removedActive) {
|
|
1483
|
+
this.workspaceActivity?.removeSession(runtime.session.sessionId, runtime.session.sessionManager.getCwd());
|
|
1484
|
+
}
|
|
1485
|
+
try {
|
|
1486
|
+
await runtime.session.abort();
|
|
1487
|
+
}
|
|
1488
|
+
finally {
|
|
1489
|
+
await runtime.dispose();
|
|
1490
|
+
}
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1064
1493
|
}
|
|
1065
1494
|
async bindSessionExtensions(session) {
|
|
1495
|
+
const baseUiContext = session.extensionRunner.getUIContext();
|
|
1496
|
+
const notify = (message, type) => {
|
|
1497
|
+
this.events.publish(session.sessionId, {
|
|
1498
|
+
type: "command.output",
|
|
1499
|
+
level: type === "error" ? "error" : "info",
|
|
1500
|
+
message,
|
|
1501
|
+
});
|
|
1502
|
+
};
|
|
1503
|
+
// PI WEB is a remote UI host, but currently only extension notifications
|
|
1504
|
+
// cross this boundary. Delegate every other UI method to Pi's headless
|
|
1505
|
+
// defaults so unsupported dialogs cancel safely instead of hanging.
|
|
1506
|
+
const uiContext = new Proxy(baseUiContext, {
|
|
1507
|
+
get(target, property, receiver) {
|
|
1508
|
+
if (property === "notify")
|
|
1509
|
+
return notify;
|
|
1510
|
+
const value = Reflect.get(target, property, receiver);
|
|
1511
|
+
return value;
|
|
1512
|
+
},
|
|
1513
|
+
});
|
|
1066
1514
|
await session.bindExtensions({
|
|
1515
|
+
uiContext,
|
|
1516
|
+
mode: "rpc",
|
|
1067
1517
|
onError: (error) => {
|
|
1068
1518
|
const message = `${error.extensionPath}: ${error.error}`;
|
|
1069
1519
|
this.publishActivity(session, "extension error", "error", message);
|
|
@@ -1156,10 +1606,15 @@ export class PiSessionService {
|
|
|
1156
1606
|
maybeGenerateSessionName(session, firstMessage) {
|
|
1157
1607
|
if (session.sessionName !== undefined || session.messages.length !== 0 || session.isStreaming || session.isCompacting)
|
|
1158
1608
|
return;
|
|
1609
|
+
const deterministicName = deterministicSessionName(firstMessage);
|
|
1610
|
+
if (deterministicName !== undefined) {
|
|
1611
|
+
this.applyGeneratedSessionName(session, deterministicName);
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1159
1614
|
const model = session.model;
|
|
1160
1615
|
if (model === undefined)
|
|
1161
1616
|
return;
|
|
1162
|
-
void generateShortSessionName(
|
|
1617
|
+
void generateShortSessionName(session.agent.streamFn, model, firstMessage).then((name) => {
|
|
1163
1618
|
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
1164
1619
|
}).catch(() => {
|
|
1165
1620
|
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
|
|
@@ -1172,10 +1627,11 @@ export class PiSessionService {
|
|
|
1172
1627
|
this.publishSessionName(session);
|
|
1173
1628
|
}
|
|
1174
1629
|
applyAuthChange(change = {}) {
|
|
1175
|
-
|
|
1630
|
+
// ModelRuntime.login()/logout() refresh the shared runtime before AuthService
|
|
1631
|
+
// emits the change, so no refresh is needed here. Keeping this synchronous
|
|
1632
|
+
// also lets every active session observe the same committed auth snapshot.
|
|
1176
1633
|
for (const active of this.active.values()) {
|
|
1177
1634
|
const { session } = active.runtime;
|
|
1178
|
-
session.modelRegistry.refresh();
|
|
1179
1635
|
this.syncCurrentModelAuthWarning(session, change.removedProviderId);
|
|
1180
1636
|
this.publishStatus(session);
|
|
1181
1637
|
}
|
|
@@ -1187,10 +1643,10 @@ export class PiSessionService {
|
|
|
1187
1643
|
if (model.provider === "unknown" && model.id === "unknown")
|
|
1188
1644
|
return;
|
|
1189
1645
|
const warningKey = authLossWarningKey(session.sessionId, model.provider, model.id);
|
|
1190
|
-
const registered = session.
|
|
1646
|
+
const registered = session.modelRuntime.getModel(model.provider, model.id);
|
|
1191
1647
|
if (registered === undefined)
|
|
1192
1648
|
return;
|
|
1193
|
-
if (session.
|
|
1649
|
+
if (session.modelRuntime.hasConfiguredAuth(model.provider)) {
|
|
1194
1650
|
this.authLossWarnings.delete(warningKey);
|
|
1195
1651
|
return;
|
|
1196
1652
|
}
|
|
@@ -1334,8 +1790,10 @@ export class PiSessionService {
|
|
|
1334
1790
|
const stats = session.getSessionStats();
|
|
1335
1791
|
const model = session.model === undefined ? undefined : modelToClientModel(session.model);
|
|
1336
1792
|
const contextUsage = session.getContextUsage();
|
|
1793
|
+
const warnings = this.warningsForSession(session);
|
|
1337
1794
|
return {
|
|
1338
1795
|
sessionId: session.sessionId,
|
|
1796
|
+
persisted: sessionFileExists(session.sessionFile),
|
|
1339
1797
|
...(model === undefined ? {} : { model }),
|
|
1340
1798
|
thinkingLevel: session.thinkingLevel,
|
|
1341
1799
|
isStreaming: session.isStreaming,
|
|
@@ -1347,8 +1805,23 @@ export class PiSessionService {
|
|
|
1347
1805
|
tokens: stats.tokens,
|
|
1348
1806
|
cost: stats.cost,
|
|
1349
1807
|
...(contextUsage === undefined ? {} : { contextUsage }),
|
|
1808
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
1350
1809
|
};
|
|
1351
1810
|
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Compute the live warning set for a session: runtime/resource diagnostics from
|
|
1813
|
+
* the active runtime (if any) plus the Anthropic subscription-auth notice. Read
|
|
1814
|
+
* fresh on each status publish so a rebuilt runtime or an auth/model change is
|
|
1815
|
+
* reflected without caching a stale snapshot.
|
|
1816
|
+
*/
|
|
1817
|
+
warningsForSession(session) {
|
|
1818
|
+
const runtime = this.active.get(session.sessionId)?.runtime;
|
|
1819
|
+
const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
|
|
1820
|
+
const anthropic = anthropicSubscriptionWarning(session, join(this.agentDir, "auth.json"));
|
|
1821
|
+
if (anthropic !== undefined)
|
|
1822
|
+
warnings.push(anthropic);
|
|
1823
|
+
return warnings;
|
|
1824
|
+
}
|
|
1352
1825
|
pendingMessageCount(session) {
|
|
1353
1826
|
return session.pendingMessageCount + this.compactionQueuedMessages(session.sessionId).length;
|
|
1354
1827
|
}
|
|
@@ -1368,6 +1841,38 @@ function previewResponseFromPlan(plan) {
|
|
|
1368
1841
|
...(plan.skippedBusySessionIds.length === 0 ? {} : { skippedBusySessionIds: plan.skippedBusySessionIds }),
|
|
1369
1842
|
};
|
|
1370
1843
|
}
|
|
1844
|
+
function uniqueBulkSessionRefs(refs) {
|
|
1845
|
+
const seen = new Set();
|
|
1846
|
+
const unique = [];
|
|
1847
|
+
for (const ref of refs) {
|
|
1848
|
+
const key = `${ref.cwd ?? ""}\0${ref.id}`;
|
|
1849
|
+
if (seen.has(key))
|
|
1850
|
+
continue;
|
|
1851
|
+
seen.add(key);
|
|
1852
|
+
unique.push(ref);
|
|
1853
|
+
}
|
|
1854
|
+
return unique;
|
|
1855
|
+
}
|
|
1856
|
+
function bulkRefToLookup(ref) {
|
|
1857
|
+
return ref.cwd === undefined ? ref.id : { id: ref.id, cwd: ref.cwd };
|
|
1858
|
+
}
|
|
1859
|
+
function findArchivedRecordForBulkRef(records, ref) {
|
|
1860
|
+
return records.find((record) => (ref.cwd === undefined || record.cwd === ref.cwd) && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
|
|
1861
|
+
}
|
|
1862
|
+
function findListedSessionForBulkRef(context, ref) {
|
|
1863
|
+
if (ref.cwd !== undefined)
|
|
1864
|
+
return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
1865
|
+
return context.allSessions === undefined ? undefined : findSessionByIdOrPrefix(context.allSessions, ref.id);
|
|
1866
|
+
}
|
|
1867
|
+
function findSessionByIdOrPrefix(sessions, sessionId) {
|
|
1868
|
+
return sessions.find((session) => session.id === sessionId) ?? sessions.find((session) => session.id.startsWith(sessionId));
|
|
1869
|
+
}
|
|
1870
|
+
function uniqueStrings(values) {
|
|
1871
|
+
return [...new Set(values)];
|
|
1872
|
+
}
|
|
1873
|
+
function errorMessage(error) {
|
|
1874
|
+
return error instanceof Error ? error.message : String(error);
|
|
1875
|
+
}
|
|
1371
1876
|
function modelToClientModel(model) {
|
|
1372
1877
|
if (model === undefined)
|
|
1373
1878
|
return {};
|
|
@@ -1386,6 +1891,7 @@ function clientSessionFromListEntry(session) {
|
|
|
1386
1891
|
id: session.id,
|
|
1387
1892
|
path: session.path,
|
|
1388
1893
|
cwd: session.cwd,
|
|
1894
|
+
persisted: true,
|
|
1389
1895
|
...(session.name === undefined ? {} : { name: session.name }),
|
|
1390
1896
|
created: session.created.toISOString(),
|
|
1391
1897
|
modified: session.modified.toISOString(),
|
|
@@ -1514,6 +2020,76 @@ function archivedTimestamp(record) {
|
|
|
1514
2020
|
function isDefined(value) {
|
|
1515
2021
|
return value !== undefined;
|
|
1516
2022
|
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Resolve the delegation capability from server-owned, persisted session
|
|
2025
|
+
* provenance. A copied marker is not enough: the child header and reciprocal
|
|
2026
|
+
* parent link must identify the exact same session files.
|
|
2027
|
+
*/
|
|
2028
|
+
export async function sessionAllowsDelegationTools(sessionManager, managers) {
|
|
2029
|
+
const trackedLink = await verifiedTrackedSubsessionLink(managers, {
|
|
2030
|
+
sessionId: sessionManager.getSessionId(),
|
|
2031
|
+
sessionFile: sessionManager.getSessionFile(),
|
|
2032
|
+
sessionManager,
|
|
2033
|
+
cwd: sessionManager.getCwd(),
|
|
2034
|
+
});
|
|
2035
|
+
return trackedLink === undefined;
|
|
2036
|
+
}
|
|
2037
|
+
async function verifiedTrackedSubsessionLink(managers, session) {
|
|
2038
|
+
// Child markers are only hints; the current child header and reciprocal
|
|
2039
|
+
// parent custom link must agree on the exact ids and files before relinking.
|
|
2040
|
+
const entries = session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch();
|
|
2041
|
+
let marker;
|
|
2042
|
+
for (const entry of entries) {
|
|
2043
|
+
const parsed = parsePersistedChildSubsessionLink(entry);
|
|
2044
|
+
if (parsed?.spawnedSessionId === session.sessionId)
|
|
2045
|
+
marker = parsed;
|
|
2046
|
+
}
|
|
2047
|
+
if (marker === undefined)
|
|
2048
|
+
return undefined;
|
|
2049
|
+
const childSessionFile = nonEmptyString(session.sessionFile);
|
|
2050
|
+
if (childSessionFile === undefined)
|
|
2051
|
+
return undefined;
|
|
2052
|
+
const childHeader = await readSessionHeaderSummary(childSessionFile);
|
|
2053
|
+
if (childHeader?.id !== session.sessionId)
|
|
2054
|
+
return undefined;
|
|
2055
|
+
const parentSessionFile = nonEmptyString(childHeader.parentSession);
|
|
2056
|
+
if (parentSessionFile === undefined)
|
|
2057
|
+
return undefined;
|
|
2058
|
+
const parentHeader = await readSessionHeaderSummary(parentSessionFile);
|
|
2059
|
+
if (parentHeader?.id !== marker.spawnedBySessionId)
|
|
2060
|
+
return undefined;
|
|
2061
|
+
const parentLink = findReciprocalParentSubsessionLink(managers, parentSessionFile, marker.spawnedBySessionId, session.sessionId, childSessionFile);
|
|
2062
|
+
if (parentLink === undefined)
|
|
2063
|
+
return undefined;
|
|
2064
|
+
return {
|
|
2065
|
+
parentSessionId: marker.spawnedBySessionId,
|
|
2066
|
+
childSessionId: session.sessionId,
|
|
2067
|
+
childSessionFile,
|
|
2068
|
+
parentSessionFile,
|
|
2069
|
+
cwd: parentLink.cwd ?? session.cwd,
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
function findReciprocalParentSubsessionLink(managers, parentSessionFile, parentSessionId, childSessionId, childSessionFile) {
|
|
2073
|
+
let parentManager;
|
|
2074
|
+
try {
|
|
2075
|
+
parentManager = managers.open(parentSessionFile);
|
|
2076
|
+
}
|
|
2077
|
+
catch {
|
|
2078
|
+
return undefined;
|
|
2079
|
+
}
|
|
2080
|
+
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
2081
|
+
for (const entry of entries) {
|
|
2082
|
+
const link = parsePersistedParentSubsessionLink(entry);
|
|
2083
|
+
if (link === undefined)
|
|
2084
|
+
continue;
|
|
2085
|
+
if (link.spawnedBySessionId !== parentSessionId || link.spawnedSessionId !== childSessionId)
|
|
2086
|
+
continue;
|
|
2087
|
+
if (link.spawnedSessionFile === undefined || !sessionPathsEqual(link.spawnedSessionFile, childSessionFile))
|
|
2088
|
+
continue;
|
|
2089
|
+
return link;
|
|
2090
|
+
}
|
|
2091
|
+
return undefined;
|
|
2092
|
+
}
|
|
1517
2093
|
function trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile) {
|
|
1518
2094
|
return {
|
|
1519
2095
|
parentSessionId,
|
|
@@ -1579,6 +2155,16 @@ function subsessionHydratedParentKey(parentSessionId, parentSessionFile) {
|
|
|
1579
2155
|
function sessionPathsEqual(a, b) {
|
|
1580
2156
|
return cwdPathsEqual(a, b);
|
|
1581
2157
|
}
|
|
2158
|
+
function sessionFileExists(sessionFile) {
|
|
2159
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
2160
|
+
return false;
|
|
2161
|
+
try {
|
|
2162
|
+
return statSync(sessionFile).isFile();
|
|
2163
|
+
}
|
|
2164
|
+
catch {
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
1582
2168
|
function sessionFileMatches(session, expectedSessionFile) {
|
|
1583
2169
|
const sessionFile = nonEmptyString(session.sessionFile);
|
|
1584
2170
|
return sessionFile !== undefined && expectedSessionFile !== undefined && sessionPathsEqual(sessionFile, expectedSessionFile);
|
|
@@ -1697,11 +2283,13 @@ const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
|
|
|
1697
2283
|
const SUBSESSION_CHILD_LINK_CUSTOM_TYPE = "pi-web.subsession.spawned";
|
|
1698
2284
|
/** customType marking a parent-facing subsession-completion notice. */
|
|
1699
2285
|
const SUBSESSION_NOTIFICATION_CUSTOM_TYPE = "subsession.completion";
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
2286
|
+
const SUBSESSION_NOTIFICATION_MAX_OUTPUT_CHARS = 2000;
|
|
2287
|
+
/** Avoid duplicating a partial result in context when deliberate inspection can return the full output. */
|
|
2288
|
+
function formatSubsessionNotificationOutput(childSessionId, text) {
|
|
2289
|
+
if (text.length > SUBSESSION_NOTIFICATION_MAX_OUTPUT_CHARS) {
|
|
2290
|
+
return `Output from subsession ${childSessionId} was too long for this completion notice and was omitted. Call check_subsession with sessionId "${childSessionId}" to retrieve the final output.`;
|
|
2291
|
+
}
|
|
2292
|
+
return `--- SUBSESSION OUTPUT: ${childSessionId} ---\n${text === "" ? "(no output)" : text}`;
|
|
1705
2293
|
}
|
|
1706
2294
|
/** Most recent assistant text from a history message list, or "" if none. */
|
|
1707
2295
|
function finalAssistantText(messages) {
|