@jmfederico/pi-web 1.202606.6 → 1.202607.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/README.md +87 -293
- package/dist/cli.js +25 -7
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/CodeViewer-tXF3UTsY.js +4 -0
- package/dist/client/assets/TerminalPanel-D_ihiWAt.js +187 -0
- package/dist/client/assets/UnifiedDiffViewer-BziirXBJ.js +39 -0
- package/dist/client/assets/index-Brfk1lwl.js +2950 -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 +6 -3
- package/dist/config.js +33 -1
- package/dist/config.js.map +1 -1
- package/dist/docker/piWebDockerCommandPlan.js +211 -0
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -0
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +15 -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 +30 -2
- package/dist/server/app.js +32 -5
- 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 +112 -2
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/machines/machineClient.js +51 -13
- package/dist/server/machines/machineClient.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +72 -4
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineService.js +4 -26
- package/dist/server/machines/machineService.js.map +1 -1
- package/dist/server/piPackageRoutes.js +87 -0
- package/dist/server/piPackageRoutes.js.map +1 -0
- package/dist/server/piPackageService.js +69 -0
- package/dist/server/piPackageService.js.map +1 -0
- package/dist/server/piWebPluginService.js +43 -8
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebStatus.js +57 -0
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +2 -1
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessions/attachmentService.js +69 -11
- package/dist/server/sessions/attachmentService.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/piSessionManagerGateway.js +20 -2
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +905 -73
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionArchiveStore.js +51 -22
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
- package/dist/server/sessions/sessionCleanup.js +157 -0
- package/dist/server/sessions/sessionCleanup.js.map +1 -0
- 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 +55 -1
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +9 -4
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +28 -18
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +55 -3
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/fileContentService.js +112 -1
- package/dist/server/workspaces/fileContentService.js.map +1 -1
- package/dist/server/workspaces/fileSuggestions.js +7 -3
- package/dist/server/workspaces/fileSuggestions.js.map +1 -1
- package/dist/server/workspaces/pathSafety.js +16 -1
- package/dist/server/workspaces/pathSafety.js.map +1 -1
- package/dist/server/workspaces/projectPiWebConfig.js +6 -1
- package/dist/server/workspaces/projectPiWebConfig.js.map +1 -1
- package/dist/shared/activity.js +0 -15
- package/dist/shared/activity.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +146 -7
- package/dist/shared/apiTypes.js +5 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +29 -2
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +15 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/piWebStatusParsing.js +20 -8
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/dist/shared/promptAttachments.js +30 -11
- package/dist/shared/promptAttachments.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 +62 -14
- package/docs/plugins.md +155 -17
- package/package.json +35 -31
- package/dist/client/assets/CodeViewer-Bv8l_MFi.js +0 -4
- package/dist/client/assets/TerminalPanel-BZKsaE9s.js +0 -126
- package/dist/client/assets/index-C9oPLpm7.js +0 -2408
- package/dist/client/assets/vendor-editor-core-Bc1A7SO7.js +0 -12
- package/dist/client/assets/vendor-editor-languages-D0CnB2js.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,19 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { open, readFile, writeFile } from "node:fs/promises";
|
|
2
3
|
import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
4
5
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
5
6
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
6
7
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
7
8
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
8
|
-
import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
9
|
+
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
9
10
|
import { computeEditPreview } from "./editPreview.js";
|
|
10
11
|
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
|
11
12
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
12
13
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
13
|
-
import { cwdPathsEqual } from "../workingDirectory.js";
|
|
14
|
+
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
14
15
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
15
16
|
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
16
17
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
18
|
+
import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
17
19
|
const noopLogger = { info() { } };
|
|
18
20
|
function noop() {
|
|
19
21
|
// Intentionally empty default unsubscribe callback.
|
|
@@ -50,20 +52,50 @@ function parsePromptStreamingBehavior(value) {
|
|
|
50
52
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
51
53
|
if (!(options.sessionManager instanceof SessionManager))
|
|
52
54
|
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
53
|
-
|
|
55
|
+
const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.delegationToolsEnabled);
|
|
56
|
+
return createAgentSessionRuntime(runtimeFactory, {
|
|
57
|
+
cwd: options.cwd,
|
|
58
|
+
agentDir: options.agentDir,
|
|
59
|
+
sessionManager: options.sessionManager,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, delegationToolsEnabled) {
|
|
63
|
+
// These inputs belong only to the session being opened. A later runtime
|
|
64
|
+
// replacement resolves its own model and delegation capability.
|
|
65
|
+
let pendingInitialModel = initialModel;
|
|
66
|
+
let pendingDelegationToolsEnabled = delegationToolsEnabled;
|
|
67
|
+
return async (options) => {
|
|
68
|
+
const model = pendingInitialModel;
|
|
69
|
+
const toolsEnabled = pendingDelegationToolsEnabled;
|
|
70
|
+
pendingInitialModel = undefined;
|
|
71
|
+
pendingDelegationToolsEnabled = undefined;
|
|
72
|
+
return createRuntime({
|
|
73
|
+
...options,
|
|
74
|
+
...(model === undefined ? {} : { initialModel: model }),
|
|
75
|
+
...(toolsEnabled === undefined ? {} : { delegationToolsEnabled: toolsEnabled }),
|
|
76
|
+
});
|
|
77
|
+
};
|
|
54
78
|
}
|
|
55
|
-
function
|
|
56
|
-
return
|
|
79
|
+
export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions) {
|
|
80
|
+
return [
|
|
81
|
+
createPiWebEditToolDefinition(cwd),
|
|
82
|
+
...(delegationEnabled && spawn !== undefined ? [createSpawnSessionToolDefinition(cwd, { spawn })] : []),
|
|
83
|
+
...(delegationEnabled && subsessions !== undefined ? createSubsessionToolDefinitions(cwd, subsessions) : []),
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
function createDefaultRuntimeFactory(authStorage, modelRegistry, sessionManagers, spawn, subsessions) {
|
|
87
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => {
|
|
57
88
|
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
90
|
+
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
91
|
+
const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions);
|
|
92
|
+
const result = await createAgentSessionFromServices({
|
|
93
|
+
services,
|
|
94
|
+
sessionManager,
|
|
95
|
+
customTools,
|
|
96
|
+
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
97
|
+
...(initialModel === undefined ? {} : { model: initialModel }),
|
|
98
|
+
});
|
|
67
99
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
68
100
|
};
|
|
69
101
|
}
|
|
@@ -92,6 +124,7 @@ export class PiSessionService {
|
|
|
92
124
|
constructor(events, deps = {}) {
|
|
93
125
|
this.events = events;
|
|
94
126
|
this.active = new Map();
|
|
127
|
+
this.pendingSessionOpens = new Map();
|
|
95
128
|
this.activities = new Map();
|
|
96
129
|
this.compactionPromptQueues = new Map();
|
|
97
130
|
this.compactionDrainTimers = new Map();
|
|
@@ -100,6 +133,10 @@ export class PiSessionService {
|
|
|
100
133
|
this.subsessionParents = new Map();
|
|
101
134
|
/** Parent session id -> the set of tracked subsession ids it spawned. */
|
|
102
135
|
this.subsessionChildren = new Map();
|
|
136
|
+
/** Tracked subsession id -> persisted recovery details for the child. */
|
|
137
|
+
this.subsessionLinks = new Map();
|
|
138
|
+
/** Parent id/file identities whose persisted links have already been loaded. */
|
|
139
|
+
this.subsessionHydratedParents = new Set();
|
|
103
140
|
/**
|
|
104
141
|
* Tracked subsession id -> whether a completion notification is armed.
|
|
105
142
|
* Armed when the child starts working; firing on completion disarms it so a
|
|
@@ -112,14 +149,15 @@ export class PiSessionService {
|
|
|
112
149
|
this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
|
|
113
150
|
this.spawnTargets = deps.spawnTargets;
|
|
114
151
|
this.logger = deps.logger ?? noopLogger;
|
|
152
|
+
this.now = deps.now ?? (() => new Date());
|
|
115
153
|
// Subsessions are a beta capability gated behind their own flag, and they
|
|
116
154
|
// also require the spawn capability (they share its project-scope resolver).
|
|
117
155
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
118
|
-
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
156
|
+
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
119
157
|
spawn: (input) => this.spawnSubsession(input),
|
|
120
|
-
list: (parentSessionId) => this.listSubsessions(parentSessionId),
|
|
121
|
-
check: (parentSessionId, sessionId) => this.checkSubsession(parentSessionId, sessionId),
|
|
122
|
-
read: (parentSessionId, sessionId, query) => this.readSubsession(parentSessionId, sessionId, query),
|
|
158
|
+
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
159
|
+
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
160
|
+
read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
|
|
123
161
|
});
|
|
124
162
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
125
163
|
this.workspaceActivity = deps.workspaceActivity;
|
|
@@ -133,27 +171,79 @@ export class PiSessionService {
|
|
|
133
171
|
this.publishActivity(session, result === "success" ? "compaction complete" : "compaction failed", result === "success" ? "idle" : "error", detail);
|
|
134
172
|
this.publishStatus(session);
|
|
135
173
|
},
|
|
174
|
+
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
136
175
|
}, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
|
|
137
176
|
}
|
|
138
177
|
activeCount() {
|
|
139
178
|
return this.active.size;
|
|
140
179
|
}
|
|
180
|
+
async cleanupPreview(request) {
|
|
181
|
+
return previewResponseFromPlan(await this.cleanupPlan(request));
|
|
182
|
+
}
|
|
183
|
+
async cleanup(request) {
|
|
184
|
+
const plan = await this.cleanupPlan(request);
|
|
185
|
+
if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
186
|
+
throw new Error("Archive store does not support deletion");
|
|
187
|
+
const archiveInputs = [];
|
|
188
|
+
const readyArchiveInputs = [];
|
|
189
|
+
const deleteRecords = [];
|
|
190
|
+
const readyDeleteRecords = [];
|
|
191
|
+
const skippedBusySessionIds = new Set(plan.skippedBusySessionIds);
|
|
192
|
+
for (const input of plan.archiveInputs) {
|
|
193
|
+
if (this.activeSessionHasWork(input.sessionId)) {
|
|
194
|
+
skippedBusySessionIds.add(input.sessionId);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
await this.closeActive(input.sessionId);
|
|
198
|
+
readyArchiveInputs.push(input);
|
|
199
|
+
}
|
|
200
|
+
await this.archiveStoreArchiveMany(readyArchiveInputs);
|
|
201
|
+
archiveInputs.push(...readyArchiveInputs);
|
|
202
|
+
for (const record of plan.deleteRecords) {
|
|
203
|
+
if (this.activeSessionHasWork(record.sessionId)) {
|
|
204
|
+
skippedBusySessionIds.add(record.sessionId);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
await this.closeActive(record.sessionId);
|
|
208
|
+
readyDeleteRecords.push(record);
|
|
209
|
+
}
|
|
210
|
+
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
211
|
+
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
212
|
+
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
213
|
+
return summarizeSessionCleanupExecution({
|
|
214
|
+
archiveInputs,
|
|
215
|
+
deleteRecords,
|
|
216
|
+
thresholds: plan.thresholds,
|
|
217
|
+
generatedAt: plan.generatedAt,
|
|
218
|
+
skippedBusySessionIds: [...skippedBusySessionIds],
|
|
219
|
+
});
|
|
220
|
+
}
|
|
141
221
|
async dispose() {
|
|
142
222
|
clearInterval(this.heartbeat);
|
|
143
223
|
this.clearCompactionDrainTimers();
|
|
224
|
+
const pendingOpens = this.pendingSessionOpenPromises();
|
|
225
|
+
if (pendingOpens.length > 0)
|
|
226
|
+
await Promise.allSettled(pendingOpens);
|
|
144
227
|
const activeSessions = Array.from(new Set(this.active.values()));
|
|
145
228
|
this.active.clear();
|
|
229
|
+
this.pendingSessionOpens.clear();
|
|
146
230
|
this.activities.clear();
|
|
147
231
|
this.compactionPromptQueues.clear();
|
|
148
232
|
this.authLossWarnings.clear();
|
|
149
233
|
this.subsessionParents.clear();
|
|
150
234
|
this.subsessionChildren.clear();
|
|
235
|
+
this.subsessionLinks.clear();
|
|
236
|
+
this.subsessionHydratedParents.clear();
|
|
151
237
|
this.subsessionNotifyArmed.clear();
|
|
152
238
|
await Promise.all(activeSessions.map(async (active) => {
|
|
153
239
|
active.unsubscribe();
|
|
154
240
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
|
|
155
|
-
|
|
156
|
-
|
|
241
|
+
try {
|
|
242
|
+
await active.runtime.session.abort();
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await active.runtime.dispose();
|
|
246
|
+
}
|
|
157
247
|
}));
|
|
158
248
|
}
|
|
159
249
|
async list(cwd) {
|
|
@@ -171,20 +261,27 @@ export class PiSessionService {
|
|
|
171
261
|
.filter(isDefined);
|
|
172
262
|
return [...unarchivedSessions, ...archivedSessions];
|
|
173
263
|
}
|
|
174
|
-
async start(cwd,
|
|
175
|
-
|
|
264
|
+
async start(cwd, options = {}) {
|
|
265
|
+
return this.startSession(cwd, options);
|
|
266
|
+
}
|
|
267
|
+
async startSession(cwd, options) {
|
|
268
|
+
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
|
|
269
|
+
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
270
|
+
...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
|
|
271
|
+
});
|
|
176
272
|
const { session } = active.runtime;
|
|
177
273
|
const created = {
|
|
178
274
|
id: session.sessionId,
|
|
179
275
|
path: session.sessionFile ?? "",
|
|
180
276
|
cwd,
|
|
277
|
+
persisted: sessionFileExists(session.sessionFile),
|
|
181
278
|
created: new Date().toISOString(),
|
|
182
279
|
modified: new Date().toISOString(),
|
|
183
280
|
messageCount: session.messages.length,
|
|
184
281
|
firstMessage: "",
|
|
185
282
|
// Include the parent so listeners can nest the new session in the tree
|
|
186
283
|
// immediately, instead of showing it flat until the next reload.
|
|
187
|
-
...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
|
|
284
|
+
...(options.parentSession === undefined ? {} : { parentSessionPath: options.parentSession }),
|
|
188
285
|
};
|
|
189
286
|
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
190
287
|
// session to their list without a manual reload.
|
|
@@ -202,7 +299,7 @@ export class PiSessionService {
|
|
|
202
299
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
203
300
|
if (!decision.allowed)
|
|
204
301
|
throw spawnTargetError(decision);
|
|
205
|
-
const created = await this.start(decision.cwd);
|
|
302
|
+
const created = await this.start(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
|
|
206
303
|
await this.prompt(created.id, input.prompt);
|
|
207
304
|
this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
|
|
208
305
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
@@ -219,69 +316,247 @@ export class PiSessionService {
|
|
|
219
316
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
220
317
|
if (!decision.allowed)
|
|
221
318
|
throw spawnTargetError(decision);
|
|
222
|
-
const created = await this.
|
|
223
|
-
|
|
319
|
+
const created = await this.startSession(decision.cwd, {
|
|
320
|
+
...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
|
|
321
|
+
...(input.model === undefined ? {} : { initialModel: input.model }),
|
|
322
|
+
creationProvenance: "tracked-subsession",
|
|
323
|
+
});
|
|
324
|
+
const parentSessionFile = nonEmptyString(input.parentSessionFile);
|
|
325
|
+
const link = {
|
|
326
|
+
parentSessionId: input.parentSessionId,
|
|
327
|
+
childSessionId: created.id,
|
|
328
|
+
...(created.path === "" ? {} : { childSessionFile: created.path }),
|
|
329
|
+
...(parentSessionFile === undefined ? {} : { parentSessionFile }),
|
|
330
|
+
cwd: decision.cwd,
|
|
331
|
+
};
|
|
332
|
+
this.registerVerifiedSubsession(link);
|
|
333
|
+
this.persistSubsessionLink(link);
|
|
334
|
+
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
224
335
|
await this.prompt(created.id, input.prompt);
|
|
225
336
|
this.logger.info({ parentSessionId: input.parentSessionId, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_subsession started a tracked child session");
|
|
226
337
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
227
338
|
}
|
|
228
339
|
/** Summaries of the tracked subsessions spawned by `parentSessionId`. */
|
|
229
|
-
async listSubsessions(parentSessionId) {
|
|
340
|
+
async listSubsessions(parentSessionId, parentSessionFile) {
|
|
341
|
+
const parentFile = nonEmptyString(parentSessionFile);
|
|
342
|
+
await this.hydrateSubsessionsForParent(parentSessionId, parentFile);
|
|
230
343
|
const childIds = this.subsessionChildren.get(parentSessionId);
|
|
231
344
|
if (childIds === undefined)
|
|
232
345
|
return [];
|
|
233
|
-
|
|
346
|
+
const authorizedChildIds = [...childIds].filter((childId) => this.subsessionLinkBelongsToParent(parentSessionId, parentFile, childId));
|
|
347
|
+
return Promise.all(authorizedChildIds.map(async (childId) => ({ sessionId: childId, ...(await this.subsessionSummaryFields(childId)) })));
|
|
234
348
|
}
|
|
235
349
|
/** Status and final result of a subsession, scoped to the caller's children. */
|
|
236
|
-
async checkSubsession(parentSessionId, sessionId) {
|
|
237
|
-
const session = await this.openSubsession(parentSessionId, sessionId);
|
|
350
|
+
async checkSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
351
|
+
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile);
|
|
238
352
|
const messages = historyMessages(session);
|
|
239
353
|
return {
|
|
240
354
|
sessionId,
|
|
241
355
|
cwd: session.sessionManager.getCwd(),
|
|
242
|
-
status:
|
|
356
|
+
status: this.subsessionStatus(session),
|
|
243
357
|
finalText: finalAssistantText(messages),
|
|
244
358
|
messageCount: messages.length,
|
|
245
359
|
};
|
|
246
360
|
}
|
|
247
361
|
/** Filtered, paginated transcript of a subsession, scoped to the caller's children. */
|
|
248
|
-
async readSubsession(parentSessionId, sessionId, query) {
|
|
249
|
-
const session = await this.openSubsession(parentSessionId, sessionId);
|
|
362
|
+
async readSubsession(parentSessionId, sessionId, query, parentSessionFile) {
|
|
363
|
+
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile);
|
|
250
364
|
const view = buildTranscriptView(historyMessages(session), query);
|
|
251
365
|
return {
|
|
252
366
|
sessionId,
|
|
253
367
|
cwd: session.sessionManager.getCwd(),
|
|
254
|
-
status:
|
|
368
|
+
status: this.subsessionStatus(session),
|
|
255
369
|
...view,
|
|
256
370
|
};
|
|
257
371
|
}
|
|
258
372
|
/** Open a session after verifying it is one of the caller's tracked children. */
|
|
259
|
-
async openSubsession(parentSessionId, sessionId) {
|
|
260
|
-
|
|
373
|
+
async openSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
374
|
+
const parentFile = nonEmptyString(parentSessionFile);
|
|
375
|
+
await this.hydrateSubsessionsForParent(parentSessionId, parentFile);
|
|
376
|
+
if (this.subsessionParents.get(sessionId) !== parentSessionId || !this.subsessionLinkBelongsToParent(parentSessionId, parentFile, sessionId)) {
|
|
261
377
|
throw new Error(`Session ${sessionId} is not one of your subsessions`);
|
|
262
378
|
}
|
|
263
|
-
return this.
|
|
379
|
+
return this.getOrOpenTrackedSubsession(sessionId);
|
|
380
|
+
}
|
|
381
|
+
subsessionLinkBelongsToParent(parentSessionId, parentSessionFile, childSessionId) {
|
|
382
|
+
const link = this.subsessionLinks.get(childSessionId);
|
|
383
|
+
if (link?.parentSessionId !== parentSessionId)
|
|
384
|
+
return false;
|
|
385
|
+
return parentSessionFile === undefined || trackedLinkParentFileMatches(link, parentSessionFile);
|
|
386
|
+
}
|
|
387
|
+
activeChildForSubsessionLink(link) {
|
|
388
|
+
const active = this.active.get(link.childSessionId);
|
|
389
|
+
if (active === undefined)
|
|
390
|
+
return undefined;
|
|
391
|
+
return activeSessionFileMatches(active, link.childSessionFile) ? active : undefined;
|
|
392
|
+
}
|
|
393
|
+
activeParentForSubsessionLink(link) {
|
|
394
|
+
const active = this.active.get(link.parentSessionId);
|
|
395
|
+
if (active === undefined)
|
|
396
|
+
return undefined;
|
|
397
|
+
return activeSessionFileMatches(active, link.parentSessionFile) ? active : undefined;
|
|
264
398
|
}
|
|
265
|
-
|
|
399
|
+
subsessionLinkForActiveChild(session) {
|
|
400
|
+
const childId = session.sessionId;
|
|
401
|
+
const parentId = this.subsessionParents.get(childId);
|
|
402
|
+
const link = this.subsessionLinks.get(childId);
|
|
403
|
+
if (parentId === undefined || link?.parentSessionId !== parentId)
|
|
404
|
+
return undefined;
|
|
405
|
+
return sessionFileMatches(session, link.childSessionFile) ? link : undefined;
|
|
406
|
+
}
|
|
407
|
+
registerVerifiedSubsession(link) {
|
|
408
|
+
const { childSessionId, parentSessionId } = link;
|
|
409
|
+
const previousParentId = this.subsessionParents.get(childSessionId);
|
|
410
|
+
if (previousParentId !== undefined && previousParentId !== parentSessionId) {
|
|
411
|
+
const previousChildren = this.subsessionChildren.get(previousParentId);
|
|
412
|
+
previousChildren?.delete(childSessionId);
|
|
413
|
+
if (previousChildren?.size === 0)
|
|
414
|
+
this.subsessionChildren.delete(previousParentId);
|
|
415
|
+
}
|
|
266
416
|
this.subsessionParents.set(childSessionId, parentSessionId);
|
|
267
417
|
const children = this.subsessionChildren.get(parentSessionId) ?? new Set();
|
|
268
418
|
children.add(childSessionId);
|
|
269
419
|
this.subsessionChildren.set(parentSessionId, children);
|
|
270
|
-
this.
|
|
420
|
+
this.subsessionLinks.set(childSessionId, link);
|
|
421
|
+
if (!this.subsessionNotifyArmed.has(childSessionId))
|
|
422
|
+
this.subsessionNotifyArmed.set(childSessionId, false);
|
|
423
|
+
}
|
|
424
|
+
unregisterSubsession(childSessionId) {
|
|
425
|
+
const parentSessionId = this.subsessionParents.get(childSessionId);
|
|
426
|
+
this.subsessionParents.delete(childSessionId);
|
|
427
|
+
this.subsessionLinks.delete(childSessionId);
|
|
428
|
+
this.subsessionNotifyArmed.delete(childSessionId);
|
|
429
|
+
if (parentSessionId === undefined)
|
|
430
|
+
return;
|
|
431
|
+
const children = this.subsessionChildren.get(parentSessionId);
|
|
432
|
+
children?.delete(childSessionId);
|
|
433
|
+
if (children?.size === 0)
|
|
434
|
+
this.subsessionChildren.delete(parentSessionId);
|
|
435
|
+
}
|
|
436
|
+
persistSubsessionLink(link) {
|
|
437
|
+
const parent = this.activeParentForSubsessionLink(link)?.runtime.session;
|
|
438
|
+
if (parent === undefined)
|
|
439
|
+
return;
|
|
440
|
+
if (parent.sessionManager.appendCustomEntry === undefined)
|
|
441
|
+
return;
|
|
442
|
+
try {
|
|
443
|
+
parent.sessionManager.appendCustomEntry(SUBSESSION_LINK_CUSTOM_TYPE, persistedParentSubsessionLinkData(link));
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
this.logger.info({ parentSessionId: link.parentSessionId, sessionId: link.childSessionId, error: error instanceof Error ? error.message : String(error) }, "failed to persist subsession link");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
persistSubsessionChildMarker(parentSessionId, childSessionId) {
|
|
450
|
+
const child = this.active.get(childSessionId)?.runtime.session;
|
|
451
|
+
if (child === undefined)
|
|
452
|
+
return;
|
|
453
|
+
if (child.sessionManager.appendCustomEntry === undefined)
|
|
454
|
+
return;
|
|
455
|
+
try {
|
|
456
|
+
child.sessionManager.appendCustomEntry(SUBSESSION_CHILD_LINK_CUSTOM_TYPE, persistedChildSubsessionLinkData(parentSessionId, childSessionId));
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
this.logger.info({ parentSessionId, sessionId: childSessionId, error: error instanceof Error ? error.message : String(error) }, "failed to persist subsession child marker");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async hydrateSubsessionsForParent(parentSessionId, parentSessionFile) {
|
|
463
|
+
const hydrationKey = subsessionHydratedParentKey(parentSessionId, parentSessionFile);
|
|
464
|
+
if (this.subsessionHydratedParents.has(hydrationKey))
|
|
465
|
+
return;
|
|
466
|
+
const activeParent = this.active.get(parentSessionId);
|
|
467
|
+
if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
|
|
468
|
+
const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
|
|
469
|
+
await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
470
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (parentSessionFile === undefined)
|
|
474
|
+
return;
|
|
475
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId) {
|
|
476
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
let parentManager;
|
|
480
|
+
try {
|
|
481
|
+
parentManager = this.sessionManager.open(parentSessionFile);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
|
|
488
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
489
|
+
}
|
|
490
|
+
async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile) {
|
|
491
|
+
// Parent custom links are the authoritative recovery record: verify the
|
|
492
|
+
// exact live child file/header before tracking.
|
|
493
|
+
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
494
|
+
for (const entry of entries) {
|
|
495
|
+
const link = parsePersistedParentSubsessionLink(entry);
|
|
496
|
+
if (link === undefined)
|
|
497
|
+
continue;
|
|
498
|
+
const verified = await this.verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link);
|
|
499
|
+
if (verified === undefined)
|
|
500
|
+
continue;
|
|
501
|
+
this.registerVerifiedSubsession(verified);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link) {
|
|
505
|
+
if (parentSessionFile === undefined)
|
|
506
|
+
return undefined;
|
|
507
|
+
if (link.spawnedBySessionId !== parentSessionId)
|
|
508
|
+
return undefined;
|
|
509
|
+
if (!(await this.parentLinkHasValidChildTarget(parentSessionFile, link)))
|
|
510
|
+
return undefined;
|
|
511
|
+
return trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile);
|
|
512
|
+
}
|
|
513
|
+
async parentLinkHasValidChildTarget(parentSessionFile, link) {
|
|
514
|
+
return link.spawnedSessionFile !== undefined
|
|
515
|
+
&& await sessionFileHeaderMatches(link.spawnedSessionFile, { sessionId: link.spawnedSessionId, parentSessionFile });
|
|
516
|
+
}
|
|
517
|
+
async recoverSubsessionTrackingForOpenedSession(session) {
|
|
518
|
+
const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
|
|
519
|
+
if (link === undefined)
|
|
520
|
+
return;
|
|
521
|
+
this.registerVerifiedSubsession(link);
|
|
522
|
+
}
|
|
523
|
+
verifiedSubsessionLinkFromOpenedChild(session) {
|
|
524
|
+
return verifiedTrackedSubsessionLink(this.sessionManager, {
|
|
525
|
+
sessionId: session.sessionId,
|
|
526
|
+
sessionFile: session.sessionFile,
|
|
527
|
+
sessionManager: session.sessionManager,
|
|
528
|
+
cwd: session.sessionManager.getCwd(),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
async getOrOpenTrackedSubsession(sessionId) {
|
|
532
|
+
const link = this.subsessionLinks.get(sessionId);
|
|
533
|
+
if (link === undefined)
|
|
534
|
+
throw new Error("Session not found");
|
|
535
|
+
const active = this.activeChildForSubsessionLink(link);
|
|
536
|
+
if (active !== undefined)
|
|
537
|
+
return active.runtime.session;
|
|
538
|
+
if (link.childSessionFile !== undefined) {
|
|
539
|
+
if (!(await sessionFileHeaderMatches(link.childSessionFile, { sessionId, parentSessionFile: link.parentSessionFile })))
|
|
540
|
+
throw new Error("Session not found");
|
|
541
|
+
const sessionManager = this.sessionManager.open(link.childSessionFile);
|
|
542
|
+
return (await this.create(sessionManager, link.cwd ?? sessionManager.getCwd())).runtime.session;
|
|
543
|
+
}
|
|
544
|
+
throw new Error("Session not found");
|
|
271
545
|
}
|
|
272
546
|
async subsessionSummaryFields(childSessionId) {
|
|
273
|
-
const
|
|
547
|
+
const link = this.subsessionLinks.get(childSessionId);
|
|
548
|
+
const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
|
|
274
549
|
if (active !== undefined) {
|
|
275
|
-
return { cwd: active.runtime.cwd, status:
|
|
550
|
+
return { cwd: active.runtime.cwd, status: this.subsessionStatus(active.runtime.session) };
|
|
276
551
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
552
|
+
if (link?.childSessionFile !== undefined && (await sessionFileHeaderMatches(link.childSessionFile, { sessionId: childSessionId, parentSessionFile: link.parentSessionFile }))) {
|
|
553
|
+
return { cwd: link.cwd ?? "", status: "idle" };
|
|
554
|
+
}
|
|
555
|
+
if (link?.cwd !== undefined)
|
|
556
|
+
return { cwd: link.cwd, status: "unknown" };
|
|
280
557
|
return { cwd: "", status: "unknown" };
|
|
281
558
|
}
|
|
282
|
-
|
|
283
|
-
if (await this.archiveStore.isArchived(session.sessionId))
|
|
284
|
-
return "archived";
|
|
559
|
+
subsessionStatus(session) {
|
|
285
560
|
if (this.hasActiveWork(session))
|
|
286
561
|
return "working";
|
|
287
562
|
if (this.activities.get(session.sessionId)?.phase === "error")
|
|
@@ -295,10 +570,10 @@ export class PiSessionService {
|
|
|
295
570
|
* parent is busy and delivers immediately when it is idle).
|
|
296
571
|
*/
|
|
297
572
|
updateSubsessionTracking(session) {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
if (parentId === undefined)
|
|
573
|
+
const link = this.subsessionLinkForActiveChild(session);
|
|
574
|
+
if (link === undefined)
|
|
301
575
|
return;
|
|
576
|
+
const childId = link.childSessionId;
|
|
302
577
|
if (this.hasActiveWork(session)) {
|
|
303
578
|
this.subsessionNotifyArmed.set(childId, true);
|
|
304
579
|
return;
|
|
@@ -309,8 +584,24 @@ export class PiSessionService {
|
|
|
309
584
|
const status = this.activities.get(childId)?.phase === "error" ? "error" : "idle";
|
|
310
585
|
const finalText = finalAssistantText(historyMessages(session));
|
|
311
586
|
const preview = finalText === "" ? "(no output)" : truncateForNotification(finalText);
|
|
312
|
-
const text = `Subsession ${childId} stopped working (status: ${status}). Latest output:\n\n${preview}\n\
|
|
313
|
-
void this.notifyParentOfSubsession(
|
|
587
|
+
const text = `Subsession ${childId} stopped working (status: ${status}). Latest output:\n\n${preview}\n\nStatus and latest output are available through check_subsession with sessionId "${childId}"; its full transcript is available through read_subsession.`;
|
|
588
|
+
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
|
589
|
+
}
|
|
590
|
+
async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
|
|
591
|
+
const link = this.subsessionLinks.get(childSessionId);
|
|
592
|
+
if (link?.parentSessionId !== parentSessionId)
|
|
593
|
+
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
594
|
+
const active = this.activeParentForSubsessionLink(link);
|
|
595
|
+
if (active !== undefined)
|
|
596
|
+
return active.runtime.session;
|
|
597
|
+
const parentSessionFile = link.parentSessionFile;
|
|
598
|
+
if (parentSessionFile === undefined)
|
|
599
|
+
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
600
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId) {
|
|
601
|
+
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
602
|
+
}
|
|
603
|
+
const sessionManager = this.sessionManager.open(parentSessionFile);
|
|
604
|
+
return (await this.create(sessionManager, sessionManager.getCwd())).runtime.session;
|
|
314
605
|
}
|
|
315
606
|
/**
|
|
316
607
|
* Deliver a subsession-completion notice to the parent as a system-authored
|
|
@@ -321,7 +612,7 @@ export class PiSessionService {
|
|
|
321
612
|
*/
|
|
322
613
|
async notifyParentOfSubsession(parentId, childId, text) {
|
|
323
614
|
try {
|
|
324
|
-
const session = await this.
|
|
615
|
+
const session = await this.getOrOpenParentForSubsession(parentId, childId);
|
|
325
616
|
await session.sendCustomMessage({ customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, content: text, display: true, details: { sessionId: childId } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
326
617
|
this.publishStatus(session);
|
|
327
618
|
}
|
|
@@ -459,7 +750,7 @@ export class PiSessionService {
|
|
|
459
750
|
this.publishStatus(session);
|
|
460
751
|
}
|
|
461
752
|
async saveAttachments(ref, attachments, folder) {
|
|
462
|
-
const parsed = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false });
|
|
753
|
+
const parsed = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false, allowFileAttachments: true });
|
|
463
754
|
if (parsed.length === 0)
|
|
464
755
|
return [];
|
|
465
756
|
await this.assertWritable(ref);
|
|
@@ -511,6 +802,23 @@ export class PiSessionService {
|
|
|
511
802
|
const active = await this.getActive(ref);
|
|
512
803
|
return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
|
|
513
804
|
}
|
|
805
|
+
async reloadSessionRuntime(session) {
|
|
806
|
+
if (this.hasActiveWork(session))
|
|
807
|
+
throw new Error("Stop current session activity before reloading");
|
|
808
|
+
this.publishActivity(session, "reloading resources", "active");
|
|
809
|
+
try {
|
|
810
|
+
await session.reload();
|
|
811
|
+
this.publishActivity(session, "resources reloaded", "idle");
|
|
812
|
+
this.publishStatus(session);
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
816
|
+
this.publishActivity(session, "reload failed", "error", message);
|
|
817
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
818
|
+
this.publishStatus(session);
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
514
822
|
async archive(ref) {
|
|
515
823
|
const session = await this.getOrOpen(ref);
|
|
516
824
|
if (this.hasActiveWork(session))
|
|
@@ -519,6 +827,69 @@ export class PiSessionService {
|
|
|
519
827
|
await this.closeActive(session.sessionId);
|
|
520
828
|
await this.archiveStore.archive(archiveInput);
|
|
521
829
|
}
|
|
830
|
+
async archiveMany(refs) {
|
|
831
|
+
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
832
|
+
const [archivedRecords, sessionContext] = await Promise.all([
|
|
833
|
+
this.archiveStore.list(),
|
|
834
|
+
this.bulkSessionLookupContext(uniqueRefs),
|
|
835
|
+
]);
|
|
836
|
+
const failures = [];
|
|
837
|
+
const alreadyArchivedSessionIds = [];
|
|
838
|
+
const planItems = [];
|
|
839
|
+
for (const ref of uniqueRefs) {
|
|
840
|
+
const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
841
|
+
if (archived !== undefined) {
|
|
842
|
+
alreadyArchivedSessionIds.push(archived.sessionId);
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
const active = this.activeForLookup(bulkRefToLookup(ref));
|
|
846
|
+
const listed = findListedSessionForBulkRef(sessionContext, ref);
|
|
847
|
+
const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
|
|
848
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
849
|
+
failures.push({ sessionId: resolvedSessionId, error: "Stop current session activity before archiving" });
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
if (listed !== undefined) {
|
|
854
|
+
planItems.push({ input: archiveInputFromListEntry(listed) });
|
|
855
|
+
}
|
|
856
|
+
else if (active !== undefined) {
|
|
857
|
+
planItems.push({ input: archiveInputFromActiveSession(active.runtime.session) });
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
failures.push({ sessionId: ref.id, error: "Session not found" });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
failures.push({ sessionId: resolvedSessionId, error: errorMessage(error) });
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const readyInputs = [];
|
|
868
|
+
for (const item of planItems) {
|
|
869
|
+
try {
|
|
870
|
+
await this.closeActive(item.input.sessionId);
|
|
871
|
+
readyInputs.push(item.input);
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
failures.push({ sessionId: item.input.sessionId, error: errorMessage(error) });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const archivedSessionIds = [...alreadyArchivedSessionIds];
|
|
878
|
+
try {
|
|
879
|
+
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
880
|
+
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
for (const input of readyInputs)
|
|
884
|
+
failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
|
|
885
|
+
}
|
|
886
|
+
return {
|
|
887
|
+
archived: true,
|
|
888
|
+
archivedSessionIds: uniqueStrings(archivedSessionIds),
|
|
889
|
+
failures,
|
|
890
|
+
generatedAt: new Date().toISOString(),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
522
893
|
async archiveTree(ref) {
|
|
523
894
|
const session = await this.getOrOpen(ref);
|
|
524
895
|
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
@@ -530,8 +901,7 @@ export class PiSessionService {
|
|
|
530
901
|
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
531
902
|
for (const input of archiveInputs)
|
|
532
903
|
await this.closeActive(input.sessionId);
|
|
533
|
-
|
|
534
|
-
await this.archiveStore.archive(input);
|
|
904
|
+
await this.archiveStoreArchiveMany(archiveInputs);
|
|
535
905
|
return {
|
|
536
906
|
archived: true,
|
|
537
907
|
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
@@ -557,6 +927,57 @@ export class PiSessionService {
|
|
|
557
927
|
await this.ensureArchivedRecordMoved(record);
|
|
558
928
|
await this.archiveStore.deleteArchived(record.sessionId);
|
|
559
929
|
}
|
|
930
|
+
async deleteArchivedMany(refs) {
|
|
931
|
+
if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
932
|
+
throw new Error("Archive store does not support deletion");
|
|
933
|
+
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
934
|
+
const archivedRecords = await this.archiveStore.list();
|
|
935
|
+
const failures = [];
|
|
936
|
+
const planItems = [];
|
|
937
|
+
for (const ref of uniqueRefs) {
|
|
938
|
+
const record = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
939
|
+
if (record === undefined) {
|
|
940
|
+
failures.push({ sessionId: ref.id, error: "Archived session not found" });
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const active = this.activeForLookup({ id: record.sessionId, cwd: record.cwd });
|
|
944
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
945
|
+
failures.push({ sessionId: record.sessionId, error: "Stop current session activity before deleting archived session" });
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
planItems.push({ record });
|
|
949
|
+
}
|
|
950
|
+
const readyRecords = [];
|
|
951
|
+
for (const item of planItems) {
|
|
952
|
+
try {
|
|
953
|
+
await this.closeActive(item.record.sessionId);
|
|
954
|
+
readyRecords.push(item.record);
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
failures.push({ sessionId: item.record.sessionId, error: errorMessage(error) });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const moveFailures = await this.moveLegacyArchivedRecordsForDelete(readyRecords);
|
|
961
|
+
failures.push(...moveFailures);
|
|
962
|
+
const moveFailureIds = new Set(moveFailures.map((failure) => failure.sessionId));
|
|
963
|
+
const deleteIds = readyRecords
|
|
964
|
+
.map((record) => record.sessionId)
|
|
965
|
+
.filter((sessionId) => !moveFailureIds.has(sessionId));
|
|
966
|
+
let deletedSessionIds = [];
|
|
967
|
+
try {
|
|
968
|
+
deletedSessionIds = await this.archiveStoreDeleteArchivedMany(deleteIds);
|
|
969
|
+
}
|
|
970
|
+
catch (error) {
|
|
971
|
+
for (const sessionId of deleteIds)
|
|
972
|
+
failures.push({ sessionId, error: errorMessage(error) });
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
deleted: true,
|
|
976
|
+
deletedSessionIds,
|
|
977
|
+
failures,
|
|
978
|
+
generatedAt: new Date().toISOString(),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
560
981
|
async reload(ref) {
|
|
561
982
|
await this.assertWritable(ref);
|
|
562
983
|
const session = await this.getOrOpen(ref);
|
|
@@ -572,6 +993,8 @@ export class PiSessionService {
|
|
|
572
993
|
if (sessionFile === undefined || sessionFile === "")
|
|
573
994
|
throw new Error("Session is not persisted");
|
|
574
995
|
await clearParentSession(sessionFile);
|
|
996
|
+
clearParentSessionHeader(session.sessionManager);
|
|
997
|
+
this.unregisterSubsession(session.sessionId);
|
|
575
998
|
}
|
|
576
999
|
async abort(ref) {
|
|
577
1000
|
const active = this.activeForLookup(ref);
|
|
@@ -592,6 +1015,96 @@ export class PiSessionService {
|
|
|
592
1015
|
// Best-effort shutdown; callers that need errors await closeActive directly.
|
|
593
1016
|
});
|
|
594
1017
|
}
|
|
1018
|
+
async bulkSessionLookupContext(refs) {
|
|
1019
|
+
const cwdSet = new Set();
|
|
1020
|
+
let needsAllSessions = false;
|
|
1021
|
+
for (const ref of refs) {
|
|
1022
|
+
if (ref.cwd === undefined)
|
|
1023
|
+
needsAllSessions = true;
|
|
1024
|
+
else
|
|
1025
|
+
cwdSet.add(ref.cwd);
|
|
1026
|
+
}
|
|
1027
|
+
const [sessionsByCwd, allSessions] = await Promise.all([
|
|
1028
|
+
this.listSessionsByCwd([...cwdSet]),
|
|
1029
|
+
needsAllSessions ? this.sessionManager.listAll?.() ?? Promise.resolve([]) : Promise.resolve(undefined),
|
|
1030
|
+
]);
|
|
1031
|
+
return allSessions === undefined ? { sessionsByCwd } : { sessionsByCwd, allSessions };
|
|
1032
|
+
}
|
|
1033
|
+
async listSessionsByCwd(cwds) {
|
|
1034
|
+
const uniqueCwds = uniqueStrings(cwds);
|
|
1035
|
+
const entries = await Promise.all(uniqueCwds.map(async (cwd) => [cwd, await this.sessionManager.list(cwd)]));
|
|
1036
|
+
return new Map(entries);
|
|
1037
|
+
}
|
|
1038
|
+
async archiveStoreArchiveMany(inputs) {
|
|
1039
|
+
if (inputs.length === 0)
|
|
1040
|
+
return [];
|
|
1041
|
+
if (this.archiveStore.archiveMany !== undefined)
|
|
1042
|
+
return this.archiveStore.archiveMany(inputs);
|
|
1043
|
+
const records = [];
|
|
1044
|
+
for (const input of inputs)
|
|
1045
|
+
records.push(await this.archiveStore.archive(input));
|
|
1046
|
+
return records;
|
|
1047
|
+
}
|
|
1048
|
+
async archiveStoreDeleteArchivedMany(sessionIds) {
|
|
1049
|
+
if (sessionIds.length === 0)
|
|
1050
|
+
return [];
|
|
1051
|
+
if (this.archiveStore.deleteArchivedMany !== undefined)
|
|
1052
|
+
return this.archiveStore.deleteArchivedMany(sessionIds);
|
|
1053
|
+
if (this.archiveStore.deleteArchived === undefined)
|
|
1054
|
+
throw new Error("Archive store does not support deletion");
|
|
1055
|
+
for (const sessionId of sessionIds)
|
|
1056
|
+
await this.archiveStore.deleteArchived(sessionId);
|
|
1057
|
+
return [...sessionIds];
|
|
1058
|
+
}
|
|
1059
|
+
async moveLegacyArchivedRecordsForDelete(records) {
|
|
1060
|
+
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
1061
|
+
if (legacyRecords.length === 0)
|
|
1062
|
+
return [];
|
|
1063
|
+
let sessionsByCwd;
|
|
1064
|
+
try {
|
|
1065
|
+
sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1066
|
+
}
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
return legacyRecords.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
1069
|
+
}
|
|
1070
|
+
const moveInputs = legacyRecords
|
|
1071
|
+
.map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
|
|
1072
|
+
.filter(isDefined)
|
|
1073
|
+
.map(archiveInputFromListEntry);
|
|
1074
|
+
if (moveInputs.length === 0)
|
|
1075
|
+
return [];
|
|
1076
|
+
try {
|
|
1077
|
+
await this.archiveStoreArchiveMany(moveInputs);
|
|
1078
|
+
return [];
|
|
1079
|
+
}
|
|
1080
|
+
catch (error) {
|
|
1081
|
+
const failedIds = new Set(moveInputs.map((input) => input.sessionId));
|
|
1082
|
+
return legacyRecords
|
|
1083
|
+
.filter((record) => failedIds.has(record.sessionId))
|
|
1084
|
+
.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
async cleanupPlan(request) {
|
|
1088
|
+
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll?.() ?? [], this.archiveStore.list()]);
|
|
1089
|
+
return planSessionCleanup({
|
|
1090
|
+
sessions,
|
|
1091
|
+
archivedRecords,
|
|
1092
|
+
activeSessions: this.cleanupActiveSessionStatuses(),
|
|
1093
|
+
thresholds: request.thresholds,
|
|
1094
|
+
...(request.projectCwds === undefined ? {} : { projectCwds: request.projectCwds }),
|
|
1095
|
+
now: this.now(),
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
cleanupActiveSessionStatuses() {
|
|
1099
|
+
return [...new Set(this.active.values())].map((active) => ({
|
|
1100
|
+
sessionId: active.runtime.session.sessionId,
|
|
1101
|
+
hasActiveWork: this.hasActiveWork(active.runtime.session),
|
|
1102
|
+
}));
|
|
1103
|
+
}
|
|
1104
|
+
activeSessionHasWork(sessionId) {
|
|
1105
|
+
const active = this.active.get(sessionId);
|
|
1106
|
+
return active !== undefined && this.hasActiveWork(active.runtime.session);
|
|
1107
|
+
}
|
|
595
1108
|
reconcilableSessionIds(cwd, listedSessionIds, archivedById) {
|
|
596
1109
|
const sessionIds = new Set(listedSessionIds);
|
|
597
1110
|
for (const active of new Set(this.active.values())) {
|
|
@@ -615,7 +1128,19 @@ export class PiSessionService {
|
|
|
615
1128
|
const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
|
|
616
1129
|
if (session === undefined)
|
|
617
1130
|
return record;
|
|
618
|
-
|
|
1131
|
+
const [moved] = await this.archiveStoreArchiveMany([archiveInputFromListEntry(session)]);
|
|
1132
|
+
return moved ?? record;
|
|
1133
|
+
}
|
|
1134
|
+
async ensureArchivedRecordsMoved(records) {
|
|
1135
|
+
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
1136
|
+
if (legacyRecords.length === 0)
|
|
1137
|
+
return;
|
|
1138
|
+
const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1139
|
+
const moveInputs = legacyRecords
|
|
1140
|
+
.map((record) => sessionsByCwd.get(record.cwd)?.find((candidate) => candidate.id === record.sessionId))
|
|
1141
|
+
.filter(isDefined)
|
|
1142
|
+
.map(archiveInputFromListEntry);
|
|
1143
|
+
await this.archiveStoreArchiveMany(moveInputs);
|
|
619
1144
|
}
|
|
620
1145
|
async archiveInputForSession(session) {
|
|
621
1146
|
const cwd = session.sessionManager.getCwd();
|
|
@@ -678,6 +1203,9 @@ export class PiSessionService {
|
|
|
678
1203
|
return [...names];
|
|
679
1204
|
}
|
|
680
1205
|
async closeActive(sessionId) {
|
|
1206
|
+
const pendingOpens = this.pendingSessionOpenPromises(sessionId);
|
|
1207
|
+
if (pendingOpens.length > 0)
|
|
1208
|
+
await Promise.allSettled(pendingOpens);
|
|
681
1209
|
const active = this.active.get(sessionId);
|
|
682
1210
|
if (!active)
|
|
683
1211
|
return;
|
|
@@ -689,7 +1217,8 @@ export class PiSessionService {
|
|
|
689
1217
|
// Disarm subsession notification before teardown so the abort below cannot
|
|
690
1218
|
// emit a "stopped working" event that notifies the parent (e.g. on archive).
|
|
691
1219
|
// The parent/children link is kept so the parent can still see the child.
|
|
692
|
-
this.
|
|
1220
|
+
if (this.subsessionLinkForActiveChild(active.runtime.session) !== undefined)
|
|
1221
|
+
this.subsessionNotifyArmed.delete(sessionId);
|
|
693
1222
|
clearSessionQueue(active.runtime.session);
|
|
694
1223
|
active.unsubscribe();
|
|
695
1224
|
try {
|
|
@@ -711,14 +1240,40 @@ export class PiSessionService {
|
|
|
711
1240
|
if (active !== undefined)
|
|
712
1241
|
return active;
|
|
713
1242
|
const archived = await this.getArchived(ref);
|
|
714
|
-
if (archived?.archivePath !== undefined)
|
|
715
|
-
|
|
1243
|
+
if (archived?.archivePath !== undefined) {
|
|
1244
|
+
const { archivePath } = archived;
|
|
1245
|
+
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath));
|
|
1246
|
+
}
|
|
716
1247
|
const match = isPiSessionRef(ref)
|
|
717
1248
|
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
718
1249
|
: (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
719
1250
|
if (!match)
|
|
720
1251
|
throw new Error("Session not found");
|
|
721
|
-
return this.
|
|
1252
|
+
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path));
|
|
1253
|
+
}
|
|
1254
|
+
openExistingSession(sessionId, cwd, openSessionManager) {
|
|
1255
|
+
const active = this.activeForLookup({ id: sessionId, cwd });
|
|
1256
|
+
if (active !== undefined)
|
|
1257
|
+
return Promise.resolve(active);
|
|
1258
|
+
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
|
|
1259
|
+
const existing = this.pendingSessionOpens.get(key);
|
|
1260
|
+
if (existing !== undefined)
|
|
1261
|
+
return existing.promise;
|
|
1262
|
+
const pending = {
|
|
1263
|
+
sessionId,
|
|
1264
|
+
promise: this.create(openSessionManager(), cwd),
|
|
1265
|
+
};
|
|
1266
|
+
pending.promise = pending.promise.finally(() => {
|
|
1267
|
+
if (this.pendingSessionOpens.get(key) === pending)
|
|
1268
|
+
this.pendingSessionOpens.delete(key);
|
|
1269
|
+
});
|
|
1270
|
+
this.pendingSessionOpens.set(key, pending);
|
|
1271
|
+
return pending.promise;
|
|
1272
|
+
}
|
|
1273
|
+
pendingSessionOpenPromises(sessionId) {
|
|
1274
|
+
return [...this.pendingSessionOpens.values()]
|
|
1275
|
+
.filter((pending) => sessionId === undefined || pending.sessionId === sessionId)
|
|
1276
|
+
.map((pending) => pending.promise);
|
|
722
1277
|
}
|
|
723
1278
|
async getArchived(ref) {
|
|
724
1279
|
const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
|
|
@@ -739,18 +1294,53 @@ export class PiSessionService {
|
|
|
739
1294
|
}
|
|
740
1295
|
return undefined;
|
|
741
1296
|
}
|
|
742
|
-
async create(sessionManager, cwd) {
|
|
743
|
-
const
|
|
744
|
-
|
|
1297
|
+
async create(sessionManager, cwd, options = {}) {
|
|
1298
|
+
const delegationToolsEnabled = options.creationProvenance !== "tracked-subsession"
|
|
1299
|
+
&& await sessionAllowsDelegationTools(sessionManager, this.sessionManager);
|
|
1300
|
+
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
1301
|
+
cwd,
|
|
1302
|
+
agentDir: this.agentDir,
|
|
1303
|
+
sessionManager,
|
|
1304
|
+
delegationToolsEnabled,
|
|
1305
|
+
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
1306
|
+
});
|
|
745
1307
|
const active = { runtime, unsubscribe: noop };
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
await this.bindSessionExtensions(session);
|
|
1308
|
+
try {
|
|
1309
|
+
await this.bindSessionExtensions(runtime.session);
|
|
749
1310
|
this.bindRuntime(active);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1311
|
+
runtime.setRebindSession(async (session) => {
|
|
1312
|
+
await this.bindSessionExtensions(session);
|
|
1313
|
+
this.bindRuntime(active);
|
|
1314
|
+
await this.recoverSubsessionTrackingForOpenedSession(session);
|
|
1315
|
+
});
|
|
1316
|
+
this.active.set(runtime.session.sessionId, active);
|
|
1317
|
+
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
1318
|
+
this.publishStatus(runtime.session);
|
|
1319
|
+
return active;
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
active.unsubscribe();
|
|
1323
|
+
let removedActive = false;
|
|
1324
|
+
for (const [sessionId, candidate] of this.active.entries()) {
|
|
1325
|
+
if (candidate !== active)
|
|
1326
|
+
continue;
|
|
1327
|
+
this.active.delete(sessionId);
|
|
1328
|
+
this.activities.delete(sessionId);
|
|
1329
|
+
this.clearAuthLossWarningsForSession(sessionId);
|
|
1330
|
+
this.clearCompactionPromptQueue(sessionId);
|
|
1331
|
+
removedActive = true;
|
|
1332
|
+
}
|
|
1333
|
+
if (removedActive) {
|
|
1334
|
+
this.workspaceActivity?.removeSession(runtime.session.sessionId, runtime.session.sessionManager.getCwd());
|
|
1335
|
+
}
|
|
1336
|
+
try {
|
|
1337
|
+
await runtime.session.abort();
|
|
1338
|
+
}
|
|
1339
|
+
finally {
|
|
1340
|
+
await runtime.dispose();
|
|
1341
|
+
}
|
|
1342
|
+
throw error;
|
|
1343
|
+
}
|
|
754
1344
|
}
|
|
755
1345
|
async bindSessionExtensions(session) {
|
|
756
1346
|
await session.bindExtensions({
|
|
@@ -846,10 +1436,15 @@ export class PiSessionService {
|
|
|
846
1436
|
maybeGenerateSessionName(session, firstMessage) {
|
|
847
1437
|
if (session.sessionName !== undefined || session.messages.length !== 0 || session.isStreaming || session.isCompacting)
|
|
848
1438
|
return;
|
|
1439
|
+
const deterministicName = deterministicSessionName(firstMessage);
|
|
1440
|
+
if (deterministicName !== undefined) {
|
|
1441
|
+
this.applyGeneratedSessionName(session, deterministicName);
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
849
1444
|
const model = session.model;
|
|
850
1445
|
if (model === undefined)
|
|
851
1446
|
return;
|
|
852
|
-
void generateShortSessionName(
|
|
1447
|
+
void generateShortSessionName(session.agent.streamFn, model, firstMessage).then((name) => {
|
|
853
1448
|
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
854
1449
|
}).catch(() => {
|
|
855
1450
|
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
|
|
@@ -1026,6 +1621,7 @@ export class PiSessionService {
|
|
|
1026
1621
|
const contextUsage = session.getContextUsage();
|
|
1027
1622
|
return {
|
|
1028
1623
|
sessionId: session.sessionId,
|
|
1624
|
+
persisted: sessionFileExists(session.sessionFile),
|
|
1029
1625
|
...(model === undefined ? {} : { model }),
|
|
1030
1626
|
thinkingLevel: session.thinkingLevel,
|
|
1031
1627
|
isStreaming: session.isStreaming,
|
|
@@ -1049,6 +1645,47 @@ export class PiSessionService {
|
|
|
1049
1645
|
return queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)).some((message) => message.text === text);
|
|
1050
1646
|
}
|
|
1051
1647
|
}
|
|
1648
|
+
function previewResponseFromPlan(plan) {
|
|
1649
|
+
return {
|
|
1650
|
+
generatedAt: plan.generatedAt,
|
|
1651
|
+
thresholds: plan.thresholds,
|
|
1652
|
+
projects: plan.projects,
|
|
1653
|
+
totals: plan.totals,
|
|
1654
|
+
...(plan.skippedBusySessionIds.length === 0 ? {} : { skippedBusySessionIds: plan.skippedBusySessionIds }),
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
function uniqueBulkSessionRefs(refs) {
|
|
1658
|
+
const seen = new Set();
|
|
1659
|
+
const unique = [];
|
|
1660
|
+
for (const ref of refs) {
|
|
1661
|
+
const key = `${ref.cwd ?? ""}\0${ref.id}`;
|
|
1662
|
+
if (seen.has(key))
|
|
1663
|
+
continue;
|
|
1664
|
+
seen.add(key);
|
|
1665
|
+
unique.push(ref);
|
|
1666
|
+
}
|
|
1667
|
+
return unique;
|
|
1668
|
+
}
|
|
1669
|
+
function bulkRefToLookup(ref) {
|
|
1670
|
+
return ref.cwd === undefined ? ref.id : { id: ref.id, cwd: ref.cwd };
|
|
1671
|
+
}
|
|
1672
|
+
function findArchivedRecordForBulkRef(records, ref) {
|
|
1673
|
+
return records.find((record) => (ref.cwd === undefined || record.cwd === ref.cwd) && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
|
|
1674
|
+
}
|
|
1675
|
+
function findListedSessionForBulkRef(context, ref) {
|
|
1676
|
+
if (ref.cwd !== undefined)
|
|
1677
|
+
return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
1678
|
+
return context.allSessions === undefined ? undefined : findSessionByIdOrPrefix(context.allSessions, ref.id);
|
|
1679
|
+
}
|
|
1680
|
+
function findSessionByIdOrPrefix(sessions, sessionId) {
|
|
1681
|
+
return sessions.find((session) => session.id === sessionId) ?? sessions.find((session) => session.id.startsWith(sessionId));
|
|
1682
|
+
}
|
|
1683
|
+
function uniqueStrings(values) {
|
|
1684
|
+
return [...new Set(values)];
|
|
1685
|
+
}
|
|
1686
|
+
function errorMessage(error) {
|
|
1687
|
+
return error instanceof Error ? error.message : String(error);
|
|
1688
|
+
}
|
|
1052
1689
|
function modelToClientModel(model) {
|
|
1053
1690
|
if (model === undefined)
|
|
1054
1691
|
return {};
|
|
@@ -1067,6 +1704,7 @@ function clientSessionFromListEntry(session) {
|
|
|
1067
1704
|
id: session.id,
|
|
1068
1705
|
path: session.path,
|
|
1069
1706
|
cwd: session.cwd,
|
|
1707
|
+
persisted: true,
|
|
1070
1708
|
...(session.name === undefined ? {} : { name: session.name }),
|
|
1071
1709
|
created: session.created.toISOString(),
|
|
1072
1710
|
modified: session.modified.toISOString(),
|
|
@@ -1195,6 +1833,191 @@ function archivedTimestamp(record) {
|
|
|
1195
1833
|
function isDefined(value) {
|
|
1196
1834
|
return value !== undefined;
|
|
1197
1835
|
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Resolve the delegation capability from server-owned, persisted session
|
|
1838
|
+
* provenance. A copied marker is not enough: the child header and reciprocal
|
|
1839
|
+
* parent link must identify the exact same session files.
|
|
1840
|
+
*/
|
|
1841
|
+
export async function sessionAllowsDelegationTools(sessionManager, managers) {
|
|
1842
|
+
const trackedLink = await verifiedTrackedSubsessionLink(managers, {
|
|
1843
|
+
sessionId: sessionManager.getSessionId(),
|
|
1844
|
+
sessionFile: sessionManager.getSessionFile(),
|
|
1845
|
+
sessionManager,
|
|
1846
|
+
cwd: sessionManager.getCwd(),
|
|
1847
|
+
});
|
|
1848
|
+
return trackedLink === undefined;
|
|
1849
|
+
}
|
|
1850
|
+
async function verifiedTrackedSubsessionLink(managers, session) {
|
|
1851
|
+
// Child markers are only hints; the current child header and reciprocal
|
|
1852
|
+
// parent custom link must agree on the exact ids and files before relinking.
|
|
1853
|
+
const entries = session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch();
|
|
1854
|
+
let marker;
|
|
1855
|
+
for (const entry of entries) {
|
|
1856
|
+
const parsed = parsePersistedChildSubsessionLink(entry);
|
|
1857
|
+
if (parsed?.spawnedSessionId === session.sessionId)
|
|
1858
|
+
marker = parsed;
|
|
1859
|
+
}
|
|
1860
|
+
if (marker === undefined)
|
|
1861
|
+
return undefined;
|
|
1862
|
+
const childSessionFile = nonEmptyString(session.sessionFile);
|
|
1863
|
+
if (childSessionFile === undefined)
|
|
1864
|
+
return undefined;
|
|
1865
|
+
const childHeader = await readSessionHeaderSummary(childSessionFile);
|
|
1866
|
+
if (childHeader?.id !== session.sessionId)
|
|
1867
|
+
return undefined;
|
|
1868
|
+
const parentSessionFile = nonEmptyString(childHeader.parentSession);
|
|
1869
|
+
if (parentSessionFile === undefined)
|
|
1870
|
+
return undefined;
|
|
1871
|
+
const parentHeader = await readSessionHeaderSummary(parentSessionFile);
|
|
1872
|
+
if (parentHeader?.id !== marker.spawnedBySessionId)
|
|
1873
|
+
return undefined;
|
|
1874
|
+
const parentLink = findReciprocalParentSubsessionLink(managers, parentSessionFile, marker.spawnedBySessionId, session.sessionId, childSessionFile);
|
|
1875
|
+
if (parentLink === undefined)
|
|
1876
|
+
return undefined;
|
|
1877
|
+
return {
|
|
1878
|
+
parentSessionId: marker.spawnedBySessionId,
|
|
1879
|
+
childSessionId: session.sessionId,
|
|
1880
|
+
childSessionFile,
|
|
1881
|
+
parentSessionFile,
|
|
1882
|
+
cwd: parentLink.cwd ?? session.cwd,
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
function findReciprocalParentSubsessionLink(managers, parentSessionFile, parentSessionId, childSessionId, childSessionFile) {
|
|
1886
|
+
let parentManager;
|
|
1887
|
+
try {
|
|
1888
|
+
parentManager = managers.open(parentSessionFile);
|
|
1889
|
+
}
|
|
1890
|
+
catch {
|
|
1891
|
+
return undefined;
|
|
1892
|
+
}
|
|
1893
|
+
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
1894
|
+
for (const entry of entries) {
|
|
1895
|
+
const link = parsePersistedParentSubsessionLink(entry);
|
|
1896
|
+
if (link === undefined)
|
|
1897
|
+
continue;
|
|
1898
|
+
if (link.spawnedBySessionId !== parentSessionId || link.spawnedSessionId !== childSessionId)
|
|
1899
|
+
continue;
|
|
1900
|
+
if (link.spawnedSessionFile === undefined || !sessionPathsEqual(link.spawnedSessionFile, childSessionFile))
|
|
1901
|
+
continue;
|
|
1902
|
+
return link;
|
|
1903
|
+
}
|
|
1904
|
+
return undefined;
|
|
1905
|
+
}
|
|
1906
|
+
function trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile) {
|
|
1907
|
+
return {
|
|
1908
|
+
parentSessionId,
|
|
1909
|
+
childSessionId: link.spawnedSessionId,
|
|
1910
|
+
...(link.spawnedSessionFile === undefined ? {} : { childSessionFile: link.spawnedSessionFile }),
|
|
1911
|
+
parentSessionFile,
|
|
1912
|
+
...(link.cwd === undefined ? {} : { cwd: link.cwd }),
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
function persistedParentSubsessionLinkData(link) {
|
|
1916
|
+
return {
|
|
1917
|
+
version: 1,
|
|
1918
|
+
spawnedBySessionId: link.parentSessionId,
|
|
1919
|
+
spawnedSessionId: link.childSessionId,
|
|
1920
|
+
...(link.childSessionFile === undefined ? {} : { spawnedSessionFile: link.childSessionFile }),
|
|
1921
|
+
...(link.cwd === undefined ? {} : { cwd: link.cwd }),
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
function persistedChildSubsessionLinkData(parentSessionId, childSessionId) {
|
|
1925
|
+
return {
|
|
1926
|
+
version: 1,
|
|
1927
|
+
spawnedBySessionId: parentSessionId,
|
|
1928
|
+
spawnedSessionId: childSessionId,
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
function parsePersistedParentSubsessionLink(entry) {
|
|
1932
|
+
if (!isRecord(entry) || entry["type"] !== "custom" || entry["customType"] !== SUBSESSION_LINK_CUSTOM_TYPE)
|
|
1933
|
+
return undefined;
|
|
1934
|
+
const data = entry["data"];
|
|
1935
|
+
if (!isRecord(data))
|
|
1936
|
+
return undefined;
|
|
1937
|
+
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
1938
|
+
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
1939
|
+
if (spawnedBySessionId === undefined || spawnedBySessionId === "" || spawnedSessionId === undefined || spawnedSessionId === "")
|
|
1940
|
+
return undefined;
|
|
1941
|
+
const spawnedSessionFile = getString(data, "spawnedSessionFile");
|
|
1942
|
+
const cwd = getString(data, "cwd");
|
|
1943
|
+
return {
|
|
1944
|
+
spawnedBySessionId,
|
|
1945
|
+
spawnedSessionId,
|
|
1946
|
+
...(spawnedSessionFile === undefined || spawnedSessionFile === "" ? {} : { spawnedSessionFile }),
|
|
1947
|
+
...(cwd === undefined || cwd === "" ? {} : { cwd }),
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
function parsePersistedChildSubsessionLink(entry) {
|
|
1951
|
+
if (!isRecord(entry) || entry["type"] !== "custom" || entry["customType"] !== SUBSESSION_CHILD_LINK_CUSTOM_TYPE)
|
|
1952
|
+
return undefined;
|
|
1953
|
+
const data = entry["data"];
|
|
1954
|
+
if (!isRecord(data))
|
|
1955
|
+
return undefined;
|
|
1956
|
+
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
1957
|
+
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
1958
|
+
if (spawnedBySessionId === undefined || spawnedBySessionId === "" || spawnedSessionId === undefined || spawnedSessionId === "")
|
|
1959
|
+
return undefined;
|
|
1960
|
+
return { spawnedBySessionId, spawnedSessionId };
|
|
1961
|
+
}
|
|
1962
|
+
function nonEmptyString(value) {
|
|
1963
|
+
return value === undefined || value === "" ? undefined : value;
|
|
1964
|
+
}
|
|
1965
|
+
function subsessionHydratedParentKey(parentSessionId, parentSessionFile) {
|
|
1966
|
+
return `${parentSessionId}\0${parentSessionFile ?? ""}`;
|
|
1967
|
+
}
|
|
1968
|
+
function sessionPathsEqual(a, b) {
|
|
1969
|
+
return cwdPathsEqual(a, b);
|
|
1970
|
+
}
|
|
1971
|
+
function sessionFileExists(sessionFile) {
|
|
1972
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
1973
|
+
return false;
|
|
1974
|
+
try {
|
|
1975
|
+
return statSync(sessionFile).isFile();
|
|
1976
|
+
}
|
|
1977
|
+
catch {
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
function sessionFileMatches(session, expectedSessionFile) {
|
|
1982
|
+
const sessionFile = nonEmptyString(session.sessionFile);
|
|
1983
|
+
return sessionFile !== undefined && expectedSessionFile !== undefined && sessionPathsEqual(sessionFile, expectedSessionFile);
|
|
1984
|
+
}
|
|
1985
|
+
function activeSessionFileMatches(active, expectedSessionFile) {
|
|
1986
|
+
return sessionFileMatches(active.runtime.session, expectedSessionFile);
|
|
1987
|
+
}
|
|
1988
|
+
function trackedLinkParentFileMatches(link, parentSessionFile) {
|
|
1989
|
+
return link.parentSessionFile !== undefined && sessionPathsEqual(link.parentSessionFile, parentSessionFile);
|
|
1990
|
+
}
|
|
1991
|
+
async function readSessionHeaderSummary(sessionFile) {
|
|
1992
|
+
let file;
|
|
1993
|
+
try {
|
|
1994
|
+
file = await open(sessionFile, "r");
|
|
1995
|
+
const buffer = Buffer.alloc(4096);
|
|
1996
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
1997
|
+
const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n", 1)[0];
|
|
1998
|
+
if (firstLine === undefined || firstLine === "")
|
|
1999
|
+
return undefined;
|
|
2000
|
+
const header = JSON.parse(firstLine);
|
|
2001
|
+
if (!isRecord(header) || header["type"] !== "session" || typeof header["id"] !== "string")
|
|
2002
|
+
return undefined;
|
|
2003
|
+
const parentSession = getString(header, "parentSession");
|
|
2004
|
+
return { id: header["id"], ...(parentSession === undefined ? {} : { parentSession }) };
|
|
2005
|
+
}
|
|
2006
|
+
catch {
|
|
2007
|
+
return undefined;
|
|
2008
|
+
}
|
|
2009
|
+
finally {
|
|
2010
|
+
await file?.close().catch(() => undefined);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
async function sessionFileHeaderMatches(sessionFile, expected) {
|
|
2014
|
+
const header = await readSessionHeaderSummary(sessionFile);
|
|
2015
|
+
if (header?.id !== expected.sessionId)
|
|
2016
|
+
return false;
|
|
2017
|
+
if (expected.parentSessionFile === undefined)
|
|
2018
|
+
return true;
|
|
2019
|
+
return header.parentSession !== undefined && sessionPathsEqual(header.parentSession, expected.parentSessionFile);
|
|
2020
|
+
}
|
|
1198
2021
|
async function clearParentSession(sessionFile) {
|
|
1199
2022
|
const content = await readFile(sessionFile, "utf8");
|
|
1200
2023
|
const newlineIndex = content.indexOf("\n");
|
|
@@ -1208,6 +2031,11 @@ async function clearParentSession(sessionFile) {
|
|
|
1208
2031
|
delete header["parentSession"];
|
|
1209
2032
|
await writeFile(sessionFile, `${JSON.stringify(header)}${rest}`, "utf8");
|
|
1210
2033
|
}
|
|
2034
|
+
function clearParentSessionHeader(sessionManager) {
|
|
2035
|
+
const header = sessionManager.getHeader?.();
|
|
2036
|
+
if (header !== undefined && header !== null)
|
|
2037
|
+
delete header.parentSession;
|
|
2038
|
+
}
|
|
1211
2039
|
function clearSessionQueue(session) {
|
|
1212
2040
|
session.clearQueue();
|
|
1213
2041
|
}
|
|
@@ -1262,6 +2090,10 @@ function historyMessages(session) {
|
|
|
1262
2090
|
}
|
|
1263
2091
|
return messages;
|
|
1264
2092
|
}
|
|
2093
|
+
/** custom entry type used to persist parent -> child subsession links outside LLM context. */
|
|
2094
|
+
const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
|
|
2095
|
+
/** custom entry type used to mark a child as created by spawn_subsession. */
|
|
2096
|
+
const SUBSESSION_CHILD_LINK_CUSTOM_TYPE = "pi-web.subsession.spawned";
|
|
1265
2097
|
/** customType marking a parent-facing subsession-completion notice. */
|
|
1266
2098
|
const SUBSESSION_NOTIFICATION_CUSTOM_TYPE = "subsession.completion";
|
|
1267
2099
|
const SUBSESSION_NOTIFICATION_PREVIEW_CHARS = 2000;
|