@chainingintention/pi-web-cn 1.202606.25 → 1.202606.26
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 +84 -277
- package/dist/cli.js +31 -11
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/CodeViewer-CEaJIjgA.js +4 -0
- package/dist/client/assets/{TerminalPanel-6pE_JF7k.js → TerminalPanel-BdMJbtLB.js} +1 -1
- package/dist/client/assets/UnifiedDiffViewer-Dkgg-4fh.js +34 -0
- package/dist/client/assets/index-iRSp25EB.js +2735 -0
- package/dist/client/assets/{vendor-editor-core-Bc1A7SO7.js → vendor-editor-core-C7aWiQy0.js} +2 -2
- package/dist/client/assets/{vendor-editor-languages-D0CnB2js.js → vendor-editor-languages-EW34FfVy.js} +1 -1
- package/dist/client/index.html +6 -3
- package/dist/config.js +9 -1
- package/dist/config.js.map +1 -1
- package/dist/plugin-api.d.ts +30 -2
- package/dist/server/app.js +35 -17
- package/dist/server/app.js.map +1 -1
- package/dist/server/gitRoutes.js +1 -1
- package/dist/server/gitRoutes.js.map +1 -1
- package/dist/server/machines/machineClient.js +33 -5
- package/dist/server/machines/machineClient.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +18 -3
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/managementEmbed.js +42 -22
- package/dist/server/managementEmbed.js.map +1 -1
- package/dist/server/sessiond/sessionProxyRoutes.js +1 -1
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +2 -2
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/attachmentService.js +68 -10
- package/dist/server/sessions/attachmentService.js.map +1 -1
- package/dist/server/sessions/authProviderOptions.js +1 -5
- package/dist/server/sessions/authProviderOptions.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 +515 -51
- package/dist/server/sessions/piSessionService.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/sessionNameGenerator.js +39 -2
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +17 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +7 -2
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +14 -4
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +74 -24
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/effectivePathAccess.js +27 -0
- package/dist/server/workspaces/effectivePathAccess.js.map +1 -0
- package/dist/server/workspaces/fileContentService.js +118 -7
- package/dist/server/workspaces/fileContentService.js.map +1 -1
- package/dist/server/workspaces/fileOperationService.js +1 -19
- package/dist/server/workspaces/fileOperationService.js.map +1 -1
- package/dist/server/workspaces/fileSuggestions.js +311 -23
- package/dist/server/workspaces/fileSuggestions.js.map +1 -1
- package/dist/server/workspaces/fileTreeService.js +16 -7
- package/dist/server/workspaces/fileTreeService.js.map +1 -1
- package/dist/server/workspaces/imagePreviewService.js +5 -5
- package/dist/server/workspaces/imagePreviewService.js.map +1 -1
- package/dist/server/workspaces/pathAccessPolicy.js +90 -0
- package/dist/server/workspaces/pathAccessPolicy.js.map +1 -0
- package/dist/server/workspaces/pathSafety.js +19 -10
- package/dist/server/workspaces/pathSafety.js.map +1 -1
- package/dist/server/workspaces/projectPiWebConfig.js +61 -0
- package/dist/server/workspaces/projectPiWebConfig.js.map +1 -0
- package/dist/server/workspaces/workspaceDeletionRoutes.js +1 -1
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +89 -6
- package/dist/shared/apiTypes.js +2 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +4 -2
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +6 -1
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/piWebConfigParsing.js +44 -3
- package/dist/shared/piWebConfigParsing.js.map +1 -1
- package/dist/shared/promptAttachments.js +30 -11
- package/dist/shared/promptAttachments.js.map +1 -1
- package/docs/assets/pi-web-desktop.png +0 -0
- package/docs/assets/pi-web-mobile.png +0 -0
- package/docs/assets/pi-web-tablet.png +0 -0
- package/docs/config.md +203 -0
- package/docs/plugins.md +128 -4
- package/package.json +3 -1
- package/dist/client/assets/CodeViewer-BmIwBtZe.js +0 -4
- package/dist/client/assets/index-FZVnfelo.js +0 -2354
- package/docs/assets/pi-web-demo.gif +0 -0
- package/docs/assets/pi-web-demo.webm +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { open, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
@@ -15,6 +15,7 @@ import { cwdPathsEqual } from "../workingDirectory.js";
|
|
|
15
15
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
16
16
|
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
17
17
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
18
|
+
import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
18
19
|
import { createManagedAgentToolOptions, createManagedPythonToolDefinition } from "./managementAgentTools.js";
|
|
19
20
|
import { PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR, managementAgentToolNames, withRuntimeCreationEnvironment, writeManagementPermissionSystemPolicy } from "./managementPermissionSystem.js";
|
|
20
21
|
import { archiveCandidateFromActiveSession, archiveCandidateFromArchivedRecord, archiveCandidateFromListEntry, archiveInputFromActiveSession, archiveInputFromCandidate, archiveInputFromListEntry, clientSessionFromArchivedRecord, compareArchivedRecords, } from "./sessionArchiveMapping.js";
|
|
@@ -85,19 +86,36 @@ function pathInsideOrEqual(parent, child) {
|
|
|
85
86
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
86
87
|
if (!(options.sessionManager instanceof SessionManager))
|
|
87
88
|
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
88
|
-
const runtimeFactory = (
|
|
89
|
-
|
|
89
|
+
const runtimeFactory = createRuntimeWithContextAndOneShotInitialModel(createRuntime, options.managementContext, options.initialModel);
|
|
90
|
+
return createAgentSessionRuntime(runtimeFactory, {
|
|
91
|
+
cwd: options.cwd,
|
|
92
|
+
agentDir: options.agentDir,
|
|
93
|
+
sessionManager: options.sessionManager,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function createRuntimeWithContextAndOneShotInitialModel(createRuntime, managementContext, initialModel) {
|
|
97
|
+
// The inherited model belongs only to the session being spawned. Do not keep
|
|
98
|
+
// reapplying it if that runtime later creates/forks/switches sessions itself.
|
|
99
|
+
let pendingInitialModel = initialModel;
|
|
100
|
+
return async (options) => {
|
|
101
|
+
const model = pendingInitialModel;
|
|
102
|
+
pendingInitialModel = undefined;
|
|
103
|
+
return createRuntime({
|
|
104
|
+
...options,
|
|
105
|
+
...(managementContext === undefined ? {} : { managementContext }),
|
|
106
|
+
...(model === undefined ? {} : { initialModel: model }),
|
|
107
|
+
});
|
|
90
108
|
};
|
|
91
|
-
return createAgentSessionRuntime(runtimeFactory, { ...options, sessionManager: options.sessionManager });
|
|
92
109
|
}
|
|
93
110
|
function createDefaultRuntimeFactory(authStorage, modelRegistry, spawn, subsessions) {
|
|
94
|
-
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext }) => {
|
|
111
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel }) => {
|
|
95
112
|
if (managementContext !== undefined) {
|
|
96
113
|
return createManagementRuntimeFactory(authStorage, modelRegistry, spawn, subsessions, managementContext)({
|
|
97
114
|
cwd,
|
|
98
115
|
agentDir,
|
|
99
116
|
sessionManager,
|
|
100
117
|
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
118
|
+
...(initialModel === undefined ? {} : { initialModel }),
|
|
101
119
|
});
|
|
102
120
|
}
|
|
103
121
|
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
|
|
@@ -106,18 +124,18 @@ function createDefaultRuntimeFactory(authStorage, modelRegistry, spawn, subsessi
|
|
|
106
124
|
...(spawn === undefined ? [] : [defineTool(createSpawnSessionToolDefinition(cwd, { spawn }))]),
|
|
107
125
|
...(subsessions === undefined ? [] : createSubsessionToolDefinitions(cwd, subsessions).map((tool) => defineTool(tool))),
|
|
108
126
|
];
|
|
109
|
-
const
|
|
127
|
+
const result = await createAgentSessionFromServices({
|
|
110
128
|
services,
|
|
111
129
|
sessionManager,
|
|
112
|
-
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
113
130
|
customTools,
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
132
|
+
...(initialModel === undefined ? {} : { model: initialModel }),
|
|
133
|
+
});
|
|
116
134
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
117
135
|
};
|
|
118
136
|
}
|
|
119
137
|
function createManagementRuntimeFactory(authStorage, modelRegistry, spawn, subsessions, managementContext) {
|
|
120
|
-
return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
138
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel }) => {
|
|
121
139
|
const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
|
|
122
140
|
return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
|
|
123
141
|
const services = await createAgentSessionServices({
|
|
@@ -142,6 +160,7 @@ function createManagementRuntimeFactory(authStorage, modelRegistry, spawn, subse
|
|
|
142
160
|
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
143
161
|
customTools,
|
|
144
162
|
tools: managementAgentToolNames(managementContext),
|
|
163
|
+
...(initialModel === undefined ? {} : { model: initialModel }),
|
|
145
164
|
};
|
|
146
165
|
const result = await createAgentSessionFromServices(options);
|
|
147
166
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
@@ -193,6 +212,10 @@ export class PiSessionService {
|
|
|
193
212
|
this.subsessionParents = new Map();
|
|
194
213
|
/** Parent session id -> the set of tracked subsession ids it spawned. */
|
|
195
214
|
this.subsessionChildren = new Map();
|
|
215
|
+
/** Tracked subsession id -> persisted recovery details for the child. */
|
|
216
|
+
this.subsessionLinks = new Map();
|
|
217
|
+
/** Parent id/file identities whose persisted links have already been loaded. */
|
|
218
|
+
this.subsessionHydratedParents = new Set();
|
|
196
219
|
/**
|
|
197
220
|
* Tracked subsession id -> whether a completion notification is armed.
|
|
198
221
|
* Armed when the child starts working; firing on completion disarms it so a
|
|
@@ -205,14 +228,15 @@ export class PiSessionService {
|
|
|
205
228
|
this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
|
|
206
229
|
this.spawnTargets = deps.spawnTargets;
|
|
207
230
|
this.logger = deps.logger ?? noopLogger;
|
|
231
|
+
this.now = deps.now ?? (() => new Date());
|
|
208
232
|
// Subsessions are a beta capability gated behind their own flag, and they
|
|
209
233
|
// also require the spawn capability (they share its project-scope resolver).
|
|
210
234
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
211
235
|
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
212
236
|
spawn: (input) => this.spawnSubsession(input),
|
|
213
|
-
list: (parentSessionId) => this.listSubsessions(parentSessionId),
|
|
214
|
-
check: (parentSessionId, sessionId) => this.checkSubsession(parentSessionId, sessionId),
|
|
215
|
-
read: (parentSessionId, sessionId, query) => this.readSubsession(parentSessionId, sessionId, query),
|
|
237
|
+
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
238
|
+
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
239
|
+
read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
|
|
216
240
|
});
|
|
217
241
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
218
242
|
this.workspaceActivity = deps.workspaceActivity;
|
|
@@ -231,6 +255,44 @@ export class PiSessionService {
|
|
|
231
255
|
activeCount() {
|
|
232
256
|
return this.active.size;
|
|
233
257
|
}
|
|
258
|
+
async cleanupPreview(request) {
|
|
259
|
+
return previewResponseFromPlan(await this.cleanupPlan(request));
|
|
260
|
+
}
|
|
261
|
+
async cleanup(request) {
|
|
262
|
+
const plan = await this.cleanupPlan(request);
|
|
263
|
+
if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined)
|
|
264
|
+
throw new Error("Archive store does not support deletion");
|
|
265
|
+
const archiveInputs = [];
|
|
266
|
+
const deleteRecords = [];
|
|
267
|
+
const skippedBusySessionIds = new Set(plan.skippedBusySessionIds);
|
|
268
|
+
for (const input of plan.archiveInputs) {
|
|
269
|
+
if (this.activeSessionHasWork(input.sessionId)) {
|
|
270
|
+
skippedBusySessionIds.add(input.sessionId);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
await this.closeActive(input.sessionId);
|
|
274
|
+
await this.archiveStore.archive(input);
|
|
275
|
+
archiveInputs.push(input);
|
|
276
|
+
}
|
|
277
|
+
for (const record of plan.deleteRecords) {
|
|
278
|
+
if (this.activeSessionHasWork(record.sessionId)) {
|
|
279
|
+
skippedBusySessionIds.add(record.sessionId);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
await this.closeActive(record.sessionId);
|
|
283
|
+
if (record.archivePath === undefined)
|
|
284
|
+
await this.ensureArchivedRecordMoved(record);
|
|
285
|
+
await this.archiveStore.deleteArchived?.(record.sessionId);
|
|
286
|
+
deleteRecords.push(record);
|
|
287
|
+
}
|
|
288
|
+
return summarizeSessionCleanupExecution({
|
|
289
|
+
archiveInputs,
|
|
290
|
+
deleteRecords,
|
|
291
|
+
thresholds: plan.thresholds,
|
|
292
|
+
generatedAt: plan.generatedAt,
|
|
293
|
+
skippedBusySessionIds: [...skippedBusySessionIds],
|
|
294
|
+
});
|
|
295
|
+
}
|
|
234
296
|
async dispose() {
|
|
235
297
|
clearInterval(this.heartbeat);
|
|
236
298
|
this.clearCompactionDrainTimers();
|
|
@@ -241,6 +303,8 @@ export class PiSessionService {
|
|
|
241
303
|
this.authLossWarnings.clear();
|
|
242
304
|
this.subsessionParents.clear();
|
|
243
305
|
this.subsessionChildren.clear();
|
|
306
|
+
this.subsessionLinks.clear();
|
|
307
|
+
this.subsessionHydratedParents.clear();
|
|
244
308
|
this.subsessionNotifyArmed.clear();
|
|
245
309
|
await Promise.all(activeSessions.map(async (active) => {
|
|
246
310
|
active.unsubscribe();
|
|
@@ -265,7 +329,10 @@ export class PiSessionService {
|
|
|
265
329
|
return [...unarchivedSessions, ...archivedSessions];
|
|
266
330
|
}
|
|
267
331
|
async start(cwd, options = {}) {
|
|
268
|
-
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd,
|
|
332
|
+
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
|
|
333
|
+
...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
|
|
334
|
+
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
335
|
+
});
|
|
269
336
|
const { session } = active.runtime;
|
|
270
337
|
const created = {
|
|
271
338
|
id: session.sessionId,
|
|
@@ -295,7 +362,7 @@ export class PiSessionService {
|
|
|
295
362
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
296
363
|
if (!decision.allowed)
|
|
297
364
|
throw spawnTargetError(decision);
|
|
298
|
-
const created = await this.start(decision.cwd);
|
|
365
|
+
const created = await this.start(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
|
|
299
366
|
await this.prompt(created.id, input.prompt);
|
|
300
367
|
this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
|
|
301
368
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
@@ -312,69 +379,294 @@ export class PiSessionService {
|
|
|
312
379
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
313
380
|
if (!decision.allowed)
|
|
314
381
|
throw spawnTargetError(decision);
|
|
315
|
-
const created = await this.start(decision.cwd,
|
|
316
|
-
|
|
382
|
+
const created = await this.start(decision.cwd, {
|
|
383
|
+
...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
|
|
384
|
+
...(input.model === undefined ? {} : { initialModel: input.model }),
|
|
385
|
+
});
|
|
386
|
+
const parentSessionFile = nonEmptyString(input.parentSessionFile);
|
|
387
|
+
const link = {
|
|
388
|
+
parentSessionId: input.parentSessionId,
|
|
389
|
+
childSessionId: created.id,
|
|
390
|
+
...(created.path === "" ? {} : { childSessionFile: created.path }),
|
|
391
|
+
...(parentSessionFile === undefined ? {} : { parentSessionFile }),
|
|
392
|
+
cwd: decision.cwd,
|
|
393
|
+
};
|
|
394
|
+
this.registerVerifiedSubsession(link);
|
|
395
|
+
this.persistSubsessionLink(link);
|
|
396
|
+
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
317
397
|
await this.prompt(created.id, input.prompt);
|
|
318
398
|
this.logger.info({ parentSessionId: input.parentSessionId, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_subsession started a tracked child session");
|
|
319
399
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
320
400
|
}
|
|
321
401
|
/** Summaries of the tracked subsessions spawned by `parentSessionId`. */
|
|
322
|
-
async listSubsessions(parentSessionId) {
|
|
402
|
+
async listSubsessions(parentSessionId, parentSessionFile) {
|
|
403
|
+
const parentFile = nonEmptyString(parentSessionFile);
|
|
404
|
+
await this.hydrateSubsessionsForParent(parentSessionId, parentFile);
|
|
323
405
|
const childIds = this.subsessionChildren.get(parentSessionId);
|
|
324
406
|
if (childIds === undefined)
|
|
325
407
|
return [];
|
|
326
|
-
|
|
408
|
+
const authorizedChildIds = [...childIds].filter((childId) => this.subsessionLinkBelongsToParent(parentSessionId, parentFile, childId));
|
|
409
|
+
return Promise.all(authorizedChildIds.map(async (childId) => ({ sessionId: childId, ...(await this.subsessionSummaryFields(childId)) })));
|
|
327
410
|
}
|
|
328
411
|
/** Status and final result of a subsession, scoped to the caller's children. */
|
|
329
|
-
async checkSubsession(parentSessionId, sessionId) {
|
|
330
|
-
const session = await this.openSubsession(parentSessionId, sessionId);
|
|
412
|
+
async checkSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
413
|
+
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile);
|
|
331
414
|
const messages = historyMessages(session);
|
|
332
415
|
return {
|
|
333
416
|
sessionId,
|
|
334
417
|
cwd: session.sessionManager.getCwd(),
|
|
335
|
-
status:
|
|
418
|
+
status: this.subsessionStatus(session),
|
|
336
419
|
finalText: finalAssistantText(messages),
|
|
337
420
|
messageCount: messages.length,
|
|
338
421
|
};
|
|
339
422
|
}
|
|
340
423
|
/** Filtered, paginated transcript of a subsession, scoped to the caller's children. */
|
|
341
|
-
async readSubsession(parentSessionId, sessionId, query) {
|
|
342
|
-
const session = await this.openSubsession(parentSessionId, sessionId);
|
|
424
|
+
async readSubsession(parentSessionId, sessionId, query, parentSessionFile) {
|
|
425
|
+
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile);
|
|
343
426
|
const view = buildTranscriptView(historyMessages(session), query);
|
|
344
427
|
return {
|
|
345
428
|
sessionId,
|
|
346
429
|
cwd: session.sessionManager.getCwd(),
|
|
347
|
-
status:
|
|
430
|
+
status: this.subsessionStatus(session),
|
|
348
431
|
...view,
|
|
349
432
|
};
|
|
350
433
|
}
|
|
351
434
|
/** Open a session after verifying it is one of the caller's tracked children. */
|
|
352
|
-
async openSubsession(parentSessionId, sessionId) {
|
|
353
|
-
|
|
435
|
+
async openSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
436
|
+
const parentFile = nonEmptyString(parentSessionFile);
|
|
437
|
+
await this.hydrateSubsessionsForParent(parentSessionId, parentFile);
|
|
438
|
+
if (this.subsessionParents.get(sessionId) !== parentSessionId || !this.subsessionLinkBelongsToParent(parentSessionId, parentFile, sessionId)) {
|
|
354
439
|
throw new Error(`Session ${sessionId} is not one of your subsessions`);
|
|
355
440
|
}
|
|
356
|
-
return this.
|
|
441
|
+
return this.getOrOpenTrackedSubsession(sessionId);
|
|
442
|
+
}
|
|
443
|
+
subsessionLinkBelongsToParent(parentSessionId, parentSessionFile, childSessionId) {
|
|
444
|
+
const link = this.subsessionLinks.get(childSessionId);
|
|
445
|
+
if (link?.parentSessionId !== parentSessionId)
|
|
446
|
+
return false;
|
|
447
|
+
return parentSessionFile === undefined || trackedLinkParentFileMatches(link, parentSessionFile);
|
|
448
|
+
}
|
|
449
|
+
activeChildForSubsessionLink(link) {
|
|
450
|
+
const active = this.active.get(link.childSessionId);
|
|
451
|
+
if (active === undefined)
|
|
452
|
+
return undefined;
|
|
453
|
+
return activeSessionFileMatches(active, link.childSessionFile) ? active : undefined;
|
|
454
|
+
}
|
|
455
|
+
activeParentForSubsessionLink(link) {
|
|
456
|
+
const active = this.active.get(link.parentSessionId);
|
|
457
|
+
if (active === undefined)
|
|
458
|
+
return undefined;
|
|
459
|
+
return activeSessionFileMatches(active, link.parentSessionFile) ? active : undefined;
|
|
357
460
|
}
|
|
358
|
-
|
|
461
|
+
subsessionLinkForActiveChild(session) {
|
|
462
|
+
const childId = session.sessionId;
|
|
463
|
+
const parentId = this.subsessionParents.get(childId);
|
|
464
|
+
const link = this.subsessionLinks.get(childId);
|
|
465
|
+
if (parentId === undefined || link?.parentSessionId !== parentId)
|
|
466
|
+
return undefined;
|
|
467
|
+
return sessionFileMatches(session, link.childSessionFile) ? link : undefined;
|
|
468
|
+
}
|
|
469
|
+
registerVerifiedSubsession(link) {
|
|
470
|
+
const { childSessionId, parentSessionId } = link;
|
|
471
|
+
const previousParentId = this.subsessionParents.get(childSessionId);
|
|
472
|
+
if (previousParentId !== undefined && previousParentId !== parentSessionId) {
|
|
473
|
+
const previousChildren = this.subsessionChildren.get(previousParentId);
|
|
474
|
+
previousChildren?.delete(childSessionId);
|
|
475
|
+
if (previousChildren?.size === 0)
|
|
476
|
+
this.subsessionChildren.delete(previousParentId);
|
|
477
|
+
}
|
|
359
478
|
this.subsessionParents.set(childSessionId, parentSessionId);
|
|
360
479
|
const children = this.subsessionChildren.get(parentSessionId) ?? new Set();
|
|
361
480
|
children.add(childSessionId);
|
|
362
481
|
this.subsessionChildren.set(parentSessionId, children);
|
|
363
|
-
this.
|
|
482
|
+
this.subsessionLinks.set(childSessionId, link);
|
|
483
|
+
if (!this.subsessionNotifyArmed.has(childSessionId))
|
|
484
|
+
this.subsessionNotifyArmed.set(childSessionId, false);
|
|
485
|
+
}
|
|
486
|
+
unregisterSubsession(childSessionId) {
|
|
487
|
+
const parentSessionId = this.subsessionParents.get(childSessionId);
|
|
488
|
+
this.subsessionParents.delete(childSessionId);
|
|
489
|
+
this.subsessionLinks.delete(childSessionId);
|
|
490
|
+
this.subsessionNotifyArmed.delete(childSessionId);
|
|
491
|
+
if (parentSessionId === undefined)
|
|
492
|
+
return;
|
|
493
|
+
const children = this.subsessionChildren.get(parentSessionId);
|
|
494
|
+
children?.delete(childSessionId);
|
|
495
|
+
if (children?.size === 0)
|
|
496
|
+
this.subsessionChildren.delete(parentSessionId);
|
|
497
|
+
}
|
|
498
|
+
persistSubsessionLink(link) {
|
|
499
|
+
const parent = this.activeParentForSubsessionLink(link)?.runtime.session;
|
|
500
|
+
if (parent === undefined)
|
|
501
|
+
return;
|
|
502
|
+
if (parent.sessionManager.appendCustomEntry === undefined)
|
|
503
|
+
return;
|
|
504
|
+
try {
|
|
505
|
+
parent.sessionManager.appendCustomEntry(SUBSESSION_LINK_CUSTOM_TYPE, persistedParentSubsessionLinkData(link));
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
this.logger.info({ parentSessionId: link.parentSessionId, sessionId: link.childSessionId, error: error instanceof Error ? error.message : String(error) }, "failed to persist subsession link");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
persistSubsessionChildMarker(parentSessionId, childSessionId) {
|
|
512
|
+
const child = this.active.get(childSessionId)?.runtime.session;
|
|
513
|
+
if (child === undefined)
|
|
514
|
+
return;
|
|
515
|
+
if (child.sessionManager.appendCustomEntry === undefined)
|
|
516
|
+
return;
|
|
517
|
+
try {
|
|
518
|
+
child.sessionManager.appendCustomEntry(SUBSESSION_CHILD_LINK_CUSTOM_TYPE, persistedChildSubsessionLinkData(parentSessionId, childSessionId));
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
this.logger.info({ parentSessionId, sessionId: childSessionId, error: error instanceof Error ? error.message : String(error) }, "failed to persist subsession child marker");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async hydrateSubsessionsForParent(parentSessionId, parentSessionFile) {
|
|
525
|
+
const hydrationKey = subsessionHydratedParentKey(parentSessionId, parentSessionFile);
|
|
526
|
+
if (this.subsessionHydratedParents.has(hydrationKey))
|
|
527
|
+
return;
|
|
528
|
+
const activeParent = this.active.get(parentSessionId);
|
|
529
|
+
if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
|
|
530
|
+
const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
|
|
531
|
+
await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
532
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (parentSessionFile === undefined)
|
|
536
|
+
return;
|
|
537
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId) {
|
|
538
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
let parentManager;
|
|
542
|
+
try {
|
|
543
|
+
parentManager = this.sessionManager.open(parentSessionFile);
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
|
|
550
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
551
|
+
}
|
|
552
|
+
async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile) {
|
|
553
|
+
// Parent custom links are the authoritative recovery record: verify the
|
|
554
|
+
// exact live child file/header before tracking.
|
|
555
|
+
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
556
|
+
for (const entry of entries) {
|
|
557
|
+
const link = parsePersistedParentSubsessionLink(entry);
|
|
558
|
+
if (link === undefined)
|
|
559
|
+
continue;
|
|
560
|
+
const verified = await this.verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link);
|
|
561
|
+
if (verified === undefined)
|
|
562
|
+
continue;
|
|
563
|
+
this.registerVerifiedSubsession(verified);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link) {
|
|
567
|
+
if (parentSessionFile === undefined)
|
|
568
|
+
return undefined;
|
|
569
|
+
if (link.spawnedBySessionId !== parentSessionId)
|
|
570
|
+
return undefined;
|
|
571
|
+
if (!(await this.parentLinkHasValidChildTarget(parentSessionFile, link)))
|
|
572
|
+
return undefined;
|
|
573
|
+
return trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile);
|
|
574
|
+
}
|
|
575
|
+
async parentLinkHasValidChildTarget(parentSessionFile, link) {
|
|
576
|
+
return link.spawnedSessionFile !== undefined
|
|
577
|
+
&& await sessionFileHeaderMatches(link.spawnedSessionFile, { sessionId: link.spawnedSessionId, parentSessionFile });
|
|
578
|
+
}
|
|
579
|
+
async recoverSubsessionTrackingForOpenedSession(session) {
|
|
580
|
+
const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
|
|
581
|
+
if (link === undefined)
|
|
582
|
+
return;
|
|
583
|
+
this.registerVerifiedSubsession(link);
|
|
584
|
+
}
|
|
585
|
+
async verifiedSubsessionLinkFromOpenedChild(session) {
|
|
586
|
+
// Child markers are only hints; the current child header and reciprocal
|
|
587
|
+
// parent custom link must agree on the exact ids and files before relinking.
|
|
588
|
+
const entries = session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch();
|
|
589
|
+
let marker;
|
|
590
|
+
for (const entry of entries) {
|
|
591
|
+
const parsed = parsePersistedChildSubsessionLink(entry);
|
|
592
|
+
if (parsed?.spawnedSessionId === session.sessionId)
|
|
593
|
+
marker = parsed;
|
|
594
|
+
}
|
|
595
|
+
if (marker === undefined)
|
|
596
|
+
return undefined;
|
|
597
|
+
const childSessionFile = nonEmptyString(session.sessionFile);
|
|
598
|
+
if (childSessionFile === undefined)
|
|
599
|
+
return undefined;
|
|
600
|
+
const childHeader = await readSessionHeaderSummary(childSessionFile);
|
|
601
|
+
if (childHeader?.id !== session.sessionId)
|
|
602
|
+
return undefined;
|
|
603
|
+
const parentSessionFile = nonEmptyString(childHeader.parentSession);
|
|
604
|
+
if (parentSessionFile === undefined)
|
|
605
|
+
return undefined;
|
|
606
|
+
const parentHeader = await readSessionHeaderSummary(parentSessionFile);
|
|
607
|
+
if (parentHeader?.id !== marker.spawnedBySessionId)
|
|
608
|
+
return undefined;
|
|
609
|
+
const parentLink = this.findReciprocalParentSubsessionLink(parentSessionFile, marker.spawnedBySessionId, session.sessionId, childSessionFile);
|
|
610
|
+
if (parentLink === undefined)
|
|
611
|
+
return undefined;
|
|
612
|
+
return {
|
|
613
|
+
parentSessionId: marker.spawnedBySessionId,
|
|
614
|
+
childSessionId: session.sessionId,
|
|
615
|
+
childSessionFile,
|
|
616
|
+
parentSessionFile,
|
|
617
|
+
cwd: parentLink.cwd ?? session.sessionManager.getCwd(),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
findReciprocalParentSubsessionLink(parentSessionFile, parentSessionId, childSessionId, childSessionFile) {
|
|
621
|
+
let parentManager;
|
|
622
|
+
try {
|
|
623
|
+
parentManager = this.sessionManager.open(parentSessionFile);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
629
|
+
for (const entry of entries) {
|
|
630
|
+
const link = parsePersistedParentSubsessionLink(entry);
|
|
631
|
+
if (link === undefined)
|
|
632
|
+
continue;
|
|
633
|
+
if (link.spawnedBySessionId !== parentSessionId || link.spawnedSessionId !== childSessionId)
|
|
634
|
+
continue;
|
|
635
|
+
if (link.spawnedSessionFile === undefined || !sessionPathsEqual(link.spawnedSessionFile, childSessionFile))
|
|
636
|
+
continue;
|
|
637
|
+
return link;
|
|
638
|
+
}
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
async getOrOpenTrackedSubsession(sessionId) {
|
|
642
|
+
const link = this.subsessionLinks.get(sessionId);
|
|
643
|
+
if (link === undefined)
|
|
644
|
+
throw new Error("Session not found");
|
|
645
|
+
const active = this.activeChildForSubsessionLink(link);
|
|
646
|
+
if (active !== undefined)
|
|
647
|
+
return active.runtime.session;
|
|
648
|
+
if (link.childSessionFile !== undefined) {
|
|
649
|
+
if (!(await sessionFileHeaderMatches(link.childSessionFile, { sessionId, parentSessionFile: link.parentSessionFile })))
|
|
650
|
+
throw new Error("Session not found");
|
|
651
|
+
const sessionManager = this.sessionManager.open(link.childSessionFile);
|
|
652
|
+
return (await this.create(sessionManager, link.cwd ?? sessionManager.getCwd())).runtime.session;
|
|
653
|
+
}
|
|
654
|
+
throw new Error("Session not found");
|
|
364
655
|
}
|
|
365
656
|
async subsessionSummaryFields(childSessionId) {
|
|
366
|
-
const
|
|
657
|
+
const link = this.subsessionLinks.get(childSessionId);
|
|
658
|
+
const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
|
|
367
659
|
if (active !== undefined) {
|
|
368
|
-
return { cwd: active.runtime.cwd, status:
|
|
660
|
+
return { cwd: active.runtime.cwd, status: this.subsessionStatus(active.runtime.session) };
|
|
369
661
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
662
|
+
if (link?.childSessionFile !== undefined && (await sessionFileHeaderMatches(link.childSessionFile, { sessionId: childSessionId, parentSessionFile: link.parentSessionFile }))) {
|
|
663
|
+
return { cwd: link.cwd ?? "", status: "idle" };
|
|
664
|
+
}
|
|
665
|
+
if (link?.cwd !== undefined)
|
|
666
|
+
return { cwd: link.cwd, status: "unknown" };
|
|
373
667
|
return { cwd: "", status: "unknown" };
|
|
374
668
|
}
|
|
375
|
-
|
|
376
|
-
if (await this.archiveStore.isArchived(session.sessionId))
|
|
377
|
-
return "archived";
|
|
669
|
+
subsessionStatus(session) {
|
|
378
670
|
if (this.hasActiveWork(session))
|
|
379
671
|
return "working";
|
|
380
672
|
if (this.activities.get(session.sessionId)?.phase === "error")
|
|
@@ -388,10 +680,10 @@ export class PiSessionService {
|
|
|
388
680
|
* parent is busy and delivers immediately when it is idle).
|
|
389
681
|
*/
|
|
390
682
|
updateSubsessionTracking(session) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
if (parentId === undefined)
|
|
683
|
+
const link = this.subsessionLinkForActiveChild(session);
|
|
684
|
+
if (link === undefined)
|
|
394
685
|
return;
|
|
686
|
+
const childId = link.childSessionId;
|
|
395
687
|
if (this.hasActiveWork(session)) {
|
|
396
688
|
this.subsessionNotifyArmed.set(childId, true);
|
|
397
689
|
return;
|
|
@@ -403,7 +695,23 @@ export class PiSessionService {
|
|
|
403
695
|
const finalText = finalAssistantText(historyMessages(session));
|
|
404
696
|
const preview = finalText === "" ? "(no output)" : truncateForNotification(finalText);
|
|
405
697
|
const text = `Subsession ${childId} stopped working (status: ${status}). Latest output:\n\n${preview}\n\nUse check_subsession with sessionId "${childId}" for its status and latest output, or read_subsession to look through its full transcript.`;
|
|
406
|
-
void this.notifyParentOfSubsession(
|
|
698
|
+
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
|
699
|
+
}
|
|
700
|
+
async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
|
|
701
|
+
const link = this.subsessionLinks.get(childSessionId);
|
|
702
|
+
if (link?.parentSessionId !== parentSessionId)
|
|
703
|
+
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
704
|
+
const active = this.activeParentForSubsessionLink(link);
|
|
705
|
+
if (active !== undefined)
|
|
706
|
+
return active.runtime.session;
|
|
707
|
+
const parentSessionFile = link.parentSessionFile;
|
|
708
|
+
if (parentSessionFile === undefined)
|
|
709
|
+
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
710
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId) {
|
|
711
|
+
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
712
|
+
}
|
|
713
|
+
const sessionManager = this.sessionManager.open(parentSessionFile);
|
|
714
|
+
return (await this.create(sessionManager, sessionManager.getCwd())).runtime.session;
|
|
407
715
|
}
|
|
408
716
|
/**
|
|
409
717
|
* Deliver a subsession-completion notice to the parent as a system-authored
|
|
@@ -414,7 +722,7 @@ export class PiSessionService {
|
|
|
414
722
|
*/
|
|
415
723
|
async notifyParentOfSubsession(parentId, childId, text) {
|
|
416
724
|
try {
|
|
417
|
-
const session = await this.
|
|
725
|
+
const session = await this.getOrOpenParentForSubsession(parentId, childId);
|
|
418
726
|
await session.sendCustomMessage({ customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, content: text, display: true, details: { sessionId: childId } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
419
727
|
this.publishStatus(session);
|
|
420
728
|
}
|
|
@@ -552,7 +860,7 @@ export class PiSessionService {
|
|
|
552
860
|
this.publishStatus(session);
|
|
553
861
|
}
|
|
554
862
|
async saveAttachments(ref, attachments, folder) {
|
|
555
|
-
const parsed = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false });
|
|
863
|
+
const parsed = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false, allowFileAttachments: true });
|
|
556
864
|
if (parsed.length === 0)
|
|
557
865
|
return [];
|
|
558
866
|
await this.assertWritable(ref);
|
|
@@ -665,6 +973,8 @@ export class PiSessionService {
|
|
|
665
973
|
if (sessionFile === undefined || sessionFile === "")
|
|
666
974
|
throw new Error("会话尚未持久化");
|
|
667
975
|
await clearParentSession(sessionFile);
|
|
976
|
+
clearParentSessionHeader(session.sessionManager);
|
|
977
|
+
this.unregisterSubsession(session.sessionId);
|
|
668
978
|
}
|
|
669
979
|
async abort(ref) {
|
|
670
980
|
const active = this.activeForLookupAny(ref);
|
|
@@ -685,6 +995,27 @@ export class PiSessionService {
|
|
|
685
995
|
// Best-effort shutdown; callers that need errors await closeActive directly.
|
|
686
996
|
});
|
|
687
997
|
}
|
|
998
|
+
async cleanupPlan(request) {
|
|
999
|
+
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll?.() ?? [], this.archiveStore.list()]);
|
|
1000
|
+
return planSessionCleanup({
|
|
1001
|
+
sessions,
|
|
1002
|
+
archivedRecords,
|
|
1003
|
+
activeSessions: this.cleanupActiveSessionStatuses(),
|
|
1004
|
+
thresholds: request.thresholds,
|
|
1005
|
+
...(request.projectCwds === undefined ? {} : { projectCwds: request.projectCwds }),
|
|
1006
|
+
now: this.now(),
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
cleanupActiveSessionStatuses() {
|
|
1010
|
+
return [...new Set(this.active.values())].map((active) => ({
|
|
1011
|
+
sessionId: active.runtime.session.sessionId,
|
|
1012
|
+
hasActiveWork: this.hasActiveWork(active.runtime.session),
|
|
1013
|
+
}));
|
|
1014
|
+
}
|
|
1015
|
+
activeSessionHasWork(sessionId) {
|
|
1016
|
+
const active = this.active.get(sessionId);
|
|
1017
|
+
return active !== undefined && this.hasActiveWork(active.runtime.session);
|
|
1018
|
+
}
|
|
688
1019
|
reconcilableSessionIds(cwd, listedSessionIds, archivedById) {
|
|
689
1020
|
const sessionIds = new Set(listedSessionIds);
|
|
690
1021
|
for (const active of new Set(this.active.values())) {
|
|
@@ -782,7 +1113,8 @@ export class PiSessionService {
|
|
|
782
1113
|
// Disarm subsession notification before teardown so the abort below cannot
|
|
783
1114
|
// emit a "stopped working" event that notifies the parent (e.g. on archive).
|
|
784
1115
|
// The parent/children link is kept so the parent can still see the child.
|
|
785
|
-
this.
|
|
1116
|
+
if (this.subsessionLinkForActiveChild(active.runtime.session) !== undefined)
|
|
1117
|
+
this.subsessionNotifyArmed.delete(sessionId);
|
|
786
1118
|
clearSessionQueue(active.runtime.session);
|
|
787
1119
|
active.unsubscribe();
|
|
788
1120
|
try {
|
|
@@ -807,14 +1139,15 @@ export class PiSessionService {
|
|
|
807
1139
|
if (mismatched !== undefined)
|
|
808
1140
|
await this.closeActive(mismatched.runtime.session.sessionId);
|
|
809
1141
|
const archived = await this.getArchived(ref);
|
|
810
|
-
if (archived?.archivePath !== undefined)
|
|
811
|
-
return this.create(this.sessionManager.open(archived.archivePath), archived.cwd, managementContext);
|
|
1142
|
+
if (archived?.archivePath !== undefined) {
|
|
1143
|
+
return this.create(this.sessionManager.open(archived.archivePath), archived.cwd, managementContext === undefined ? {} : { managementContext });
|
|
1144
|
+
}
|
|
812
1145
|
const match = isPiSessionRef(ref)
|
|
813
1146
|
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
814
1147
|
: (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
815
1148
|
if (!match)
|
|
816
1149
|
throw new Error("未找到会话");
|
|
817
|
-
return this.create(this.sessionManager.open(match.path), match.cwd, managementContext);
|
|
1150
|
+
return this.create(this.sessionManager.open(match.path), match.cwd, managementContext === undefined ? {} : { managementContext });
|
|
818
1151
|
}
|
|
819
1152
|
async getArchived(ref) {
|
|
820
1153
|
const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
|
|
@@ -847,16 +1180,24 @@ export class PiSessionService {
|
|
|
847
1180
|
}
|
|
848
1181
|
return undefined;
|
|
849
1182
|
}
|
|
850
|
-
async create(sessionManager, cwd,
|
|
851
|
-
const runtime = await this.createAgentRuntime(this.createRuntime,
|
|
1183
|
+
async create(sessionManager, cwd, options = {}) {
|
|
1184
|
+
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
1185
|
+
cwd,
|
|
1186
|
+
agentDir: this.agentDir,
|
|
1187
|
+
sessionManager,
|
|
1188
|
+
...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
|
|
1189
|
+
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
1190
|
+
});
|
|
852
1191
|
await this.bindSessionExtensions(runtime.session);
|
|
853
|
-
const active = { runtime, unsubscribe: noop, managementContextKey: managementContextKey(managementContext) };
|
|
1192
|
+
const active = { runtime, unsubscribe: noop, managementContextKey: managementContextKey(options.managementContext) };
|
|
854
1193
|
this.bindRuntime(active);
|
|
855
1194
|
runtime.setRebindSession(async (session) => {
|
|
856
1195
|
await this.bindSessionExtensions(session);
|
|
857
1196
|
this.bindRuntime(active);
|
|
1197
|
+
await this.recoverSubsessionTrackingForOpenedSession(session);
|
|
858
1198
|
});
|
|
859
1199
|
this.active.set(runtime.session.sessionId, active);
|
|
1200
|
+
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
860
1201
|
this.publishStatus(runtime.session);
|
|
861
1202
|
return active;
|
|
862
1203
|
}
|
|
@@ -1157,6 +1498,15 @@ export class PiSessionService {
|
|
|
1157
1498
|
return queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)).some((message) => message.text === text);
|
|
1158
1499
|
}
|
|
1159
1500
|
}
|
|
1501
|
+
function previewResponseFromPlan(plan) {
|
|
1502
|
+
return {
|
|
1503
|
+
generatedAt: plan.generatedAt,
|
|
1504
|
+
thresholds: plan.thresholds,
|
|
1505
|
+
projects: plan.projects,
|
|
1506
|
+
totals: plan.totals,
|
|
1507
|
+
...(plan.skippedBusySessionIds.length === 0 ? {} : { skippedBusySessionIds: plan.skippedBusySessionIds }),
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1160
1510
|
function modelToClientModel(model) {
|
|
1161
1511
|
if (model === undefined)
|
|
1162
1512
|
return {};
|
|
@@ -1197,6 +1547,111 @@ function addSessionName(names, name) {
|
|
|
1197
1547
|
function isDefined(value) {
|
|
1198
1548
|
return value !== undefined;
|
|
1199
1549
|
}
|
|
1550
|
+
function trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile) {
|
|
1551
|
+
return {
|
|
1552
|
+
parentSessionId,
|
|
1553
|
+
childSessionId: link.spawnedSessionId,
|
|
1554
|
+
...(link.spawnedSessionFile === undefined ? {} : { childSessionFile: link.spawnedSessionFile }),
|
|
1555
|
+
parentSessionFile,
|
|
1556
|
+
...(link.cwd === undefined ? {} : { cwd: link.cwd }),
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function persistedParentSubsessionLinkData(link) {
|
|
1560
|
+
return {
|
|
1561
|
+
version: 1,
|
|
1562
|
+
spawnedBySessionId: link.parentSessionId,
|
|
1563
|
+
spawnedSessionId: link.childSessionId,
|
|
1564
|
+
...(link.childSessionFile === undefined ? {} : { spawnedSessionFile: link.childSessionFile }),
|
|
1565
|
+
...(link.cwd === undefined ? {} : { cwd: link.cwd }),
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
function persistedChildSubsessionLinkData(parentSessionId, childSessionId) {
|
|
1569
|
+
return {
|
|
1570
|
+
version: 1,
|
|
1571
|
+
spawnedBySessionId: parentSessionId,
|
|
1572
|
+
spawnedSessionId: childSessionId,
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
function parsePersistedParentSubsessionLink(entry) {
|
|
1576
|
+
if (!isRecord(entry) || entry["type"] !== "custom" || entry["customType"] !== SUBSESSION_LINK_CUSTOM_TYPE)
|
|
1577
|
+
return undefined;
|
|
1578
|
+
const data = entry["data"];
|
|
1579
|
+
if (!isRecord(data))
|
|
1580
|
+
return undefined;
|
|
1581
|
+
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
1582
|
+
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
1583
|
+
if (spawnedBySessionId === undefined || spawnedBySessionId === "" || spawnedSessionId === undefined || spawnedSessionId === "")
|
|
1584
|
+
return undefined;
|
|
1585
|
+
const spawnedSessionFile = getString(data, "spawnedSessionFile");
|
|
1586
|
+
const cwd = getString(data, "cwd");
|
|
1587
|
+
return {
|
|
1588
|
+
spawnedBySessionId,
|
|
1589
|
+
spawnedSessionId,
|
|
1590
|
+
...(spawnedSessionFile === undefined || spawnedSessionFile === "" ? {} : { spawnedSessionFile }),
|
|
1591
|
+
...(cwd === undefined || cwd === "" ? {} : { cwd }),
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
function parsePersistedChildSubsessionLink(entry) {
|
|
1595
|
+
if (!isRecord(entry) || entry["type"] !== "custom" || entry["customType"] !== SUBSESSION_CHILD_LINK_CUSTOM_TYPE)
|
|
1596
|
+
return undefined;
|
|
1597
|
+
const data = entry["data"];
|
|
1598
|
+
if (!isRecord(data))
|
|
1599
|
+
return undefined;
|
|
1600
|
+
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
1601
|
+
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
1602
|
+
if (spawnedBySessionId === undefined || spawnedBySessionId === "" || spawnedSessionId === undefined || spawnedSessionId === "")
|
|
1603
|
+
return undefined;
|
|
1604
|
+
return { spawnedBySessionId, spawnedSessionId };
|
|
1605
|
+
}
|
|
1606
|
+
function nonEmptyString(value) {
|
|
1607
|
+
return value === undefined || value === "" ? undefined : value;
|
|
1608
|
+
}
|
|
1609
|
+
function subsessionHydratedParentKey(parentSessionId, parentSessionFile) {
|
|
1610
|
+
return `${parentSessionId}\0${parentSessionFile ?? ""}`;
|
|
1611
|
+
}
|
|
1612
|
+
function sessionPathsEqual(a, b) {
|
|
1613
|
+
return cwdPathsEqual(a, b);
|
|
1614
|
+
}
|
|
1615
|
+
function sessionFileMatches(session, expectedSessionFile) {
|
|
1616
|
+
const sessionFile = nonEmptyString(session.sessionFile);
|
|
1617
|
+
return sessionFile !== undefined && expectedSessionFile !== undefined && sessionPathsEqual(sessionFile, expectedSessionFile);
|
|
1618
|
+
}
|
|
1619
|
+
function activeSessionFileMatches(active, expectedSessionFile) {
|
|
1620
|
+
return sessionFileMatches(active.runtime.session, expectedSessionFile);
|
|
1621
|
+
}
|
|
1622
|
+
function trackedLinkParentFileMatches(link, parentSessionFile) {
|
|
1623
|
+
return link.parentSessionFile !== undefined && sessionPathsEqual(link.parentSessionFile, parentSessionFile);
|
|
1624
|
+
}
|
|
1625
|
+
async function readSessionHeaderSummary(sessionFile) {
|
|
1626
|
+
let file;
|
|
1627
|
+
try {
|
|
1628
|
+
file = await open(sessionFile, "r");
|
|
1629
|
+
const buffer = Buffer.alloc(4096);
|
|
1630
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
1631
|
+
const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n", 1)[0];
|
|
1632
|
+
if (firstLine === undefined || firstLine === "")
|
|
1633
|
+
return undefined;
|
|
1634
|
+
const header = JSON.parse(firstLine);
|
|
1635
|
+
if (!isRecord(header) || header["type"] !== "session" || typeof header["id"] !== "string")
|
|
1636
|
+
return undefined;
|
|
1637
|
+
const parentSession = getString(header, "parentSession");
|
|
1638
|
+
return { id: header["id"], ...(parentSession === undefined ? {} : { parentSession }) };
|
|
1639
|
+
}
|
|
1640
|
+
catch {
|
|
1641
|
+
return undefined;
|
|
1642
|
+
}
|
|
1643
|
+
finally {
|
|
1644
|
+
await file?.close().catch(() => undefined);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
async function sessionFileHeaderMatches(sessionFile, expected) {
|
|
1648
|
+
const header = await readSessionHeaderSummary(sessionFile);
|
|
1649
|
+
if (header?.id !== expected.sessionId)
|
|
1650
|
+
return false;
|
|
1651
|
+
if (expected.parentSessionFile === undefined)
|
|
1652
|
+
return true;
|
|
1653
|
+
return header.parentSession !== undefined && sessionPathsEqual(header.parentSession, expected.parentSessionFile);
|
|
1654
|
+
}
|
|
1200
1655
|
async function clearParentSession(sessionFile) {
|
|
1201
1656
|
const content = await readFile(sessionFile, "utf8");
|
|
1202
1657
|
const newlineIndex = content.indexOf("\n");
|
|
@@ -1210,6 +1665,11 @@ async function clearParentSession(sessionFile) {
|
|
|
1210
1665
|
delete header["parentSession"];
|
|
1211
1666
|
await writeFile(sessionFile, `${JSON.stringify(header)}${rest}`, "utf8");
|
|
1212
1667
|
}
|
|
1668
|
+
function clearParentSessionHeader(sessionManager) {
|
|
1669
|
+
const header = sessionManager.getHeader?.();
|
|
1670
|
+
if (header !== undefined && header !== null)
|
|
1671
|
+
delete header.parentSession;
|
|
1672
|
+
}
|
|
1213
1673
|
function clearSessionQueue(session) {
|
|
1214
1674
|
session.clearQueue();
|
|
1215
1675
|
}
|
|
@@ -1264,6 +1724,10 @@ function historyMessages(session) {
|
|
|
1264
1724
|
}
|
|
1265
1725
|
return messages;
|
|
1266
1726
|
}
|
|
1727
|
+
/** custom entry type used to persist parent -> child subsession links outside LLM context. */
|
|
1728
|
+
const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
|
|
1729
|
+
/** custom entry type used to mark a child as created by spawn_subsession. */
|
|
1730
|
+
const SUBSESSION_CHILD_LINK_CUSTOM_TYPE = "pi-web.subsession.spawned";
|
|
1267
1731
|
/** customType marking a parent-facing subsession-completion notice. */
|
|
1268
1732
|
const SUBSESSION_NOTIFICATION_CUSTOM_TYPE = "subsession.completion";
|
|
1269
1733
|
const SUBSESSION_NOTIFICATION_PREVIEW_CHARS = 2000;
|