@jmfederico/pi-web 1.202607.1 → 1.202607.2
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 +8 -8
- package/dist/cli.js +17 -5
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-uASb28Y1.js → CodeViewer-B4M13447.js} +1 -1
- package/dist/client/assets/{TerminalPanel-DfTmykFw.js → TerminalPanel-Bmvj-Ecn.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-laQN1x7P.js → UnifiedDiffViewer-C0hygwRo.js} +1 -1
- package/dist/client/assets/index-TvPsRyS1.js +3545 -0
- package/dist/client/index.html +1 -1
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/server/diagnostics/nodePtyNativeModule.js +36 -0
- package/dist/server/diagnostics/nodePtyNativeModule.js.map +1 -0
- package/dist/server/git/gitService.js +176 -5
- package/dist/server/git/gitService.js.map +1 -1
- package/dist/server/piWebStatus.js +6 -3
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +4 -0
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessiond.js +47 -7
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/authService.js +58 -7
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/globalProviderPolicy.js +98 -0
- package/dist/server/sessions/globalProviderPolicy.js.map +1 -0
- package/dist/server/sessions/modelCatalogRefresher.js +184 -0
- package/dist/server/sessions/modelCatalogRefresher.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +736 -100
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/plainTextTheme.js +112 -0
- package/dist/server/sessions/plainTextTheme.js.map +1 -0
- package/dist/server/sessions/sessionCommandService.js +68 -9
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionNotificationStore.js +484 -0
- package/dist/server/sessions/sessionNotificationStore.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +175 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionTreeProjection.js +250 -0
- package/dist/server/sessions/sessionTreeProjection.js.map +1 -0
- package/dist/server/sessions/sessionUnreadStore.js +520 -0
- package/dist/server/sessions/sessionUnreadStore.js.map +1 -0
- package/dist/server/sessions/spawnSessionTool.js +2 -2
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +10 -12
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +159 -2
- package/dist/shared/apiTypes.js +10 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +6 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +8 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/docs/config.md +32 -4
- package/docs/plugins.md +10 -6
- package/install.sh +1 -1
- package/package.json +8 -7
- package/dist/client/assets/index-Dhapi0oR.js +0 -3093
|
@@ -6,18 +6,26 @@ import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
|
6
6
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
7
7
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
8
8
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
9
|
+
import { projectSessionTree } from "./sessionTreeProjection.js";
|
|
9
10
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
10
11
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
11
12
|
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
12
13
|
import { computeEditPreview } from "./editPreview.js";
|
|
13
14
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
14
15
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
16
|
+
import { SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
15
17
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
16
18
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
17
19
|
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
18
20
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
19
21
|
import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
22
|
+
import { SessionNotificationStore, } from "./sessionNotificationStore.js";
|
|
23
|
+
import { plainTextTheme } from "./plainTextTheme.js";
|
|
24
|
+
import { SessionUnreadStore } from "./sessionUnreadStore.js";
|
|
20
25
|
const noopLogger = { info() { } };
|
|
26
|
+
const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
|
|
27
|
+
const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
|
|
28
|
+
const MAX_PENDING_UNREAD_MUTATIONS = SESSION_UNREAD_LIMIT + 1;
|
|
21
29
|
function noop() {
|
|
22
30
|
// Intentionally empty default unsubscribe callback.
|
|
23
31
|
}
|
|
@@ -38,6 +46,37 @@ function isPiSessionRef(ref) {
|
|
|
38
46
|
function lookupMatchesActiveSession(ref, active) {
|
|
39
47
|
return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
|
|
40
48
|
}
|
|
49
|
+
function sessionTreeNavigationOptions(request) {
|
|
50
|
+
switch (request.summary.mode) {
|
|
51
|
+
case "none":
|
|
52
|
+
return { summarize: false };
|
|
53
|
+
case "default":
|
|
54
|
+
return { summarize: true };
|
|
55
|
+
case "custom": {
|
|
56
|
+
const customInstructions = request.summary.instructions.trim();
|
|
57
|
+
if (customInstructions === "")
|
|
58
|
+
throw new Error("Custom branch-summary instructions are required");
|
|
59
|
+
if (customInstructions.length > SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH) {
|
|
60
|
+
throw new Error(`Custom branch-summary instructions must be at most ${String(SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH)} characters`);
|
|
61
|
+
}
|
|
62
|
+
return { summarize: true, customInstructions };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function decrementWeakCount(counts, key) {
|
|
67
|
+
const remaining = (counts.get(key) ?? 1) - 1;
|
|
68
|
+
if (remaining <= 0)
|
|
69
|
+
counts.delete(key);
|
|
70
|
+
else
|
|
71
|
+
counts.set(key, remaining);
|
|
72
|
+
}
|
|
73
|
+
function decrementMapCount(counts, key) {
|
|
74
|
+
const remaining = (counts.get(key) ?? 1) - 1;
|
|
75
|
+
if (remaining <= 0)
|
|
76
|
+
counts.delete(key);
|
|
77
|
+
else
|
|
78
|
+
counts.set(key, remaining);
|
|
79
|
+
}
|
|
41
80
|
function requirePromptText(value) {
|
|
42
81
|
if (typeof value !== "string")
|
|
43
82
|
throw new Error("Prompt text is required");
|
|
@@ -50,6 +89,8 @@ function parsePromptStreamingBehavior(value) {
|
|
|
50
89
|
return value;
|
|
51
90
|
throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
|
|
52
91
|
}
|
|
92
|
+
const CLEAR_RUNTIME_NOTIFICATIONS = { kind: "clear", reason: "runtime-close" };
|
|
93
|
+
const DEFER_RUNTIME_NOTIFICATIONS = { kind: "defer" };
|
|
53
94
|
function resourceDiagnosticToWarning(diagnostic, source) {
|
|
54
95
|
return {
|
|
55
96
|
severity: diagnostic.type === "error" ? "error" : "warning",
|
|
@@ -228,6 +269,15 @@ export class PiSessionService {
|
|
|
228
269
|
this.active = new Map();
|
|
229
270
|
this.pendingSessionOpens = new Map();
|
|
230
271
|
this.activities = new Map();
|
|
272
|
+
/** Runtime-identity gate held while Pi may await abandoned-branch summarization. */
|
|
273
|
+
this.treeNavigations = new WeakSet();
|
|
274
|
+
/** Counts async operations that may append an entry before they settle. */
|
|
275
|
+
this.sessionEntryMutationCounts = new WeakMap();
|
|
276
|
+
/** Runtime/session-identity reservations for operations that must not overlap tree navigation. */
|
|
277
|
+
this.treeExclusiveRuntimeOperationCounts = new WeakMap();
|
|
278
|
+
this.treeExclusiveSessionOperationCounts = new Map();
|
|
279
|
+
this.deferredSubsessionNotifications = new WeakMap();
|
|
280
|
+
this.deferredGeneratedSessionNames = new WeakMap();
|
|
231
281
|
this.compactionPromptQueues = new Map();
|
|
232
282
|
this.compactionDrainTimers = new Map();
|
|
233
283
|
this.authLossWarnings = new Set();
|
|
@@ -245,6 +295,10 @@ export class PiSessionService {
|
|
|
245
295
|
* child that works again (and stops again) notifies the parent each time.
|
|
246
296
|
*/
|
|
247
297
|
this.subsessionNotifyArmed = new Map();
|
|
298
|
+
this.notificationGenerationBySession = new WeakMap();
|
|
299
|
+
this.pendingUnreadMutations = [];
|
|
300
|
+
this.unreadPublicationFlushRequested = false;
|
|
301
|
+
this.unreadPublicationStopped = false;
|
|
248
302
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
249
303
|
this.agentDir = deps.agentDir;
|
|
250
304
|
this.sessionManager = deps.sessionManager;
|
|
@@ -252,6 +306,10 @@ export class PiSessionService {
|
|
|
252
306
|
this.spawnTargets = deps.spawnTargets;
|
|
253
307
|
this.logger = deps.logger ?? noopLogger;
|
|
254
308
|
this.now = deps.now ?? (() => new Date());
|
|
309
|
+
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
310
|
+
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
311
|
+
this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
|
|
312
|
+
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
255
313
|
// Subsessions are a beta capability gated behind their own flag, and they
|
|
256
314
|
// also require the spawn capability (they share its project-scope resolver).
|
|
257
315
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
@@ -266,19 +324,57 @@ export class PiSessionService {
|
|
|
266
324
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
267
325
|
this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, { echoUserMessage: false }), events, {
|
|
268
326
|
onCompactionStart: (session) => {
|
|
327
|
+
this.beginSessionEntryMutation(session, "compact the session");
|
|
269
328
|
this.publishActivity(session, "compacting", "active");
|
|
270
329
|
this.publishStatus(session);
|
|
271
330
|
},
|
|
272
331
|
onCompactionEnd: (session, result, detail) => {
|
|
332
|
+
this.endSessionEntryMutation(session);
|
|
273
333
|
this.publishActivity(session, result === "success" ? "compaction complete" : "compaction failed", result === "success" ? "idle" : "error", detail);
|
|
274
334
|
this.publishStatus(session);
|
|
275
335
|
},
|
|
276
336
|
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
337
|
+
getSessionTree: (session) => {
|
|
338
|
+
if (typeof session.sessionManager.getTree !== "function" || typeof session.navigateTree !== "function")
|
|
339
|
+
return undefined;
|
|
340
|
+
return projectSessionTree(session.sessionManager.getTree(), session.sessionManager.getLeafId());
|
|
341
|
+
},
|
|
342
|
+
hasActiveWork: (session) => this.hasActiveWork(session),
|
|
343
|
+
isTreeNavigationActive: (session) => this.treeNavigations.has(session),
|
|
344
|
+
runSessionReplacement: (session, operation) => this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before replacing the session", operation),
|
|
277
345
|
}, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
|
|
278
346
|
}
|
|
279
347
|
activeCount() {
|
|
280
348
|
return this.active.size;
|
|
281
349
|
}
|
|
350
|
+
notificationCatalog() {
|
|
351
|
+
return this.notificationStore.catalogSnapshot();
|
|
352
|
+
}
|
|
353
|
+
async unreadCatalog() {
|
|
354
|
+
await this.publishUnreadMutations([]);
|
|
355
|
+
return this.unreadStore.durableCatalogSnapshot();
|
|
356
|
+
}
|
|
357
|
+
async acknowledgeUnread(sessionId, request) {
|
|
358
|
+
const result = this.unreadStore.acknowledge(sessionId, {
|
|
359
|
+
...request,
|
|
360
|
+
cwd: canonicalizeStoredCwd(request.cwd),
|
|
361
|
+
});
|
|
362
|
+
await this.publishUnreadMutations(result.mutations);
|
|
363
|
+
return this.unreadStore.durableCatalogSnapshot();
|
|
364
|
+
}
|
|
365
|
+
notificationInbox(ref) {
|
|
366
|
+
return this.notificationStore.inboxSnapshot(ref.id, canonicalizeStoredCwd(ref.cwd));
|
|
367
|
+
}
|
|
368
|
+
dismissNotification(ref, request) {
|
|
369
|
+
const result = this.notificationStore.dismissNotification(ref.id, canonicalizeStoredCwd(ref.cwd), request.daemonInstanceId, request.notificationId);
|
|
370
|
+
this.publishNotificationMutations(result.mutations);
|
|
371
|
+
return result.snapshot;
|
|
372
|
+
}
|
|
373
|
+
dismissAllNotifications(ref, request) {
|
|
374
|
+
const result = this.notificationStore.dismissAll(ref.id, canonicalizeStoredCwd(ref.cwd), request.daemonInstanceId, request.throughOrder, request.throughOverflowWatermark);
|
|
375
|
+
this.publishNotificationMutations(result.mutations);
|
|
376
|
+
return result.snapshot;
|
|
377
|
+
}
|
|
282
378
|
async cleanupPreview(request) {
|
|
283
379
|
return previewResponseFromPlan(await this.cleanupPlan(request));
|
|
284
380
|
}
|
|
@@ -296,22 +392,24 @@ export class PiSessionService {
|
|
|
296
392
|
skippedBusySessionIds.add(input.sessionId);
|
|
297
393
|
continue;
|
|
298
394
|
}
|
|
299
|
-
await this.closeActive(input.sessionId);
|
|
395
|
+
await this.closeActive(input.sessionId, { kind: "clear", reason: "archive" });
|
|
300
396
|
readyArchiveInputs.push(input);
|
|
301
397
|
}
|
|
302
398
|
await this.archiveStoreArchiveMany(readyArchiveInputs);
|
|
303
399
|
archiveInputs.push(...readyArchiveInputs);
|
|
400
|
+
await this.forgetUnreadSessions(readyArchiveInputs);
|
|
304
401
|
for (const record of plan.deleteRecords) {
|
|
305
402
|
if (this.activeSessionHasWork(record.sessionId)) {
|
|
306
403
|
skippedBusySessionIds.add(record.sessionId);
|
|
307
404
|
continue;
|
|
308
405
|
}
|
|
309
|
-
await this.closeActive(record.sessionId);
|
|
406
|
+
await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
|
|
310
407
|
readyDeleteRecords.push(record);
|
|
311
408
|
}
|
|
312
409
|
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
313
410
|
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
314
411
|
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
412
|
+
await this.forgetUnreadSessions(deleteRecords);
|
|
315
413
|
return summarizeSessionCleanupExecution({
|
|
316
414
|
archiveInputs,
|
|
317
415
|
deleteRecords,
|
|
@@ -321,12 +419,16 @@ export class PiSessionService {
|
|
|
321
419
|
});
|
|
322
420
|
}
|
|
323
421
|
async dispose() {
|
|
422
|
+
this.unreadPublicationStopped = true;
|
|
423
|
+
this.clearUnreadPublicationRetry();
|
|
324
424
|
clearInterval(this.heartbeat);
|
|
325
425
|
this.clearCompactionDrainTimers();
|
|
326
426
|
const pendingOpens = this.pendingSessionOpenPromises();
|
|
327
427
|
if (pendingOpens.length > 0)
|
|
328
428
|
await Promise.allSettled(pendingOpens);
|
|
329
429
|
const activeSessions = Array.from(new Set(this.active.values()));
|
|
430
|
+
for (const active of activeSessions)
|
|
431
|
+
this.forgetUnreadActivity(active.runtime.session);
|
|
330
432
|
this.active.clear();
|
|
331
433
|
this.pendingSessionOpens.clear();
|
|
332
434
|
this.activities.clear();
|
|
@@ -337,16 +439,19 @@ export class PiSessionService {
|
|
|
337
439
|
this.subsessionLinks.clear();
|
|
338
440
|
this.subsessionHydratedParents.clear();
|
|
339
441
|
this.subsessionNotifyArmed.clear();
|
|
442
|
+
this.notificationStore.clearAll("service-dispose");
|
|
340
443
|
await Promise.all(activeSessions.map(async (active) => {
|
|
341
444
|
active.unsubscribe();
|
|
445
|
+
active.runtime.setRebindSession(undefined);
|
|
342
446
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
|
|
343
447
|
try {
|
|
344
|
-
await active.runtime.session
|
|
448
|
+
await this.abortSessionOperations(active.runtime.session);
|
|
345
449
|
}
|
|
346
450
|
finally {
|
|
347
451
|
await active.runtime.dispose();
|
|
348
452
|
}
|
|
349
453
|
}));
|
|
454
|
+
await this.publishUnreadMutations([]);
|
|
350
455
|
}
|
|
351
456
|
async list(cwd) {
|
|
352
457
|
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
|
|
@@ -355,8 +460,13 @@ export class PiSessionService {
|
|
|
355
460
|
.filter((record) => record.cwd === cwd)
|
|
356
461
|
.map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
|
|
357
462
|
const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
|
|
463
|
+
for (const record of archivedForCwd) {
|
|
464
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(record.sessionId, "archive-reconcile"));
|
|
465
|
+
}
|
|
358
466
|
const unarchivedSessions = sessions.filter((session) => !archivedById.has(session.id)).map(clientSessionFromListEntry);
|
|
359
|
-
|
|
467
|
+
const reconcilableSessionIds = this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById);
|
|
468
|
+
this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds);
|
|
469
|
+
await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
|
|
360
470
|
const archivedSessions = archivedForCwd
|
|
361
471
|
.sort(compareArchivedRecords)
|
|
362
472
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
@@ -431,7 +541,7 @@ export class PiSessionService {
|
|
|
431
541
|
...(parentSessionFile === undefined ? {} : { parentSessionFile }),
|
|
432
542
|
cwd: decision.cwd,
|
|
433
543
|
};
|
|
434
|
-
this.registerVerifiedSubsession(link);
|
|
544
|
+
await this.registerVerifiedSubsession(link);
|
|
435
545
|
this.persistSubsessionLink(link);
|
|
436
546
|
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
437
547
|
await this.prompt(created.id, input.prompt);
|
|
@@ -506,7 +616,7 @@ export class PiSessionService {
|
|
|
506
616
|
return undefined;
|
|
507
617
|
return sessionFileMatches(session, link.childSessionFile) ? link : undefined;
|
|
508
618
|
}
|
|
509
|
-
registerVerifiedSubsession(link) {
|
|
619
|
+
async registerVerifiedSubsession(link) {
|
|
510
620
|
const { childSessionId, parentSessionId } = link;
|
|
511
621
|
const previousParentId = this.subsessionParents.get(childSessionId);
|
|
512
622
|
if (previousParentId !== undefined && previousParentId !== parentSessionId) {
|
|
@@ -522,6 +632,24 @@ export class PiSessionService {
|
|
|
522
632
|
this.subsessionLinks.set(childSessionId, link);
|
|
523
633
|
if (!this.subsessionNotifyArmed.has(childSessionId))
|
|
524
634
|
this.subsessionNotifyArmed.set(childSessionId, false);
|
|
635
|
+
const cwd = this.cwdForVerifiedSubsession(link);
|
|
636
|
+
await this.publishUnreadMutations(this.unreadStore.excludeSession(childSessionId, cwd));
|
|
637
|
+
}
|
|
638
|
+
cwdForVerifiedSubsession(link) {
|
|
639
|
+
const activeCwd = this.activeChildForSubsessionLink(link)?.runtime.session.sessionManager.getCwd();
|
|
640
|
+
const linkedCwd = nonEmptyString(activeCwd) ?? nonEmptyString(link.cwd);
|
|
641
|
+
if (linkedCwd !== undefined)
|
|
642
|
+
return canonicalizeStoredCwd(linkedCwd);
|
|
643
|
+
const childSessionFile = link.childSessionFile;
|
|
644
|
+
if (childSessionFile !== undefined) {
|
|
645
|
+
try {
|
|
646
|
+
return canonicalizeStoredCwd(this.sessionManager.open(childSessionFile).getCwd());
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
throw new Error("Could not resolve cwd for verified tracked sub-session", { cause: error });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
throw new Error("Could not resolve cwd for verified tracked sub-session");
|
|
525
653
|
}
|
|
526
654
|
unregisterSubsession(childSessionId) {
|
|
527
655
|
const parentSessionId = this.subsessionParents.get(childSessionId);
|
|
@@ -568,40 +696,44 @@ export class PiSessionService {
|
|
|
568
696
|
const activeParent = this.active.get(parentSessionId);
|
|
569
697
|
if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
|
|
570
698
|
const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
|
|
571
|
-
await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
572
|
-
|
|
699
|
+
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
700
|
+
if (complete)
|
|
701
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
573
702
|
return;
|
|
574
703
|
}
|
|
575
704
|
if (parentSessionFile === undefined)
|
|
576
705
|
return;
|
|
577
|
-
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId)
|
|
578
|
-
this.subsessionHydratedParents.add(hydrationKey);
|
|
706
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId)
|
|
579
707
|
return;
|
|
580
|
-
}
|
|
581
708
|
let parentManager;
|
|
582
709
|
try {
|
|
583
710
|
parentManager = this.sessionManager.open(parentSessionFile);
|
|
584
711
|
}
|
|
585
712
|
catch {
|
|
586
|
-
this.subsessionHydratedParents.add(hydrationKey);
|
|
587
713
|
return;
|
|
588
714
|
}
|
|
589
|
-
await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
|
|
590
|
-
|
|
715
|
+
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
|
|
716
|
+
if (complete)
|
|
717
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
591
718
|
}
|
|
592
719
|
async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile) {
|
|
593
720
|
// Parent custom links are the authoritative recovery record: verify the
|
|
594
|
-
// exact live child file/header before tracking.
|
|
721
|
+
// exact live child file/header before tracking. Do not negatively cache a
|
|
722
|
+
// scan while a candidate child is temporarily unavailable.
|
|
595
723
|
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
724
|
+
let complete = true;
|
|
596
725
|
for (const entry of entries) {
|
|
597
726
|
const link = parsePersistedParentSubsessionLink(entry);
|
|
598
|
-
if (link
|
|
727
|
+
if (link?.spawnedBySessionId !== parentSessionId)
|
|
599
728
|
continue;
|
|
600
729
|
const verified = await this.verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link);
|
|
601
|
-
if (verified === undefined)
|
|
730
|
+
if (verified === undefined) {
|
|
731
|
+
complete = false;
|
|
602
732
|
continue;
|
|
603
|
-
|
|
733
|
+
}
|
|
734
|
+
await this.registerVerifiedSubsession(verified);
|
|
604
735
|
}
|
|
736
|
+
return complete;
|
|
605
737
|
}
|
|
606
738
|
async verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link) {
|
|
607
739
|
if (parentSessionFile === undefined)
|
|
@@ -620,7 +752,7 @@ export class PiSessionService {
|
|
|
620
752
|
const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
|
|
621
753
|
if (link === undefined)
|
|
622
754
|
return;
|
|
623
|
-
this.registerVerifiedSubsession(link);
|
|
755
|
+
await this.registerVerifiedSubsession(link);
|
|
624
756
|
}
|
|
625
757
|
verifiedSubsessionLinkFromOpenedChild(session) {
|
|
626
758
|
return verifiedTrackedSubsessionLink(this.sessionManager, {
|
|
@@ -729,13 +861,25 @@ export class PiSessionService {
|
|
|
729
861
|
async notifyParentOfSubsession(parentId, childId, text) {
|
|
730
862
|
try {
|
|
731
863
|
const session = await this.getOrOpenParentForSubsession(parentId, childId);
|
|
732
|
-
|
|
733
|
-
|
|
864
|
+
if (this.treeNavigations.has(session)) {
|
|
865
|
+
const pending = this.deferredSubsessionNotifications.get(session) ?? [];
|
|
866
|
+
pending.push({ parentId, childId, text });
|
|
867
|
+
this.deferredSubsessionNotifications.set(session, pending);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
await this.deliverSubsessionNotification(session, { parentId, childId, text });
|
|
734
871
|
}
|
|
735
872
|
catch (error) {
|
|
736
|
-
this.
|
|
873
|
+
this.logSubsessionNotificationFailure(parentId, childId, error);
|
|
737
874
|
}
|
|
738
875
|
}
|
|
876
|
+
async deliverSubsessionNotification(session, notification) {
|
|
877
|
+
await this.runSessionEntryMutation(session, "deliver a subsession notification", () => session.sendCustomMessage({ customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, content: notification.text, display: true, details: { sessionId: notification.childId } }, { triggerTurn: true, deliverAs: "followUp" }));
|
|
878
|
+
this.publishStatus(session);
|
|
879
|
+
}
|
|
880
|
+
logSubsessionNotificationFailure(parentId, childId, error) {
|
|
881
|
+
this.logger.info({ parentSessionId: parentId, sessionId: childId, error: error instanceof Error ? error.message : String(error) }, "failed to notify parent of subsession completion");
|
|
882
|
+
}
|
|
739
883
|
async messages(ref, page) {
|
|
740
884
|
const session = await this.getOrOpen(ref);
|
|
741
885
|
return pageMessagesAtSafeBoundary(historyMessages(session), page);
|
|
@@ -764,7 +908,7 @@ export class PiSessionService {
|
|
|
764
908
|
}
|
|
765
909
|
async availableModels(ref) {
|
|
766
910
|
const session = await this.getOrOpen(ref);
|
|
767
|
-
await session.modelRuntime.
|
|
911
|
+
await session.modelRuntime.refresh();
|
|
768
912
|
const models = session.scopedModels.length > 0
|
|
769
913
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
770
914
|
: session.modelRuntime.getAvailableSnapshot();
|
|
@@ -773,7 +917,9 @@ export class PiSessionService {
|
|
|
773
917
|
async setModel(ref, provider, modelId) {
|
|
774
918
|
await this.assertWritable(ref);
|
|
775
919
|
const session = await this.getOrOpen(ref);
|
|
776
|
-
|
|
920
|
+
this.assertTreeNavigationInactive(session, "change models");
|
|
921
|
+
await session.modelRuntime.refresh();
|
|
922
|
+
this.assertTreeNavigationInactive(session, "change models");
|
|
777
923
|
const candidates = session.scopedModels.length > 0
|
|
778
924
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
779
925
|
: session.modelRuntime.getAvailableSnapshot();
|
|
@@ -781,7 +927,7 @@ export class PiSessionService {
|
|
|
781
927
|
?? session.modelRuntime.getModel(provider, modelId);
|
|
782
928
|
if (model === undefined)
|
|
783
929
|
throw new Error(`Model not found: ${provider}/${modelId}`);
|
|
784
|
-
await session.setModel(model);
|
|
930
|
+
await this.runSessionEntryMutation(session, "change models", () => session.setModel(model));
|
|
785
931
|
this.publishActivity(session, `model: ${model.id}`, "idle", model.provider);
|
|
786
932
|
this.publishStatus(session);
|
|
787
933
|
return this.statusFromSession(session);
|
|
@@ -789,7 +935,7 @@ export class PiSessionService {
|
|
|
789
935
|
async cycleModel(ref, direction) {
|
|
790
936
|
await this.assertWritable(ref);
|
|
791
937
|
const session = await this.getOrOpen(ref);
|
|
792
|
-
const result = await session.cycleModel(direction);
|
|
938
|
+
const result = await this.runSessionEntryMutation(session, "change models", () => session.cycleModel(direction));
|
|
793
939
|
if (result === undefined)
|
|
794
940
|
throw new Error(session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available");
|
|
795
941
|
this.publishActivity(session, `model: ${result.model.id}`, "idle", result.model.provider);
|
|
@@ -803,6 +949,7 @@ export class PiSessionService {
|
|
|
803
949
|
async setThinkingLevel(ref, level) {
|
|
804
950
|
await this.assertWritable(ref);
|
|
805
951
|
const session = await this.getOrOpen(ref);
|
|
952
|
+
this.assertTreeNavigationInactive(session, "change the thinking level");
|
|
806
953
|
// pi owns the valid set; validate against the session's live levels rather
|
|
807
954
|
// than a hardcoded union so this stays correct if pi changes the set.
|
|
808
955
|
const available = session.getAvailableThinkingLevels();
|
|
@@ -817,6 +964,7 @@ export class PiSessionService {
|
|
|
817
964
|
async cycleThinkingLevel(ref) {
|
|
818
965
|
await this.assertWritable(ref);
|
|
819
966
|
const session = await this.getOrOpen(ref);
|
|
967
|
+
this.assertTreeNavigationInactive(session, "change the thinking level");
|
|
820
968
|
const level = session.cycleThinkingLevel();
|
|
821
969
|
if (level === undefined)
|
|
822
970
|
throw new Error("Current model does not support thinking");
|
|
@@ -850,6 +998,7 @@ export class PiSessionService {
|
|
|
850
998
|
const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
|
|
851
999
|
await this.assertWritable(ref);
|
|
852
1000
|
const session = await this.getOrOpen(ref);
|
|
1001
|
+
this.assertTreeNavigationInactive(session, "send a prompt");
|
|
853
1002
|
this.maybeGenerateSessionName(session, promptText);
|
|
854
1003
|
const isQueued = session.isStreaming || session.isCompacting;
|
|
855
1004
|
const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
|
|
@@ -869,7 +1018,7 @@ export class PiSessionService {
|
|
|
869
1018
|
if (behavior === undefined && echoUserMessage)
|
|
870
1019
|
this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) });
|
|
871
1020
|
const promptOptions = buildPromptOptions(behavior, images);
|
|
872
|
-
const promptPromise = session.prompt(text, promptOptions).catch((error) => {
|
|
1021
|
+
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions)).catch((error) => {
|
|
873
1022
|
const message = error instanceof Error ? error.message : String(error);
|
|
874
1023
|
this.publishActivity(session, "error", "error", message);
|
|
875
1024
|
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
@@ -896,6 +1045,7 @@ export class PiSessionService {
|
|
|
896
1045
|
await this.assertWritable(ref);
|
|
897
1046
|
const active = await this.getActive(ref);
|
|
898
1047
|
const { session } = active.runtime;
|
|
1048
|
+
this.assertTreeNavigationInactive(session, "run a shell command");
|
|
899
1049
|
const isExcluded = text.startsWith("!!");
|
|
900
1050
|
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
|
901
1051
|
if (!command)
|
|
@@ -904,11 +1054,11 @@ export class PiSessionService {
|
|
|
904
1054
|
throw new Error("A bash command is already running");
|
|
905
1055
|
this.publishActivity(session, "running bash", "active", command);
|
|
906
1056
|
this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded });
|
|
907
|
-
void session.executeBash(command, (chunk) => {
|
|
1057
|
+
void this.runSessionEntryMutation(session, "run a shell command", () => session.executeBash(command, (chunk) => {
|
|
908
1058
|
this.events.publish(session.sessionId, { type: "shell.chunk", chunk });
|
|
909
1059
|
this.publishActivity(session, "running bash", "active", command);
|
|
910
1060
|
this.publishStatus(session);
|
|
911
|
-
}, { excludeFromContext: isExcluded }).then((result) => {
|
|
1061
|
+
}, { excludeFromContext: isExcluded })).then((result) => {
|
|
912
1062
|
this.events.publish(session.sessionId, {
|
|
913
1063
|
type: "shell.end",
|
|
914
1064
|
output: result.output,
|
|
@@ -937,30 +1087,103 @@ export class PiSessionService {
|
|
|
937
1087
|
const active = await this.getActive(ref);
|
|
938
1088
|
return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
|
|
939
1089
|
}
|
|
940
|
-
async
|
|
1090
|
+
async navigateTree(ref, request) {
|
|
1091
|
+
if (request.targetId.trim() === "")
|
|
1092
|
+
throw new Error("Session tree target is required");
|
|
1093
|
+
if (this.isTreeExclusiveSessionIdentityActive(sessionIdFromLookup(ref))) {
|
|
1094
|
+
throw new Error("Stop current session activity before navigating the session tree");
|
|
1095
|
+
}
|
|
1096
|
+
await this.assertWritable(ref);
|
|
1097
|
+
const options = sessionTreeNavigationOptions(request);
|
|
1098
|
+
const session = await this.getOrOpen(ref);
|
|
1099
|
+
if (typeof session.navigateTree !== "function")
|
|
1100
|
+
throw new Error("Session tree navigation is not supported by this Pi runtime");
|
|
941
1101
|
if (this.hasActiveWork(session))
|
|
942
|
-
throw new Error("Stop current session activity before
|
|
943
|
-
|
|
1102
|
+
throw new Error("Stop current session activity before navigating the session tree");
|
|
1103
|
+
// Acquire synchronously after the active-work check. No leaf-producing work
|
|
1104
|
+
// may enter this runtime until Pi's potentially asynchronous summary settles.
|
|
1105
|
+
this.treeNavigations.add(session);
|
|
944
1106
|
try {
|
|
945
|
-
|
|
946
|
-
|
|
1107
|
+
if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
|
|
1108
|
+
throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
|
|
1109
|
+
}
|
|
1110
|
+
this.publishActivity(session, options.summarize ? "summarizing branch" : "navigating session tree", "active");
|
|
947
1111
|
this.publishStatus(session);
|
|
1112
|
+
const result = await session.navigateTree(request.targetId, options);
|
|
1113
|
+
if (result.cancelled) {
|
|
1114
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1115
|
+
this.publishActivity(session, result.aborted === true ? "branch summary aborted" : "tree navigation cancelled", "idle");
|
|
1116
|
+
}
|
|
1117
|
+
return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
|
|
1118
|
+
}
|
|
1119
|
+
if (this.isCurrentActiveSession(session))
|
|
1120
|
+
this.publishActivity(session, "session tree navigated", "idle");
|
|
1121
|
+
return { cancelled: false, ...(result.editorText === undefined ? {} : { editorText: result.editorText }) };
|
|
948
1122
|
}
|
|
949
1123
|
catch (error) {
|
|
950
1124
|
const message = error instanceof Error ? error.message : String(error);
|
|
951
|
-
this.
|
|
952
|
-
|
|
953
|
-
|
|
1125
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1126
|
+
this.publishActivity(session, "tree navigation failed", "error", message);
|
|
1127
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1128
|
+
}
|
|
954
1129
|
throw error;
|
|
955
1130
|
}
|
|
1131
|
+
finally {
|
|
1132
|
+
this.treeNavigations.delete(session);
|
|
1133
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1134
|
+
this.flushDeferredTreeNavigationWork(session);
|
|
1135
|
+
this.publishStatus(session);
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
this.deferredGeneratedSessionNames.delete(session);
|
|
1139
|
+
this.deferredSubsessionNotifications.delete(session);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async reloadSessionRuntime(session) {
|
|
1144
|
+
if (this.hasActiveWork(session))
|
|
1145
|
+
throw new Error("Stop current session activity before reloading");
|
|
1146
|
+
await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before reloading", async () => {
|
|
1147
|
+
this.publishActivity(session, "reloading resources", "active");
|
|
1148
|
+
const priorGeneration = this.notificationGenerationBySession.get(session);
|
|
1149
|
+
let candidateGeneration;
|
|
1150
|
+
try {
|
|
1151
|
+
await session.reload(priorGeneration === undefined ? undefined : {
|
|
1152
|
+
beforeSessionStart: () => {
|
|
1153
|
+
candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
1154
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1155
|
+
this.replaceSessionNotificationContext(session, candidateGeneration);
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
if (candidateGeneration !== undefined) {
|
|
1159
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1160
|
+
}
|
|
1161
|
+
this.publishActivity(session, "resources reloaded", "idle");
|
|
1162
|
+
this.publishStatus(session);
|
|
1163
|
+
}
|
|
1164
|
+
catch (error) {
|
|
1165
|
+
if (candidateGeneration !== undefined) {
|
|
1166
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
|
|
1167
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1168
|
+
}
|
|
1169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1170
|
+
this.publishActivity(session, "reload failed", "error", message);
|
|
1171
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1172
|
+
this.publishStatus(session);
|
|
1173
|
+
throw error;
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
956
1176
|
}
|
|
957
1177
|
async archive(ref) {
|
|
958
1178
|
const session = await this.getOrOpen(ref);
|
|
959
1179
|
if (this.hasActiveWork(session))
|
|
960
1180
|
throw new Error("Stop current session activity before archiving");
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1181
|
+
await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before archiving", async () => {
|
|
1182
|
+
const archiveInput = await this.archiveInputForSession(session);
|
|
1183
|
+
await this.closeActive(session.sessionId, { kind: "clear", reason: "archive" });
|
|
1184
|
+
await this.archiveStore.archive(archiveInput);
|
|
1185
|
+
await this.forgetUnreadSessions([archiveInput]);
|
|
1186
|
+
});
|
|
964
1187
|
}
|
|
965
1188
|
async archiveMany(refs) {
|
|
966
1189
|
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
@@ -970,11 +1193,14 @@ export class PiSessionService {
|
|
|
970
1193
|
]);
|
|
971
1194
|
const failures = [];
|
|
972
1195
|
const alreadyArchivedSessionIds = [];
|
|
1196
|
+
const unreadArchivedIdentities = [];
|
|
973
1197
|
const planItems = [];
|
|
974
1198
|
for (const ref of uniqueRefs) {
|
|
975
1199
|
const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
976
1200
|
if (archived !== undefined) {
|
|
1201
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(archived.sessionId, "archive"));
|
|
977
1202
|
alreadyArchivedSessionIds.push(archived.sessionId);
|
|
1203
|
+
unreadArchivedIdentities.push(archived);
|
|
978
1204
|
continue;
|
|
979
1205
|
}
|
|
980
1206
|
const active = this.activeForLookup(bulkRefToLookup(ref));
|
|
@@ -999,25 +1225,41 @@ export class PiSessionService {
|
|
|
999
1225
|
failures.push({ sessionId: resolvedSessionId, error: errorMessage(error) });
|
|
1000
1226
|
}
|
|
1001
1227
|
}
|
|
1002
|
-
const
|
|
1228
|
+
const readyPlanItems = [];
|
|
1003
1229
|
for (const item of planItems) {
|
|
1230
|
+
const active = this.activeForLookup({ id: item.input.sessionId, cwd: item.input.cwd });
|
|
1231
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1232
|
+
failures.push({ sessionId: item.input.sessionId, error: "Stop current session activity before archiving" });
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
readyPlanItems.push(active === undefined ? item : { ...item, active });
|
|
1236
|
+
}
|
|
1237
|
+
const readyInputs = [];
|
|
1238
|
+
const archivedSessionIds = [...alreadyArchivedSessionIds];
|
|
1239
|
+
await this.runTreeExclusiveOperation(readyPlanItems.map(({ input, active }) => ({
|
|
1240
|
+
sessionId: input.sessionId,
|
|
1241
|
+
...(active === undefined ? {} : { session: active.runtime.session, runtime: active.runtime }),
|
|
1242
|
+
})), "Stop current session activity before archiving", async () => {
|
|
1243
|
+
for (const item of readyPlanItems) {
|
|
1244
|
+
try {
|
|
1245
|
+
await this.closeActive(item.input.sessionId, { kind: "clear", reason: "archive" });
|
|
1246
|
+
readyInputs.push(item.input);
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
failures.push({ sessionId: item.input.sessionId, error: errorMessage(error) });
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1004
1252
|
try {
|
|
1005
|
-
await this.
|
|
1006
|
-
|
|
1253
|
+
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
1254
|
+
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
1255
|
+
unreadArchivedIdentities.push(...archived);
|
|
1007
1256
|
}
|
|
1008
1257
|
catch (error) {
|
|
1009
|
-
|
|
1258
|
+
for (const input of readyInputs)
|
|
1259
|
+
failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
|
|
1010
1260
|
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
try {
|
|
1014
|
-
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
1015
|
-
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
1016
|
-
}
|
|
1017
|
-
catch (error) {
|
|
1018
|
-
for (const input of readyInputs)
|
|
1019
|
-
failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
|
|
1020
|
-
}
|
|
1261
|
+
});
|
|
1262
|
+
await this.forgetUnreadSessions(unreadArchivedIdentities);
|
|
1021
1263
|
return {
|
|
1022
1264
|
archived: true,
|
|
1023
1265
|
archivedSessionIds: uniqueStrings(archivedSessionIds),
|
|
@@ -1034,9 +1276,19 @@ export class PiSessionService {
|
|
|
1034
1276
|
if (busy !== undefined)
|
|
1035
1277
|
throw new Error(`Stop current session activity before archiving ${sessionDisplayName(busy)}`);
|
|
1036
1278
|
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1279
|
+
await this.runTreeExclusiveOperation(plan.unarchivedTargets.map((target) => ({
|
|
1280
|
+
sessionId: target.id,
|
|
1281
|
+
...(target.activeSession === undefined ? {} : { session: target.activeSession }),
|
|
1282
|
+
})), `Stop current session activity before archiving ${sessionDisplayName(session)}`, async () => {
|
|
1283
|
+
for (const target of plan.targets) {
|
|
1284
|
+
if (target.archived)
|
|
1285
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(target.id, "archive"));
|
|
1286
|
+
}
|
|
1287
|
+
for (const input of archiveInputs)
|
|
1288
|
+
await this.closeActive(input.sessionId, { kind: "clear", reason: "archive" });
|
|
1289
|
+
await this.archiveStoreArchiveMany(archiveInputs);
|
|
1290
|
+
});
|
|
1291
|
+
await this.forgetUnreadSessions(plan.targets.map((target) => ({ sessionId: target.id, cwd: target.cwd })));
|
|
1040
1292
|
return {
|
|
1041
1293
|
archived: true,
|
|
1042
1294
|
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
@@ -1048,8 +1300,9 @@ export class PiSessionService {
|
|
|
1048
1300
|
const archived = await this.getArchived(ref);
|
|
1049
1301
|
if (archived === undefined)
|
|
1050
1302
|
throw new Error("Session not found");
|
|
1051
|
-
await this.closeActive(archived.sessionId);
|
|
1303
|
+
await this.closeActive(archived.sessionId, { kind: "clear", reason: "restore" });
|
|
1052
1304
|
await this.archiveStore.restore(archived.sessionId);
|
|
1305
|
+
await this.forgetUnreadSessions([archived]);
|
|
1053
1306
|
}
|
|
1054
1307
|
async deleteArchived(ref) {
|
|
1055
1308
|
const record = await this.getArchived(ref);
|
|
@@ -1057,10 +1310,11 @@ export class PiSessionService {
|
|
|
1057
1310
|
throw new Error("Archived session not found");
|
|
1058
1311
|
if (this.archiveStore.deleteArchived === undefined)
|
|
1059
1312
|
throw new Error("Archive store does not support deletion");
|
|
1060
|
-
await this.closeActive(record.sessionId);
|
|
1313
|
+
await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
|
|
1061
1314
|
if (record.archivePath === undefined)
|
|
1062
1315
|
await this.ensureArchivedRecordMoved(record);
|
|
1063
1316
|
await this.archiveStore.deleteArchived(record.sessionId);
|
|
1317
|
+
await this.forgetUnreadSessions([record]);
|
|
1064
1318
|
}
|
|
1065
1319
|
async deleteArchivedMany(refs) {
|
|
1066
1320
|
if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
@@ -1085,7 +1339,7 @@ export class PiSessionService {
|
|
|
1085
1339
|
const readyRecords = [];
|
|
1086
1340
|
for (const item of planItems) {
|
|
1087
1341
|
try {
|
|
1088
|
-
await this.closeActive(item.record.sessionId);
|
|
1342
|
+
await this.closeActive(item.record.sessionId, { kind: "clear", reason: "delete" });
|
|
1089
1343
|
readyRecords.push(item.record);
|
|
1090
1344
|
}
|
|
1091
1345
|
catch (error) {
|
|
@@ -1106,6 +1360,8 @@ export class PiSessionService {
|
|
|
1106
1360
|
for (const sessionId of deleteIds)
|
|
1107
1361
|
failures.push({ sessionId, error: errorMessage(error) });
|
|
1108
1362
|
}
|
|
1363
|
+
const deletedIdSet = new Set(deletedSessionIds);
|
|
1364
|
+
await this.forgetUnreadSessions(readyRecords.filter((record) => deletedIdSet.has(record.sessionId)));
|
|
1109
1365
|
return {
|
|
1110
1366
|
deleted: true,
|
|
1111
1367
|
deletedSessionIds,
|
|
@@ -1118,9 +1374,29 @@ export class PiSessionService {
|
|
|
1118
1374
|
const session = await this.getOrOpen(ref);
|
|
1119
1375
|
if (this.hasActiveWork(session))
|
|
1120
1376
|
throw new Error("Stop current session activity before reloading");
|
|
1121
|
-
await this.
|
|
1122
|
-
|
|
1123
|
-
|
|
1377
|
+
const reopenedSession = await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before reloading", async () => {
|
|
1378
|
+
const priorGeneration = this.notificationGenerationBySession.get(session);
|
|
1379
|
+
const { sessionId, cwd } = notificationIdentityForSession(session);
|
|
1380
|
+
let candidateGeneration;
|
|
1381
|
+
try {
|
|
1382
|
+
await this.closeActive(sessionId, priorGeneration === undefined ? CLEAR_RUNTIME_NOTIFICATIONS : DEFER_RUNTIME_NOTIFICATIONS);
|
|
1383
|
+
candidateGeneration = priorGeneration === undefined
|
|
1384
|
+
? undefined
|
|
1385
|
+
: this.notificationStore.beginReplacement(priorGeneration, { sessionId, cwd });
|
|
1386
|
+
const reopened = await this.getActive(ref, candidateGeneration === undefined ? {} : { notificationGeneration: candidateGeneration });
|
|
1387
|
+
if (candidateGeneration !== undefined) {
|
|
1388
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1389
|
+
}
|
|
1390
|
+
return reopened.runtime.session;
|
|
1391
|
+
}
|
|
1392
|
+
catch (error) {
|
|
1393
|
+
if (candidateGeneration !== undefined) {
|
|
1394
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration));
|
|
1395
|
+
}
|
|
1396
|
+
throw error;
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
this.publishStatus(reopenedSession);
|
|
1124
1400
|
}
|
|
1125
1401
|
async detachParent(ref) {
|
|
1126
1402
|
const session = await this.getOrOpen(ref);
|
|
@@ -1130,6 +1406,7 @@ export class PiSessionService {
|
|
|
1130
1406
|
await clearParentSession(sessionFile);
|
|
1131
1407
|
clearParentSessionHeader(session.sessionManager);
|
|
1132
1408
|
this.unregisterSubsession(session.sessionId);
|
|
1409
|
+
await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }]);
|
|
1133
1410
|
}
|
|
1134
1411
|
async clearQueue(ref) {
|
|
1135
1412
|
await this.assertWritable(ref);
|
|
@@ -1152,17 +1429,30 @@ export class PiSessionService {
|
|
|
1152
1429
|
const sessionId = active.runtime.session.sessionId;
|
|
1153
1430
|
this.clearCompactionPromptQueue(sessionId);
|
|
1154
1431
|
clearSessionQueue(active.runtime.session);
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1432
|
+
try {
|
|
1433
|
+
await this.abortSessionOperations(active.runtime.session);
|
|
1434
|
+
this.publishActivity(active.runtime.session, "stopped", "idle");
|
|
1435
|
+
}
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1438
|
+
this.publishActivity(active.runtime.session, "stop failed", "error", message);
|
|
1439
|
+
throw error;
|
|
1440
|
+
}
|
|
1441
|
+
finally {
|
|
1442
|
+
this.publishStatus(active.runtime.session);
|
|
1443
|
+
}
|
|
1158
1444
|
}
|
|
1159
|
-
stop(ref) {
|
|
1445
|
+
async stop(ref) {
|
|
1160
1446
|
const active = this.activeForLookup(ref);
|
|
1161
|
-
if (active
|
|
1447
|
+
if (active !== undefined) {
|
|
1448
|
+
await this.closeActive(active.runtime.session.sessionId);
|
|
1162
1449
|
return;
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1450
|
+
}
|
|
1451
|
+
if (isPiSessionRef(ref)) {
|
|
1452
|
+
this.publishNotificationMutations(this.notificationStore.clearSessionIdentity(ref.id, canonicalizeStoredCwd(ref.cwd), "runtime-close"));
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
await this.closeActive(ref);
|
|
1166
1456
|
}
|
|
1167
1457
|
async bulkSessionLookupContext(refs) {
|
|
1168
1458
|
const cwdSet = new Set();
|
|
@@ -1351,13 +1641,21 @@ export class PiSessionService {
|
|
|
1351
1641
|
}
|
|
1352
1642
|
return [...names];
|
|
1353
1643
|
}
|
|
1354
|
-
async closeActive(sessionId) {
|
|
1644
|
+
async closeActive(sessionId, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
|
|
1355
1645
|
const pendingOpens = this.pendingSessionOpenPromises(sessionId);
|
|
1356
1646
|
if (pendingOpens.length > 0)
|
|
1357
1647
|
await Promise.allSettled(pendingOpens);
|
|
1358
1648
|
const active = this.active.get(sessionId);
|
|
1649
|
+
if (notificationPolicy.kind === "clear") {
|
|
1650
|
+
const generation = active === undefined ? undefined : this.notificationGenerationBySession.get(active.runtime.session);
|
|
1651
|
+
const mutations = generation === undefined
|
|
1652
|
+
? this.notificationStore.clearSession(sessionId, notificationPolicy.reason)
|
|
1653
|
+
: this.notificationStore.clearGeneration(generation, notificationPolicy.reason);
|
|
1654
|
+
this.publishNotificationMutations(mutations);
|
|
1655
|
+
}
|
|
1359
1656
|
if (!active)
|
|
1360
1657
|
return;
|
|
1658
|
+
this.forgetUnreadActivity(active.runtime.session);
|
|
1361
1659
|
this.active.delete(sessionId);
|
|
1362
1660
|
this.activities.delete(sessionId);
|
|
1363
1661
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
|
|
@@ -1370,13 +1668,36 @@ export class PiSessionService {
|
|
|
1370
1668
|
this.subsessionNotifyArmed.delete(sessionId);
|
|
1371
1669
|
clearSessionQueue(active.runtime.session);
|
|
1372
1670
|
active.unsubscribe();
|
|
1671
|
+
active.runtime.setRebindSession(undefined);
|
|
1373
1672
|
try {
|
|
1374
|
-
await active.runtime.session
|
|
1673
|
+
await this.abortSessionOperations(active.runtime.session);
|
|
1375
1674
|
}
|
|
1376
1675
|
finally {
|
|
1377
1676
|
await active.runtime.dispose();
|
|
1378
1677
|
}
|
|
1379
1678
|
}
|
|
1679
|
+
async abortSessionOperations(session) {
|
|
1680
|
+
let branchSummaryAbortFailed = false;
|
|
1681
|
+
let branchSummaryAbortError;
|
|
1682
|
+
try {
|
|
1683
|
+
session.abortBranchSummary?.();
|
|
1684
|
+
}
|
|
1685
|
+
catch (error) {
|
|
1686
|
+
branchSummaryAbortFailed = true;
|
|
1687
|
+
branchSummaryAbortError = error;
|
|
1688
|
+
}
|
|
1689
|
+
try {
|
|
1690
|
+
await session.abort();
|
|
1691
|
+
}
|
|
1692
|
+
catch (abortError) {
|
|
1693
|
+
if (branchSummaryAbortFailed) {
|
|
1694
|
+
throw new AggregateError([branchSummaryAbortError, abortError], "Failed to abort session operations", { cause: abortError });
|
|
1695
|
+
}
|
|
1696
|
+
throw abortError;
|
|
1697
|
+
}
|
|
1698
|
+
if (branchSummaryAbortFailed)
|
|
1699
|
+
throw branchSummaryAbortError;
|
|
1700
|
+
}
|
|
1380
1701
|
async assertWritable(ref) {
|
|
1381
1702
|
if (await this.getArchived(ref) !== undefined)
|
|
1382
1703
|
throw new Error("Archived sessions are read-only. Restore the session to continue.");
|
|
@@ -1384,23 +1705,23 @@ export class PiSessionService {
|
|
|
1384
1705
|
async getOrOpen(ref) {
|
|
1385
1706
|
return (await this.getActive(ref)).runtime.session;
|
|
1386
1707
|
}
|
|
1387
|
-
async getActive(ref) {
|
|
1708
|
+
async getActive(ref, options = {}) {
|
|
1388
1709
|
const active = this.activeForLookup(ref);
|
|
1389
1710
|
if (active !== undefined)
|
|
1390
1711
|
return active;
|
|
1391
1712
|
const archived = await this.getArchived(ref);
|
|
1392
1713
|
if (archived?.archivePath !== undefined) {
|
|
1393
1714
|
const { archivePath } = archived;
|
|
1394
|
-
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath));
|
|
1715
|
+
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath), { notifications: "disabled" });
|
|
1395
1716
|
}
|
|
1396
1717
|
const match = isPiSessionRef(ref)
|
|
1397
1718
|
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
1398
1719
|
: (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
1399
1720
|
if (!match)
|
|
1400
1721
|
throw new Error("Session not found");
|
|
1401
|
-
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path));
|
|
1722
|
+
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
1402
1723
|
}
|
|
1403
|
-
openExistingSession(sessionId, cwd, openSessionManager) {
|
|
1724
|
+
openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
|
|
1404
1725
|
const active = this.activeForLookup({ id: sessionId, cwd });
|
|
1405
1726
|
if (active !== undefined)
|
|
1406
1727
|
return Promise.resolve(active);
|
|
@@ -1410,7 +1731,7 @@ export class PiSessionService {
|
|
|
1410
1731
|
return existing.promise;
|
|
1411
1732
|
const pending = {
|
|
1412
1733
|
sessionId,
|
|
1413
|
-
promise: this.create(openSessionManager(), cwd),
|
|
1734
|
+
promise: this.create(openSessionManager(), cwd, options),
|
|
1414
1735
|
};
|
|
1415
1736
|
pending.promise = pending.promise.finally(() => {
|
|
1416
1737
|
if (this.pendingSessionOpens.get(key) === pending)
|
|
@@ -1432,6 +1753,9 @@ export class PiSessionService {
|
|
|
1432
1753
|
return undefined;
|
|
1433
1754
|
return archived;
|
|
1434
1755
|
}
|
|
1756
|
+
isCurrentActiveSession(session) {
|
|
1757
|
+
return this.active.get(session.sessionId)?.runtime.session === session;
|
|
1758
|
+
}
|
|
1435
1759
|
activeForLookup(ref) {
|
|
1436
1760
|
const sessionId = sessionIdFromLookup(ref);
|
|
1437
1761
|
const exact = this.active.get(sessionId);
|
|
@@ -1454,21 +1778,83 @@ export class PiSessionService {
|
|
|
1454
1778
|
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
1455
1779
|
});
|
|
1456
1780
|
const active = { runtime, unsubscribe: noop };
|
|
1781
|
+
let boundSession = runtime.session;
|
|
1782
|
+
let notificationGeneration = options.notificationGeneration;
|
|
1783
|
+
let notificationOwnership = options.notifications === "disabled"
|
|
1784
|
+
? "disabled"
|
|
1785
|
+
: notificationGeneration === undefined
|
|
1786
|
+
? "registered"
|
|
1787
|
+
: "external";
|
|
1788
|
+
if (notificationOwnership === "registered") {
|
|
1789
|
+
const notificationIdentity = notificationIdentityForSession(runtime.session);
|
|
1790
|
+
const existingCandidate = this.notificationStore.beginReplacementForSession(notificationIdentity.sessionId, notificationIdentity.cwd);
|
|
1791
|
+
if (existingCandidate !== undefined) {
|
|
1792
|
+
notificationGeneration = existingCandidate;
|
|
1793
|
+
notificationOwnership = "replacement";
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
const registration = this.notificationStore.registerSession(notificationIdentity.sessionId, notificationIdentity.cwd);
|
|
1797
|
+
notificationGeneration = registration.generation;
|
|
1798
|
+
this.publishNotificationMutations(registration.mutations);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (notificationGeneration !== undefined)
|
|
1802
|
+
this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
|
|
1457
1803
|
try {
|
|
1458
|
-
|
|
1804
|
+
if (options.creationProvenance === "tracked-subsession") {
|
|
1805
|
+
await this.publishUnreadMutations(this.unreadStore.excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
|
|
1806
|
+
}
|
|
1807
|
+
else {
|
|
1808
|
+
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
1809
|
+
}
|
|
1810
|
+
await this.bindSessionExtensions(runtime.session, notificationGeneration);
|
|
1459
1811
|
this.bindRuntime(active);
|
|
1460
1812
|
runtime.setRebindSession(async (session) => {
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1813
|
+
const priorGeneration = notificationGeneration;
|
|
1814
|
+
let candidateGeneration;
|
|
1815
|
+
try {
|
|
1816
|
+
await this.prepareUnreadRuntimeRebind(boundSession, session);
|
|
1817
|
+
await this.recoverSubsessionTrackingForOpenedSession(session);
|
|
1818
|
+
if (priorGeneration !== undefined) {
|
|
1819
|
+
candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
1820
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1821
|
+
}
|
|
1822
|
+
this.bindRuntime(active, session);
|
|
1823
|
+
boundSession = session;
|
|
1824
|
+
await this.bindSessionExtensions(session, candidateGeneration);
|
|
1825
|
+
if (candidateGeneration !== undefined) {
|
|
1826
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1827
|
+
notificationGeneration = candidateGeneration;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
catch (error) {
|
|
1831
|
+
if (candidateGeneration !== undefined) {
|
|
1832
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
|
|
1833
|
+
notificationGeneration = candidateGeneration;
|
|
1834
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1835
|
+
}
|
|
1836
|
+
throw error;
|
|
1837
|
+
}
|
|
1464
1838
|
});
|
|
1465
1839
|
this.active.set(runtime.session.sessionId, active);
|
|
1466
|
-
|
|
1840
|
+
if (notificationOwnership === "replacement" && notificationGeneration !== undefined) {
|
|
1841
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
|
|
1842
|
+
notificationOwnership = "external";
|
|
1843
|
+
}
|
|
1467
1844
|
this.publishStatus(runtime.session);
|
|
1468
1845
|
return active;
|
|
1469
1846
|
}
|
|
1470
1847
|
catch (error) {
|
|
1848
|
+
if (notificationGeneration !== undefined) {
|
|
1849
|
+
if (notificationOwnership === "registered") {
|
|
1850
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(runtime.session.sessionId, "initialization-failed"));
|
|
1851
|
+
}
|
|
1852
|
+
else if (notificationOwnership === "replacement") {
|
|
1853
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(notificationGeneration));
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1471
1856
|
active.unsubscribe();
|
|
1857
|
+
this.forgetUnreadActivity(boundSession);
|
|
1472
1858
|
let removedActive = false;
|
|
1473
1859
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
1474
1860
|
if (candidate !== active)
|
|
@@ -1491,39 +1877,169 @@ export class PiSessionService {
|
|
|
1491
1877
|
throw error;
|
|
1492
1878
|
}
|
|
1493
1879
|
}
|
|
1494
|
-
async bindSessionExtensions(session) {
|
|
1880
|
+
async bindSessionExtensions(session, generation) {
|
|
1881
|
+
const uiContext = this.sessionUiContext(session, generation);
|
|
1882
|
+
await session.bindExtensions({
|
|
1883
|
+
uiContext,
|
|
1884
|
+
mode: "rpc",
|
|
1885
|
+
onError: (error) => {
|
|
1886
|
+
const message = `${error.extensionPath}: ${error.error}`;
|
|
1887
|
+
this.publishActivity(session, "extension error", "error", message);
|
|
1888
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1889
|
+
},
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
replaceSessionNotificationContext(session, generation) {
|
|
1893
|
+
session.extensionRunner.setUIContext(this.sessionUiContext(session, generation), "rpc");
|
|
1894
|
+
}
|
|
1895
|
+
sessionUiContext(session, generation) {
|
|
1495
1896
|
const baseUiContext = session.extensionRunner.getUIContext();
|
|
1496
1897
|
const notify = (message, type) => {
|
|
1898
|
+
if (generation === undefined) {
|
|
1899
|
+
this.events.publish(session.sessionId, {
|
|
1900
|
+
type: "command.output",
|
|
1901
|
+
level: type === "error" ? "error" : "info",
|
|
1902
|
+
message,
|
|
1903
|
+
});
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const added = this.notificationStore.addNotification(generation, message, type);
|
|
1907
|
+
this.publishNotificationMutations(added.mutations);
|
|
1908
|
+
if (added.notification === undefined)
|
|
1909
|
+
return;
|
|
1497
1910
|
this.events.publish(session.sessionId, {
|
|
1498
1911
|
type: "command.output",
|
|
1499
1912
|
level: type === "error" ? "error" : "info",
|
|
1500
1913
|
message,
|
|
1914
|
+
notificationId: added.notification.id,
|
|
1501
1915
|
});
|
|
1502
1916
|
};
|
|
1503
|
-
// PI WEB
|
|
1504
|
-
//
|
|
1505
|
-
//
|
|
1506
|
-
|
|
1917
|
+
// PI WEB owns the browser-facing notification and text-formatting
|
|
1918
|
+
// boundaries. Delegate every other UI method to Pi's headless defaults so
|
|
1919
|
+
// unsupported dialogs cancel safely instead of hanging.
|
|
1920
|
+
return new Proxy(baseUiContext, {
|
|
1507
1921
|
get(target, property, receiver) {
|
|
1508
1922
|
if (property === "notify")
|
|
1509
1923
|
return notify;
|
|
1924
|
+
if (property === "theme")
|
|
1925
|
+
return plainTextTheme;
|
|
1510
1926
|
const value = Reflect.get(target, property, receiver);
|
|
1511
1927
|
return value;
|
|
1512
1928
|
},
|
|
1513
1929
|
});
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1930
|
+
}
|
|
1931
|
+
publishNotificationMutations(mutations) {
|
|
1932
|
+
for (const mutation of mutations) {
|
|
1933
|
+
this.events.publish(mutation.sessionId, mutation.inboxEvent);
|
|
1934
|
+
this.events.publishNotificationSummary(mutation.summaryEvent);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
async prepareUnreadRuntimeRebind(previous, next) {
|
|
1938
|
+
const previousCwd = canonicalizeStoredCwd(previous.sessionManager.getCwd());
|
|
1939
|
+
this.unreadStore.forgetActivity(previous.sessionId, previousCwd);
|
|
1940
|
+
const nextCwd = canonicalizeStoredCwd(next.sessionManager.getCwd());
|
|
1941
|
+
if (previous.sessionId === next.sessionId && cwdPathsEqual(previousCwd, nextCwd))
|
|
1942
|
+
return;
|
|
1943
|
+
await this.publishUnreadMutations(this.unreadStore.forgetSession(previous.sessionId, previousCwd));
|
|
1944
|
+
}
|
|
1945
|
+
forgetUnreadActivity(session) {
|
|
1946
|
+
this.unreadStore.forgetActivity(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()));
|
|
1947
|
+
}
|
|
1948
|
+
async forgetUnreadSessions(identities) {
|
|
1949
|
+
const mutations = [];
|
|
1950
|
+
for (const identity of identities) {
|
|
1951
|
+
mutations.push(...this.unreadStore.forgetSession(identity.sessionId, canonicalizeStoredCwd(identity.cwd)));
|
|
1952
|
+
}
|
|
1953
|
+
await this.publishUnreadMutations(mutations);
|
|
1954
|
+
}
|
|
1955
|
+
observeUnreadActivityState(session) {
|
|
1956
|
+
const mutations = this.unreadStore.observeActivityState(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()), this.hasActiveWork(session));
|
|
1957
|
+
if (mutations.length === 0)
|
|
1958
|
+
return;
|
|
1959
|
+
void this.publishUnreadMutations(mutations).catch(() => undefined);
|
|
1960
|
+
}
|
|
1961
|
+
publishUnreadMutations(mutations) {
|
|
1962
|
+
this.enqueueUnreadMutations(mutations);
|
|
1963
|
+
this.unreadPublicationFlushRequested = true;
|
|
1964
|
+
if (this.unreadPublication === undefined && this.unreadPublicationRetryTimer !== undefined) {
|
|
1965
|
+
const failure = this.unreadPublicationFailure;
|
|
1966
|
+
return Promise.reject(failure instanceof Error
|
|
1967
|
+
? failure
|
|
1968
|
+
: new Error("Session unread publication is awaiting retry", { cause: failure }));
|
|
1969
|
+
}
|
|
1970
|
+
return this.ensureUnreadPublication();
|
|
1971
|
+
}
|
|
1972
|
+
ensureUnreadPublication() {
|
|
1973
|
+
const existing = this.unreadPublication;
|
|
1974
|
+
if (existing !== undefined)
|
|
1975
|
+
return existing;
|
|
1976
|
+
const publication = this.drainUnreadPublication();
|
|
1977
|
+
this.unreadPublication = publication;
|
|
1978
|
+
void publication.then(() => {
|
|
1979
|
+
if (this.unreadPublication === publication)
|
|
1980
|
+
this.unreadPublication = undefined;
|
|
1981
|
+
}, (error) => {
|
|
1982
|
+
if (this.unreadPublication === publication)
|
|
1983
|
+
this.unreadPublication = undefined;
|
|
1984
|
+
this.unreadPublicationFailure = error;
|
|
1985
|
+
this.logger.info({ error: error instanceof Error ? error.message : String(error) }, "failed to publish durable session unread mutations");
|
|
1986
|
+
this.scheduleUnreadPublicationRetry();
|
|
1522
1987
|
});
|
|
1988
|
+
return publication;
|
|
1989
|
+
}
|
|
1990
|
+
async drainUnreadPublication() {
|
|
1991
|
+
while (this.unreadPublicationFlushRequested || this.pendingUnreadMutations.length > 0) {
|
|
1992
|
+
this.unreadPublicationFlushRequested = false;
|
|
1993
|
+
const batch = this.pendingUnreadMutations.splice(0);
|
|
1994
|
+
let publishedCount = 0;
|
|
1995
|
+
try {
|
|
1996
|
+
await this.unreadStore.flush();
|
|
1997
|
+
for (const mutation of batch) {
|
|
1998
|
+
this.events.publishGlobal(mutation.event);
|
|
1999
|
+
publishedCount += 1;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
catch (error) {
|
|
2003
|
+
this.prependUnreadMutations(batch.slice(publishedCount));
|
|
2004
|
+
this.unreadPublicationFlushRequested = true;
|
|
2005
|
+
throw error;
|
|
2006
|
+
}
|
|
2007
|
+
this.unreadPublicationFailure = undefined;
|
|
2008
|
+
this.clearUnreadPublicationRetry();
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
enqueueUnreadMutations(mutations) {
|
|
2012
|
+
this.pendingUnreadMutations.push(...mutations);
|
|
2013
|
+
this.trimPendingUnreadMutations();
|
|
2014
|
+
}
|
|
2015
|
+
prependUnreadMutations(mutations) {
|
|
2016
|
+
this.pendingUnreadMutations.unshift(...mutations);
|
|
2017
|
+
this.trimPendingUnreadMutations();
|
|
1523
2018
|
}
|
|
1524
|
-
|
|
2019
|
+
trimPendingUnreadMutations() {
|
|
2020
|
+
const excess = this.pendingUnreadMutations.length - MAX_PENDING_UNREAD_MUTATIONS;
|
|
2021
|
+
if (excess > 0)
|
|
2022
|
+
this.pendingUnreadMutations.splice(0, excess);
|
|
2023
|
+
}
|
|
2024
|
+
scheduleUnreadPublicationRetry() {
|
|
2025
|
+
if (this.unreadPublicationStopped || this.unreadPublicationRetryTimer !== undefined)
|
|
2026
|
+
return;
|
|
2027
|
+
const delay = this.unreadPublicationRetryDelayMs;
|
|
2028
|
+
this.unreadPublicationRetryDelayMs = Math.min(Math.max(delay * 2, this.unreadPublicationRetryInitialMs), Math.max(MAX_UNREAD_PUBLICATION_RETRY_MS, this.unreadPublicationRetryInitialMs));
|
|
2029
|
+
this.unreadPublicationRetryTimer = setTimeout(() => {
|
|
2030
|
+
this.unreadPublicationRetryTimer = undefined;
|
|
2031
|
+
void this.ensureUnreadPublication().catch(() => undefined);
|
|
2032
|
+
}, delay);
|
|
2033
|
+
this.unreadPublicationRetryTimer.unref();
|
|
2034
|
+
}
|
|
2035
|
+
clearUnreadPublicationRetry() {
|
|
2036
|
+
if (this.unreadPublicationRetryTimer !== undefined)
|
|
2037
|
+
clearTimeout(this.unreadPublicationRetryTimer);
|
|
2038
|
+
this.unreadPublicationRetryTimer = undefined;
|
|
2039
|
+
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
2040
|
+
}
|
|
2041
|
+
bindRuntime(active, session = active.runtime.session) {
|
|
1525
2042
|
active.unsubscribe();
|
|
1526
|
-
const { session } = active.runtime;
|
|
1527
2043
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
1528
2044
|
if (candidate === active) {
|
|
1529
2045
|
this.active.delete(sessionId);
|
|
@@ -1614,7 +2130,7 @@ export class PiSessionService {
|
|
|
1614
2130
|
const model = session.model;
|
|
1615
2131
|
if (model === undefined)
|
|
1616
2132
|
return;
|
|
1617
|
-
void generateShortSessionName(session.agent.
|
|
2133
|
+
void generateShortSessionName(session.agent.streamFunction, model, firstMessage).then((name) => {
|
|
1618
2134
|
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
1619
2135
|
}).catch(() => {
|
|
1620
2136
|
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
|
|
@@ -1623,9 +2139,32 @@ export class PiSessionService {
|
|
|
1623
2139
|
applyGeneratedSessionName(session, name) {
|
|
1624
2140
|
if (name === undefined || session.sessionName !== undefined)
|
|
1625
2141
|
return;
|
|
2142
|
+
if (this.treeNavigations.has(session)) {
|
|
2143
|
+
this.deferredGeneratedSessionNames.set(session, name);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
1626
2146
|
session.setSessionName(name);
|
|
1627
2147
|
this.publishSessionName(session);
|
|
1628
2148
|
}
|
|
2149
|
+
flushDeferredTreeNavigationWork(session) {
|
|
2150
|
+
const generatedName = this.deferredGeneratedSessionNames.get(session);
|
|
2151
|
+
this.deferredGeneratedSessionNames.delete(session);
|
|
2152
|
+
if (generatedName !== undefined) {
|
|
2153
|
+
try {
|
|
2154
|
+
this.applyGeneratedSessionName(session, generatedName);
|
|
2155
|
+
}
|
|
2156
|
+
catch (error) {
|
|
2157
|
+
this.logger.info({ sessionId: session.sessionId, error: error instanceof Error ? error.message : String(error) }, "failed to apply deferred session name");
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const notifications = this.deferredSubsessionNotifications.get(session) ?? [];
|
|
2161
|
+
this.deferredSubsessionNotifications.delete(session);
|
|
2162
|
+
for (const notification of notifications) {
|
|
2163
|
+
void this.deliverSubsessionNotification(session, notification).catch((error) => {
|
|
2164
|
+
this.logSubsessionNotificationFailure(notification.parentId, notification.childId, error);
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
1629
2168
|
applyAuthChange(change = {}) {
|
|
1630
2169
|
// ModelRuntime.login()/logout() refresh the shared runtime before AuthService
|
|
1631
2170
|
// emits the change, so no refresh is needed here. Keeping this synchronous
|
|
@@ -1694,6 +2233,10 @@ export class PiSessionService {
|
|
|
1694
2233
|
}
|
|
1695
2234
|
}
|
|
1696
2235
|
activityLabelFromStatus(session) {
|
|
2236
|
+
if (this.treeNavigations.has(session))
|
|
2237
|
+
return "navigating session tree";
|
|
2238
|
+
if (this.isSessionEntryMutationActive(session))
|
|
2239
|
+
return "updating session";
|
|
1697
2240
|
if (session.isCompacting)
|
|
1698
2241
|
return "compacting";
|
|
1699
2242
|
if (session.isBashRunning)
|
|
@@ -1705,7 +2248,92 @@ export class PiSessionService {
|
|
|
1705
2248
|
return "active";
|
|
1706
2249
|
}
|
|
1707
2250
|
hasActiveWork(session) {
|
|
1708
|
-
return
|
|
2251
|
+
return this.treeNavigations.has(session)
|
|
2252
|
+
|| this.isSessionEntryMutationActive(session)
|
|
2253
|
+
|| this.isTreeExclusiveOperationActive(session)
|
|
2254
|
+
|| sessionHasActiveWork(session, this.compactionQueuedMessages(session.sessionId).length);
|
|
2255
|
+
}
|
|
2256
|
+
async runTreeExclusiveOperation(targets, activeError, operation) {
|
|
2257
|
+
const sessionIds = new Set();
|
|
2258
|
+
const runtimes = new Set();
|
|
2259
|
+
const sessions = new Set();
|
|
2260
|
+
for (const target of targets) {
|
|
2261
|
+
const runtime = target.runtime ?? (target.session === undefined ? undefined : this.activeRuntimeForSession(target.session));
|
|
2262
|
+
const session = target.session ?? runtime?.session;
|
|
2263
|
+
if (session !== undefined && this.hasActiveWork(session))
|
|
2264
|
+
throw new Error(activeError);
|
|
2265
|
+
sessionIds.add(target.sessionId);
|
|
2266
|
+
if (runtime !== undefined)
|
|
2267
|
+
runtimes.add(runtime);
|
|
2268
|
+
if (session !== undefined)
|
|
2269
|
+
sessions.add(session);
|
|
2270
|
+
}
|
|
2271
|
+
for (const sessionId of sessionIds) {
|
|
2272
|
+
this.treeExclusiveSessionOperationCounts.set(sessionId, (this.treeExclusiveSessionOperationCounts.get(sessionId) ?? 0) + 1);
|
|
2273
|
+
}
|
|
2274
|
+
for (const runtime of runtimes) {
|
|
2275
|
+
this.treeExclusiveRuntimeOperationCounts.set(runtime, (this.treeExclusiveRuntimeOperationCounts.get(runtime) ?? 0) + 1);
|
|
2276
|
+
}
|
|
2277
|
+
for (const session of sessions)
|
|
2278
|
+
this.observeUnreadActivityState(session);
|
|
2279
|
+
try {
|
|
2280
|
+
return await operation();
|
|
2281
|
+
}
|
|
2282
|
+
finally {
|
|
2283
|
+
for (const runtime of runtimes)
|
|
2284
|
+
decrementWeakCount(this.treeExclusiveRuntimeOperationCounts, runtime);
|
|
2285
|
+
for (const sessionId of sessionIds)
|
|
2286
|
+
decrementMapCount(this.treeExclusiveSessionOperationCounts, sessionId);
|
|
2287
|
+
for (const session of sessions) {
|
|
2288
|
+
if (this.isCurrentActiveSession(session))
|
|
2289
|
+
this.observeUnreadActivityState(session);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
isTreeExclusiveSessionIdentityActive(sessionId) {
|
|
2294
|
+
return (this.treeExclusiveSessionOperationCounts.get(sessionId) ?? 0) > 0;
|
|
2295
|
+
}
|
|
2296
|
+
isTreeExclusiveOperationActive(session) {
|
|
2297
|
+
if (this.isTreeExclusiveSessionIdentityActive(session.sessionId))
|
|
2298
|
+
return true;
|
|
2299
|
+
const runtime = this.activeRuntimeForSession(session);
|
|
2300
|
+
return runtime !== undefined && (this.treeExclusiveRuntimeOperationCounts.get(runtime) ?? 0) > 0;
|
|
2301
|
+
}
|
|
2302
|
+
activeRuntimeForSession(session) {
|
|
2303
|
+
for (const active of new Set(this.active.values())) {
|
|
2304
|
+
if (active.runtime.session === session)
|
|
2305
|
+
return active.runtime;
|
|
2306
|
+
}
|
|
2307
|
+
return undefined;
|
|
2308
|
+
}
|
|
2309
|
+
assertTreeNavigationInactive(session, action) {
|
|
2310
|
+
if (this.treeNavigations.has(session))
|
|
2311
|
+
throw new Error(`Cannot ${action} while session tree navigation is active`);
|
|
2312
|
+
}
|
|
2313
|
+
async runSessionEntryMutation(session, action, operation) {
|
|
2314
|
+
this.beginSessionEntryMutation(session, action);
|
|
2315
|
+
try {
|
|
2316
|
+
return await operation();
|
|
2317
|
+
}
|
|
2318
|
+
finally {
|
|
2319
|
+
this.endSessionEntryMutation(session);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
beginSessionEntryMutation(session, action) {
|
|
2323
|
+
this.assertTreeNavigationInactive(session, action);
|
|
2324
|
+
this.sessionEntryMutationCounts.set(session, (this.sessionEntryMutationCounts.get(session) ?? 0) + 1);
|
|
2325
|
+
this.observeUnreadActivityState(session);
|
|
2326
|
+
}
|
|
2327
|
+
endSessionEntryMutation(session) {
|
|
2328
|
+
const remaining = (this.sessionEntryMutationCounts.get(session) ?? 1) - 1;
|
|
2329
|
+
if (remaining <= 0)
|
|
2330
|
+
this.sessionEntryMutationCounts.delete(session);
|
|
2331
|
+
else
|
|
2332
|
+
this.sessionEntryMutationCounts.set(session, remaining);
|
|
2333
|
+
this.observeUnreadActivityState(session);
|
|
2334
|
+
}
|
|
2335
|
+
isSessionEntryMutationActive(session) {
|
|
2336
|
+
return (this.sessionEntryMutationCounts.get(session) ?? 0) > 0;
|
|
1709
2337
|
}
|
|
1710
2338
|
publishActivityForEvent(session, event) {
|
|
1711
2339
|
const eventType = getString(event, "type");
|
|
@@ -1767,6 +2395,7 @@ export class PiSessionService {
|
|
|
1767
2395
|
this.workspaceActivity?.applySessionActivity(session.sessionManager.getCwd(), activity);
|
|
1768
2396
|
this.events.publish(session.sessionId, { type: "activity.update", activity });
|
|
1769
2397
|
this.events.publishGlobal({ type: "activity.update", activity });
|
|
2398
|
+
this.observeUnreadActivityState(session);
|
|
1770
2399
|
}
|
|
1771
2400
|
publishStatus(session) {
|
|
1772
2401
|
const status = this.statusFromSession(session);
|
|
@@ -1774,6 +2403,7 @@ export class PiSessionService {
|
|
|
1774
2403
|
this.workspaceActivity?.applySessionStatus(session.sessionManager.getCwd(), status);
|
|
1775
2404
|
this.events.publish(session.sessionId, { type: "status.update", status });
|
|
1776
2405
|
this.events.publishGlobal({ type: "status.update", status });
|
|
2406
|
+
this.observeUnreadActivityState(session);
|
|
1777
2407
|
}
|
|
1778
2408
|
clearStaleActiveActivity(session) {
|
|
1779
2409
|
const current = this.activities.get(session.sessionId);
|
|
@@ -1886,6 +2516,12 @@ function modelToClientModel(model) {
|
|
|
1886
2516
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
1887
2517
|
};
|
|
1888
2518
|
}
|
|
2519
|
+
function notificationIdentityForSession(session) {
|
|
2520
|
+
return {
|
|
2521
|
+
sessionId: session.sessionId,
|
|
2522
|
+
cwd: canonicalizeStoredCwd(session.sessionManager.getCwd()),
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
1889
2525
|
function clientSessionFromListEntry(session) {
|
|
1890
2526
|
return {
|
|
1891
2527
|
id: session.id,
|