@jmfederico/pi-web 1.202606.7 → 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 +9 -3
- 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/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 +1 -1
- package/dist/server/app.js +16 -1
- 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 +108 -1
- 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/machineProxyRoutes.js +64 -11
- 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/builtinCommands.js +1 -1
- package/dist/server/sessions/builtinCommands.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +503 -104
- 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/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 +38 -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 +22 -15
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/workspaces/fileSuggestions.js +7 -3
- package/dist/server/workspaces/fileSuggestions.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 +63 -1
- package/dist/shared/apiTypes.js +4 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +28 -2
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +10 -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/thinkingLevels.d.ts +1 -1
- package/dist/shared/thinkingLevels.js +1 -1
- package/dist/shared/thinkingLevels.js.map +1 -1
- package/docs/config.md +24 -13
- package/docs/plugins.md +28 -14
- package/package.json +35 -31
- 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,3 +1,4 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
1
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";
|
|
@@ -5,12 +6,12 @@ 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";
|
|
@@ -51,20 +52,50 @@ function parsePromptStreamingBehavior(value) {
|
|
|
51
52
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
52
53
|
if (!(options.sessionManager instanceof SessionManager))
|
|
53
54
|
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
54
|
-
|
|
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
|
+
};
|
|
78
|
+
}
|
|
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
|
+
];
|
|
55
85
|
}
|
|
56
|
-
function createDefaultRuntimeFactory(authStorage, modelRegistry, spawn, subsessions) {
|
|
57
|
-
return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
86
|
+
function createDefaultRuntimeFactory(authStorage, modelRegistry, sessionManagers, spawn, subsessions) {
|
|
87
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => {
|
|
58
88
|
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
});
|
|
68
99
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
69
100
|
};
|
|
70
101
|
}
|
|
@@ -93,6 +124,7 @@ export class PiSessionService {
|
|
|
93
124
|
constructor(events, deps = {}) {
|
|
94
125
|
this.events = events;
|
|
95
126
|
this.active = new Map();
|
|
127
|
+
this.pendingSessionOpens = new Map();
|
|
96
128
|
this.activities = new Map();
|
|
97
129
|
this.compactionPromptQueues = new Map();
|
|
98
130
|
this.compactionDrainTimers = new Map();
|
|
@@ -121,7 +153,7 @@ export class PiSessionService {
|
|
|
121
153
|
// Subsessions are a beta capability gated behind their own flag, and they
|
|
122
154
|
// also require the spawn capability (they share its project-scope resolver).
|
|
123
155
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
124
|
-
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 : {
|
|
125
157
|
spawn: (input) => this.spawnSubsession(input),
|
|
126
158
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
127
159
|
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
@@ -139,6 +171,7 @@ export class PiSessionService {
|
|
|
139
171
|
this.publishActivity(session, result === "success" ? "compaction complete" : "compaction failed", result === "success" ? "idle" : "error", detail);
|
|
140
172
|
this.publishStatus(session);
|
|
141
173
|
},
|
|
174
|
+
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
142
175
|
}, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
|
|
143
176
|
}
|
|
144
177
|
activeCount() {
|
|
@@ -149,10 +182,12 @@ export class PiSessionService {
|
|
|
149
182
|
}
|
|
150
183
|
async cleanup(request) {
|
|
151
184
|
const plan = await this.cleanupPlan(request);
|
|
152
|
-
if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined)
|
|
185
|
+
if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
153
186
|
throw new Error("Archive store does not support deletion");
|
|
154
187
|
const archiveInputs = [];
|
|
188
|
+
const readyArchiveInputs = [];
|
|
155
189
|
const deleteRecords = [];
|
|
190
|
+
const readyDeleteRecords = [];
|
|
156
191
|
const skippedBusySessionIds = new Set(plan.skippedBusySessionIds);
|
|
157
192
|
for (const input of plan.archiveInputs) {
|
|
158
193
|
if (this.activeSessionHasWork(input.sessionId)) {
|
|
@@ -160,20 +195,21 @@ export class PiSessionService {
|
|
|
160
195
|
continue;
|
|
161
196
|
}
|
|
162
197
|
await this.closeActive(input.sessionId);
|
|
163
|
-
|
|
164
|
-
archiveInputs.push(input);
|
|
198
|
+
readyArchiveInputs.push(input);
|
|
165
199
|
}
|
|
200
|
+
await this.archiveStoreArchiveMany(readyArchiveInputs);
|
|
201
|
+
archiveInputs.push(...readyArchiveInputs);
|
|
166
202
|
for (const record of plan.deleteRecords) {
|
|
167
203
|
if (this.activeSessionHasWork(record.sessionId)) {
|
|
168
204
|
skippedBusySessionIds.add(record.sessionId);
|
|
169
205
|
continue;
|
|
170
206
|
}
|
|
171
207
|
await this.closeActive(record.sessionId);
|
|
172
|
-
|
|
173
|
-
await this.ensureArchivedRecordMoved(record);
|
|
174
|
-
await this.archiveStore.deleteArchived?.(record.sessionId);
|
|
175
|
-
deleteRecords.push(record);
|
|
208
|
+
readyDeleteRecords.push(record);
|
|
176
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)));
|
|
177
213
|
return summarizeSessionCleanupExecution({
|
|
178
214
|
archiveInputs,
|
|
179
215
|
deleteRecords,
|
|
@@ -185,8 +221,12 @@ export class PiSessionService {
|
|
|
185
221
|
async dispose() {
|
|
186
222
|
clearInterval(this.heartbeat);
|
|
187
223
|
this.clearCompactionDrainTimers();
|
|
224
|
+
const pendingOpens = this.pendingSessionOpenPromises();
|
|
225
|
+
if (pendingOpens.length > 0)
|
|
226
|
+
await Promise.allSettled(pendingOpens);
|
|
188
227
|
const activeSessions = Array.from(new Set(this.active.values()));
|
|
189
228
|
this.active.clear();
|
|
229
|
+
this.pendingSessionOpens.clear();
|
|
190
230
|
this.activities.clear();
|
|
191
231
|
this.compactionPromptQueues.clear();
|
|
192
232
|
this.authLossWarnings.clear();
|
|
@@ -198,8 +238,12 @@ export class PiSessionService {
|
|
|
198
238
|
await Promise.all(activeSessions.map(async (active) => {
|
|
199
239
|
active.unsubscribe();
|
|
200
240
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
|
|
201
|
-
|
|
202
|
-
|
|
241
|
+
try {
|
|
242
|
+
await active.runtime.session.abort();
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await active.runtime.dispose();
|
|
246
|
+
}
|
|
203
247
|
}));
|
|
204
248
|
}
|
|
205
249
|
async list(cwd) {
|
|
@@ -217,20 +261,27 @@ export class PiSessionService {
|
|
|
217
261
|
.filter(isDefined);
|
|
218
262
|
return [...unarchivedSessions, ...archivedSessions];
|
|
219
263
|
}
|
|
220
|
-
async start(cwd,
|
|
221
|
-
|
|
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
|
+
});
|
|
222
272
|
const { session } = active.runtime;
|
|
223
273
|
const created = {
|
|
224
274
|
id: session.sessionId,
|
|
225
275
|
path: session.sessionFile ?? "",
|
|
226
276
|
cwd,
|
|
277
|
+
persisted: sessionFileExists(session.sessionFile),
|
|
227
278
|
created: new Date().toISOString(),
|
|
228
279
|
modified: new Date().toISOString(),
|
|
229
280
|
messageCount: session.messages.length,
|
|
230
281
|
firstMessage: "",
|
|
231
282
|
// Include the parent so listeners can nest the new session in the tree
|
|
232
283
|
// immediately, instead of showing it flat until the next reload.
|
|
233
|
-
...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
|
|
284
|
+
...(options.parentSession === undefined ? {} : { parentSessionPath: options.parentSession }),
|
|
234
285
|
};
|
|
235
286
|
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
236
287
|
// session to their list without a manual reload.
|
|
@@ -248,7 +299,7 @@ export class PiSessionService {
|
|
|
248
299
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
249
300
|
if (!decision.allowed)
|
|
250
301
|
throw spawnTargetError(decision);
|
|
251
|
-
const created = await this.start(decision.cwd);
|
|
302
|
+
const created = await this.start(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
|
|
252
303
|
await this.prompt(created.id, input.prompt);
|
|
253
304
|
this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
|
|
254
305
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
@@ -265,7 +316,11 @@ export class PiSessionService {
|
|
|
265
316
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
266
317
|
if (!decision.allowed)
|
|
267
318
|
throw spawnTargetError(decision);
|
|
268
|
-
const created = await this.
|
|
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
|
+
});
|
|
269
324
|
const parentSessionFile = nonEmptyString(input.parentSessionFile);
|
|
270
325
|
const link = {
|
|
271
326
|
parentSessionId: input.parentSessionId,
|
|
@@ -465,61 +520,13 @@ export class PiSessionService {
|
|
|
465
520
|
return;
|
|
466
521
|
this.registerVerifiedSubsession(link);
|
|
467
522
|
}
|
|
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;
|
|
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
|
+
});
|
|
523
530
|
}
|
|
524
531
|
async getOrOpenTrackedSubsession(sessionId) {
|
|
525
532
|
const link = this.subsessionLinks.get(sessionId);
|
|
@@ -577,7 +584,7 @@ export class PiSessionService {
|
|
|
577
584
|
const status = this.activities.get(childId)?.phase === "error" ? "error" : "idle";
|
|
578
585
|
const finalText = finalAssistantText(historyMessages(session));
|
|
579
586
|
const preview = finalText === "" ? "(no output)" : truncateForNotification(finalText);
|
|
580
|
-
const text = `Subsession ${childId} stopped working (status: ${status}). Latest output:\n\n${preview}\n\
|
|
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.`;
|
|
581
588
|
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
|
582
589
|
}
|
|
583
590
|
async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
|
|
@@ -795,6 +802,23 @@ export class PiSessionService {
|
|
|
795
802
|
const active = await this.getActive(ref);
|
|
796
803
|
return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
|
|
797
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
|
+
}
|
|
798
822
|
async archive(ref) {
|
|
799
823
|
const session = await this.getOrOpen(ref);
|
|
800
824
|
if (this.hasActiveWork(session))
|
|
@@ -803,6 +827,69 @@ export class PiSessionService {
|
|
|
803
827
|
await this.closeActive(session.sessionId);
|
|
804
828
|
await this.archiveStore.archive(archiveInput);
|
|
805
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
|
+
}
|
|
806
893
|
async archiveTree(ref) {
|
|
807
894
|
const session = await this.getOrOpen(ref);
|
|
808
895
|
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
@@ -814,8 +901,7 @@ export class PiSessionService {
|
|
|
814
901
|
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
815
902
|
for (const input of archiveInputs)
|
|
816
903
|
await this.closeActive(input.sessionId);
|
|
817
|
-
|
|
818
|
-
await this.archiveStore.archive(input);
|
|
904
|
+
await this.archiveStoreArchiveMany(archiveInputs);
|
|
819
905
|
return {
|
|
820
906
|
archived: true,
|
|
821
907
|
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
@@ -841,6 +927,57 @@ export class PiSessionService {
|
|
|
841
927
|
await this.ensureArchivedRecordMoved(record);
|
|
842
928
|
await this.archiveStore.deleteArchived(record.sessionId);
|
|
843
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
|
+
}
|
|
844
981
|
async reload(ref) {
|
|
845
982
|
await this.assertWritable(ref);
|
|
846
983
|
const session = await this.getOrOpen(ref);
|
|
@@ -878,6 +1015,75 @@ export class PiSessionService {
|
|
|
878
1015
|
// Best-effort shutdown; callers that need errors await closeActive directly.
|
|
879
1016
|
});
|
|
880
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
|
+
}
|
|
881
1087
|
async cleanupPlan(request) {
|
|
882
1088
|
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll?.() ?? [], this.archiveStore.list()]);
|
|
883
1089
|
return planSessionCleanup({
|
|
@@ -922,7 +1128,19 @@ export class PiSessionService {
|
|
|
922
1128
|
const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
|
|
923
1129
|
if (session === undefined)
|
|
924
1130
|
return record;
|
|
925
|
-
|
|
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);
|
|
926
1144
|
}
|
|
927
1145
|
async archiveInputForSession(session) {
|
|
928
1146
|
const cwd = session.sessionManager.getCwd();
|
|
@@ -985,6 +1203,9 @@ export class PiSessionService {
|
|
|
985
1203
|
return [...names];
|
|
986
1204
|
}
|
|
987
1205
|
async closeActive(sessionId) {
|
|
1206
|
+
const pendingOpens = this.pendingSessionOpenPromises(sessionId);
|
|
1207
|
+
if (pendingOpens.length > 0)
|
|
1208
|
+
await Promise.allSettled(pendingOpens);
|
|
988
1209
|
const active = this.active.get(sessionId);
|
|
989
1210
|
if (!active)
|
|
990
1211
|
return;
|
|
@@ -1019,14 +1240,40 @@ export class PiSessionService {
|
|
|
1019
1240
|
if (active !== undefined)
|
|
1020
1241
|
return active;
|
|
1021
1242
|
const archived = await this.getArchived(ref);
|
|
1022
|
-
if (archived?.archivePath !== undefined)
|
|
1023
|
-
|
|
1243
|
+
if (archived?.archivePath !== undefined) {
|
|
1244
|
+
const { archivePath } = archived;
|
|
1245
|
+
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath));
|
|
1246
|
+
}
|
|
1024
1247
|
const match = isPiSessionRef(ref)
|
|
1025
1248
|
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
1026
1249
|
: (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
1027
1250
|
if (!match)
|
|
1028
1251
|
throw new Error("Session not found");
|
|
1029
|
-
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);
|
|
1030
1277
|
}
|
|
1031
1278
|
async getArchived(ref) {
|
|
1032
1279
|
const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
|
|
@@ -1047,20 +1294,53 @@ export class PiSessionService {
|
|
|
1047
1294
|
}
|
|
1048
1295
|
return undefined;
|
|
1049
1296
|
}
|
|
1050
|
-
async create(sessionManager, cwd) {
|
|
1051
|
-
const
|
|
1052
|
-
|
|
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
|
+
});
|
|
1053
1307
|
const active = { runtime, unsubscribe: noop };
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
await this.bindSessionExtensions(session);
|
|
1308
|
+
try {
|
|
1309
|
+
await this.bindSessionExtensions(runtime.session);
|
|
1057
1310
|
this.bindRuntime(active);
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
+
}
|
|
1064
1344
|
}
|
|
1065
1345
|
async bindSessionExtensions(session) {
|
|
1066
1346
|
await session.bindExtensions({
|
|
@@ -1156,10 +1436,15 @@ export class PiSessionService {
|
|
|
1156
1436
|
maybeGenerateSessionName(session, firstMessage) {
|
|
1157
1437
|
if (session.sessionName !== undefined || session.messages.length !== 0 || session.isStreaming || session.isCompacting)
|
|
1158
1438
|
return;
|
|
1439
|
+
const deterministicName = deterministicSessionName(firstMessage);
|
|
1440
|
+
if (deterministicName !== undefined) {
|
|
1441
|
+
this.applyGeneratedSessionName(session, deterministicName);
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1159
1444
|
const model = session.model;
|
|
1160
1445
|
if (model === undefined)
|
|
1161
1446
|
return;
|
|
1162
|
-
void generateShortSessionName(
|
|
1447
|
+
void generateShortSessionName(session.agent.streamFn, model, firstMessage).then((name) => {
|
|
1163
1448
|
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
1164
1449
|
}).catch(() => {
|
|
1165
1450
|
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
|
|
@@ -1336,6 +1621,7 @@ export class PiSessionService {
|
|
|
1336
1621
|
const contextUsage = session.getContextUsage();
|
|
1337
1622
|
return {
|
|
1338
1623
|
sessionId: session.sessionId,
|
|
1624
|
+
persisted: sessionFileExists(session.sessionFile),
|
|
1339
1625
|
...(model === undefined ? {} : { model }),
|
|
1340
1626
|
thinkingLevel: session.thinkingLevel,
|
|
1341
1627
|
isStreaming: session.isStreaming,
|
|
@@ -1368,6 +1654,38 @@ function previewResponseFromPlan(plan) {
|
|
|
1368
1654
|
...(plan.skippedBusySessionIds.length === 0 ? {} : { skippedBusySessionIds: plan.skippedBusySessionIds }),
|
|
1369
1655
|
};
|
|
1370
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
|
+
}
|
|
1371
1689
|
function modelToClientModel(model) {
|
|
1372
1690
|
if (model === undefined)
|
|
1373
1691
|
return {};
|
|
@@ -1386,6 +1704,7 @@ function clientSessionFromListEntry(session) {
|
|
|
1386
1704
|
id: session.id,
|
|
1387
1705
|
path: session.path,
|
|
1388
1706
|
cwd: session.cwd,
|
|
1707
|
+
persisted: true,
|
|
1389
1708
|
...(session.name === undefined ? {} : { name: session.name }),
|
|
1390
1709
|
created: session.created.toISOString(),
|
|
1391
1710
|
modified: session.modified.toISOString(),
|
|
@@ -1514,6 +1833,76 @@ function archivedTimestamp(record) {
|
|
|
1514
1833
|
function isDefined(value) {
|
|
1515
1834
|
return value !== undefined;
|
|
1516
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
|
+
}
|
|
1517
1906
|
function trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile) {
|
|
1518
1907
|
return {
|
|
1519
1908
|
parentSessionId,
|
|
@@ -1579,6 +1968,16 @@ function subsessionHydratedParentKey(parentSessionId, parentSessionFile) {
|
|
|
1579
1968
|
function sessionPathsEqual(a, b) {
|
|
1580
1969
|
return cwdPathsEqual(a, b);
|
|
1581
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
|
+
}
|
|
1582
1981
|
function sessionFileMatches(session, expectedSessionFile) {
|
|
1583
1982
|
const sessionFile = nonEmptyString(session.sessionFile);
|
|
1584
1983
|
return sessionFile !== undefined && expectedSessionFile !== undefined && sessionPathsEqual(sessionFile, expectedSessionFile);
|