@jmfederico/pi-web 1.202606.7 → 1.202607.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +9 -3
  2. package/dist/client/assets/CodeViewer-tXF3UTsY.js +4 -0
  3. package/dist/client/assets/TerminalPanel-D_ihiWAt.js +187 -0
  4. package/dist/client/assets/UnifiedDiffViewer-BziirXBJ.js +39 -0
  5. package/dist/client/assets/index-Brfk1lwl.js +2950 -0
  6. package/dist/client/assets/vendor-editor-core-vUi74DnD.js +12 -0
  7. package/dist/client/assets/vendor-editor-languages-DoaXUodB.js +46 -0
  8. package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +1 -0
  9. package/dist/client/assets/vendor-terminal-BrP-ENHg.css +1 -0
  10. package/dist/client/assets/vendor-terminal-D8k4UKM2.js +35 -0
  11. package/dist/client/index.html +6 -3
  12. package/dist/docker/piWebDockerCommandPlan.js +211 -0
  13. package/dist/docker/piWebDockerCommandPlan.js.map +1 -0
  14. package/dist/pi-web-plugins/updates/pi-web-plugin.js +15 -5
  15. package/dist/pi-web-plugins/updates/updatesLogic.js +37 -5
  16. package/dist/piWebVersionReport.js +4 -0
  17. package/dist/piWebVersionReport.js.map +1 -1
  18. package/dist/plugin-api.d.ts +1 -1
  19. package/dist/server/app.js +16 -1
  20. package/dist/server/app.js.map +1 -1
  21. package/dist/server/browserMessageProjection.js +56 -0
  22. package/dist/server/browserMessageProjection.js.map +1 -0
  23. package/dist/server/configRoutes.js +108 -1
  24. package/dist/server/configRoutes.js.map +1 -1
  25. package/dist/server/machines/machineClient.js +19 -9
  26. package/dist/server/machines/machineClient.js.map +1 -1
  27. package/dist/server/machines/machineProxyRoutes.js +64 -11
  28. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  29. package/dist/server/machines/machineService.js +4 -26
  30. package/dist/server/machines/machineService.js.map +1 -1
  31. package/dist/server/piPackageRoutes.js +87 -0
  32. package/dist/server/piPackageRoutes.js.map +1 -0
  33. package/dist/server/piPackageService.js +69 -0
  34. package/dist/server/piPackageService.js.map +1 -0
  35. package/dist/server/piWebPluginService.js +43 -8
  36. package/dist/server/piWebPluginService.js.map +1 -1
  37. package/dist/server/piWebStatus.js +57 -0
  38. package/dist/server/piWebStatus.js.map +1 -1
  39. package/dist/server/realtime/sessionEventHub.js +2 -1
  40. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  41. package/dist/server/sessions/builtinCommands.js +1 -1
  42. package/dist/server/sessions/builtinCommands.js.map +1 -1
  43. package/dist/server/sessions/piSessionService.js +503 -104
  44. package/dist/server/sessions/piSessionService.js.map +1 -1
  45. package/dist/server/sessions/sessionArchiveStore.js +51 -22
  46. package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
  47. package/dist/server/sessions/sessionCommandService.js +16 -0
  48. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  49. package/dist/server/sessions/sessionNameGenerator.js +27 -44
  50. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  51. package/dist/server/sessions/sessionRoutes.js +38 -1
  52. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  53. package/dist/server/sessions/spawnSessionTool.js +9 -4
  54. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  55. package/dist/server/sessions/spawnSubsessionTool.js +22 -15
  56. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  57. package/dist/server/workspaces/fileSuggestions.js +7 -3
  58. package/dist/server/workspaces/fileSuggestions.js.map +1 -1
  59. package/dist/shared/activity.js +0 -15
  60. package/dist/shared/activity.js.map +1 -1
  61. package/dist/shared/apiTypes.d.ts +63 -1
  62. package/dist/shared/apiTypes.js +4 -0
  63. package/dist/shared/apiTypes.js.map +1 -1
  64. package/dist/shared/capabilities.js +28 -2
  65. package/dist/shared/capabilities.js.map +1 -1
  66. package/dist/shared/federatedRoutes.js +10 -0
  67. package/dist/shared/federatedRoutes.js.map +1 -1
  68. package/dist/shared/piWebStatusParsing.js +20 -8
  69. package/dist/shared/piWebStatusParsing.js.map +1 -1
  70. package/dist/shared/thinkingLevels.d.ts +1 -1
  71. package/dist/shared/thinkingLevels.js +1 -1
  72. package/dist/shared/thinkingLevels.js.map +1 -1
  73. package/docs/config.md +24 -13
  74. package/docs/plugins.md +28 -14
  75. package/package.json +35 -31
  76. package/dist/client/assets/CodeViewer-CQU9mJ74.js +0 -4
  77. package/dist/client/assets/TerminalPanel-DtfVBsqI.js +0 -126
  78. package/dist/client/assets/UnifiedDiffViewer-BZQQK99S.js +0 -34
  79. package/dist/client/assets/index-CbE7df66.js +0 -2742
  80. package/dist/client/assets/vendor-editor-core-C7aWiQy0.js +0 -12
  81. package/dist/client/assets/vendor-editor-languages-EW34FfVy.js +0 -26
  82. package/dist/client/assets/vendor-editor-legacy-B4QLsWF8.js +0 -1
  83. package/dist/client/assets/vendor-terminal-DDGTF8rc.css +0 -1
  84. package/dist/client/assets/vendor-terminal-DjQ08hXu.js +0 -16
@@ -1,3 +1,4 @@
1
+ import { statSync } from "node:fs";
1
2
  import { open, readFile, writeFile } from "node:fs/promises";
2
3
  import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
3
4
  import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
@@ -5,12 +6,12 @@ import { BUILTIN_COMMANDS } from "./builtinCommands.js";
5
6
  import { SessionCommandService } from "./sessionCommandService.js";
6
7
  import { SessionArchiveStore } from "./sessionArchiveStore.js";
7
8
  import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
8
- import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
9
+ import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
9
10
  import { computeEditPreview } from "./editPreview.js";
10
11
  import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
11
12
  import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
12
13
  import { parsePromptAttachments } from "../../shared/promptAttachments.js";
13
- import { cwdPathsEqual } from "../workingDirectory.js";
14
+ import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
14
15
  import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
15
16
  import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
16
17
  import { buildTranscriptView } from "./subsessionTranscript.js";
@@ -51,20 +52,50 @@ function parsePromptStreamingBehavior(value) {
51
52
  function defaultCreateAgentRuntime(createRuntime, options) {
52
53
  if (!(options.sessionManager instanceof SessionManager))
53
54
  throw new Error("Default runtime creation requires an SDK SessionManager");
54
- return createAgentSessionRuntime(createRuntime, { ...options, sessionManager: options.sessionManager });
55
+ const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.delegationToolsEnabled);
56
+ return createAgentSessionRuntime(runtimeFactory, {
57
+ cwd: options.cwd,
58
+ agentDir: options.agentDir,
59
+ sessionManager: options.sessionManager,
60
+ });
61
+ }
62
+ function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, delegationToolsEnabled) {
63
+ // These inputs belong only to the session being opened. A later runtime
64
+ // replacement resolves its own model and delegation capability.
65
+ let pendingInitialModel = initialModel;
66
+ let pendingDelegationToolsEnabled = delegationToolsEnabled;
67
+ return async (options) => {
68
+ const model = pendingInitialModel;
69
+ const toolsEnabled = pendingDelegationToolsEnabled;
70
+ pendingInitialModel = undefined;
71
+ pendingDelegationToolsEnabled = undefined;
72
+ return createRuntime({
73
+ ...options,
74
+ ...(model === undefined ? {} : { initialModel: model }),
75
+ ...(toolsEnabled === undefined ? {} : { delegationToolsEnabled: toolsEnabled }),
76
+ });
77
+ };
78
+ }
79
+ export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions) {
80
+ return [
81
+ createPiWebEditToolDefinition(cwd),
82
+ ...(delegationEnabled && spawn !== undefined ? [createSpawnSessionToolDefinition(cwd, { spawn })] : []),
83
+ ...(delegationEnabled && subsessions !== undefined ? createSubsessionToolDefinitions(cwd, subsessions) : []),
84
+ ];
55
85
  }
56
- function createDefaultRuntimeFactory(authStorage, modelRegistry, spawn, subsessions) {
57
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
86
+ function createDefaultRuntimeFactory(authStorage, modelRegistry, sessionManagers, spawn, subsessions) {
87
+ return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => {
58
88
  const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
59
- const customTools = [
60
- createPiWebEditToolDefinition(cwd),
61
- ...(spawn === undefined ? [] : [createSpawnSessionToolDefinition(cwd, { spawn })]),
62
- ...(subsessions === undefined ? [] : createSubsessionToolDefinitions(cwd, subsessions)),
63
- ];
64
- const options = sessionStartEvent === undefined
65
- ? { services, sessionManager, customTools }
66
- : { services, sessionManager, sessionStartEvent, customTools };
67
- const result = await createAgentSessionFromServices(options);
89
+ const resolvedDelegationToolsEnabled = delegationToolsEnabled
90
+ ?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
91
+ const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions);
92
+ const result = await createAgentSessionFromServices({
93
+ services,
94
+ sessionManager,
95
+ customTools,
96
+ ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
97
+ ...(initialModel === undefined ? {} : { model: initialModel }),
98
+ });
68
99
  return { ...result, services, diagnostics: services.diagnostics };
69
100
  };
70
101
  }
@@ -93,6 +124,7 @@ export class PiSessionService {
93
124
  constructor(events, deps = {}) {
94
125
  this.events = events;
95
126
  this.active = new Map();
127
+ this.pendingSessionOpens = new Map();
96
128
  this.activities = new Map();
97
129
  this.compactionPromptQueues = new Map();
98
130
  this.compactionDrainTimers = new Map();
@@ -121,7 +153,7 @@ export class PiSessionService {
121
153
  // Subsessions are a beta capability gated behind their own flag, and they
122
154
  // also require the spawn capability (they share its project-scope resolver).
123
155
  const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
124
- this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
156
+ this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
125
157
  spawn: (input) => this.spawnSubsession(input),
126
158
  list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
127
159
  check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
@@ -139,6 +171,7 @@ export class PiSessionService {
139
171
  this.publishActivity(session, result === "success" ? "compaction complete" : "compaction failed", result === "success" ? "idle" : "error", detail);
140
172
  this.publishStatus(session);
141
173
  },
174
+ reloadSession: (session) => this.reloadSessionRuntime(session),
142
175
  }, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
143
176
  }
144
177
  activeCount() {
@@ -149,10 +182,12 @@ export class PiSessionService {
149
182
  }
150
183
  async cleanup(request) {
151
184
  const plan = await this.cleanupPlan(request);
152
- if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined)
185
+ if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
153
186
  throw new Error("Archive store does not support deletion");
154
187
  const archiveInputs = [];
188
+ const readyArchiveInputs = [];
155
189
  const deleteRecords = [];
190
+ const readyDeleteRecords = [];
156
191
  const skippedBusySessionIds = new Set(plan.skippedBusySessionIds);
157
192
  for (const input of plan.archiveInputs) {
158
193
  if (this.activeSessionHasWork(input.sessionId)) {
@@ -160,20 +195,21 @@ export class PiSessionService {
160
195
  continue;
161
196
  }
162
197
  await this.closeActive(input.sessionId);
163
- await this.archiveStore.archive(input);
164
- archiveInputs.push(input);
198
+ readyArchiveInputs.push(input);
165
199
  }
200
+ await this.archiveStoreArchiveMany(readyArchiveInputs);
201
+ archiveInputs.push(...readyArchiveInputs);
166
202
  for (const record of plan.deleteRecords) {
167
203
  if (this.activeSessionHasWork(record.sessionId)) {
168
204
  skippedBusySessionIds.add(record.sessionId);
169
205
  continue;
170
206
  }
171
207
  await this.closeActive(record.sessionId);
172
- if (record.archivePath === undefined)
173
- await this.ensureArchivedRecordMoved(record);
174
- await this.archiveStore.deleteArchived?.(record.sessionId);
175
- deleteRecords.push(record);
208
+ readyDeleteRecords.push(record);
176
209
  }
210
+ await this.ensureArchivedRecordsMoved(readyDeleteRecords);
211
+ const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
212
+ deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
177
213
  return summarizeSessionCleanupExecution({
178
214
  archiveInputs,
179
215
  deleteRecords,
@@ -185,8 +221,12 @@ export class PiSessionService {
185
221
  async dispose() {
186
222
  clearInterval(this.heartbeat);
187
223
  this.clearCompactionDrainTimers();
224
+ const pendingOpens = this.pendingSessionOpenPromises();
225
+ if (pendingOpens.length > 0)
226
+ await Promise.allSettled(pendingOpens);
188
227
  const activeSessions = Array.from(new Set(this.active.values()));
189
228
  this.active.clear();
229
+ this.pendingSessionOpens.clear();
190
230
  this.activities.clear();
191
231
  this.compactionPromptQueues.clear();
192
232
  this.authLossWarnings.clear();
@@ -198,8 +238,12 @@ export class PiSessionService {
198
238
  await Promise.all(activeSessions.map(async (active) => {
199
239
  active.unsubscribe();
200
240
  this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
201
- await active.runtime.session.abort();
202
- await active.runtime.dispose();
241
+ try {
242
+ await active.runtime.session.abort();
243
+ }
244
+ finally {
245
+ await active.runtime.dispose();
246
+ }
203
247
  }));
204
248
  }
205
249
  async list(cwd) {
@@ -217,20 +261,27 @@ export class PiSessionService {
217
261
  .filter(isDefined);
218
262
  return [...unarchivedSessions, ...archivedSessions];
219
263
  }
220
- async start(cwd, parentSession) {
221
- const active = await this.create(this.sessionManager.create(cwd, parentSession === undefined ? undefined : { parentSession }), cwd);
264
+ async start(cwd, options = {}) {
265
+ return this.startSession(cwd, options);
266
+ }
267
+ async startSession(cwd, options) {
268
+ const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
269
+ ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
270
+ ...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
271
+ });
222
272
  const { session } = active.runtime;
223
273
  const created = {
224
274
  id: session.sessionId,
225
275
  path: session.sessionFile ?? "",
226
276
  cwd,
277
+ persisted: sessionFileExists(session.sessionFile),
227
278
  created: new Date().toISOString(),
228
279
  modified: new Date().toISOString(),
229
280
  messageCount: session.messages.length,
230
281
  firstMessage: "",
231
282
  // Include the parent so listeners can nest the new session in the tree
232
283
  // immediately, instead of showing it flat until the next reload.
233
- ...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
284
+ ...(options.parentSession === undefined ? {} : { parentSessionPath: options.parentSession }),
234
285
  };
235
286
  // Broadcast so other clients (and the spawning agent's UI) can add the new
236
287
  // session to their list without a manual reload.
@@ -248,7 +299,7 @@ export class PiSessionService {
248
299
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
249
300
  if (!decision.allowed)
250
301
  throw spawnTargetError(decision);
251
- const created = await this.start(decision.cwd);
302
+ const created = await this.start(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
252
303
  await this.prompt(created.id, input.prompt);
253
304
  this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
254
305
  return { sessionId: created.id, cwd: decision.cwd };
@@ -265,7 +316,11 @@ export class PiSessionService {
265
316
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
266
317
  if (!decision.allowed)
267
318
  throw spawnTargetError(decision);
268
- const created = await this.start(decision.cwd, input.parentSessionFile);
319
+ const created = await this.startSession(decision.cwd, {
320
+ ...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
321
+ ...(input.model === undefined ? {} : { initialModel: input.model }),
322
+ creationProvenance: "tracked-subsession",
323
+ });
269
324
  const parentSessionFile = nonEmptyString(input.parentSessionFile);
270
325
  const link = {
271
326
  parentSessionId: input.parentSessionId,
@@ -465,61 +520,13 @@ export class PiSessionService {
465
520
  return;
466
521
  this.registerVerifiedSubsession(link);
467
522
  }
468
- async verifiedSubsessionLinkFromOpenedChild(session) {
469
- // Child markers are only hints; the current child header and reciprocal
470
- // parent custom link must agree on the exact ids and files before relinking.
471
- const entries = session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch();
472
- let marker;
473
- for (const entry of entries) {
474
- const parsed = parsePersistedChildSubsessionLink(entry);
475
- if (parsed?.spawnedSessionId === session.sessionId)
476
- marker = parsed;
477
- }
478
- if (marker === undefined)
479
- return undefined;
480
- const childSessionFile = nonEmptyString(session.sessionFile);
481
- if (childSessionFile === undefined)
482
- return undefined;
483
- const childHeader = await readSessionHeaderSummary(childSessionFile);
484
- if (childHeader?.id !== session.sessionId)
485
- return undefined;
486
- const parentSessionFile = nonEmptyString(childHeader.parentSession);
487
- if (parentSessionFile === undefined)
488
- return undefined;
489
- const parentHeader = await readSessionHeaderSummary(parentSessionFile);
490
- if (parentHeader?.id !== marker.spawnedBySessionId)
491
- return undefined;
492
- const parentLink = this.findReciprocalParentSubsessionLink(parentSessionFile, marker.spawnedBySessionId, session.sessionId, childSessionFile);
493
- if (parentLink === undefined)
494
- return undefined;
495
- return {
496
- parentSessionId: marker.spawnedBySessionId,
497
- childSessionId: session.sessionId,
498
- childSessionFile,
499
- parentSessionFile,
500
- cwd: parentLink.cwd ?? session.sessionManager.getCwd(),
501
- };
502
- }
503
- findReciprocalParentSubsessionLink(parentSessionFile, parentSessionId, childSessionId, childSessionFile) {
504
- let parentManager;
505
- try {
506
- parentManager = this.sessionManager.open(parentSessionFile);
507
- }
508
- catch {
509
- return undefined;
510
- }
511
- const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
512
- for (const entry of entries) {
513
- const link = parsePersistedParentSubsessionLink(entry);
514
- if (link === undefined)
515
- continue;
516
- if (link.spawnedBySessionId !== parentSessionId || link.spawnedSessionId !== childSessionId)
517
- continue;
518
- if (link.spawnedSessionFile === undefined || !sessionPathsEqual(link.spawnedSessionFile, childSessionFile))
519
- continue;
520
- return link;
521
- }
522
- return undefined;
523
+ verifiedSubsessionLinkFromOpenedChild(session) {
524
+ return verifiedTrackedSubsessionLink(this.sessionManager, {
525
+ sessionId: session.sessionId,
526
+ sessionFile: session.sessionFile,
527
+ sessionManager: session.sessionManager,
528
+ cwd: session.sessionManager.getCwd(),
529
+ });
523
530
  }
524
531
  async getOrOpenTrackedSubsession(sessionId) {
525
532
  const link = this.subsessionLinks.get(sessionId);
@@ -577,7 +584,7 @@ export class PiSessionService {
577
584
  const status = this.activities.get(childId)?.phase === "error" ? "error" : "idle";
578
585
  const finalText = finalAssistantText(historyMessages(session));
579
586
  const preview = finalText === "" ? "(no output)" : truncateForNotification(finalText);
580
- const text = `Subsession ${childId} stopped working (status: ${status}). Latest output:\n\n${preview}\n\nUse check_subsession with sessionId "${childId}" for its status and latest output, or read_subsession to look through its full transcript.`;
587
+ const text = `Subsession ${childId} stopped working (status: ${status}). Latest output:\n\n${preview}\n\nStatus and latest output are available through check_subsession with sessionId "${childId}"; its full transcript is available through read_subsession.`;
581
588
  void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
582
589
  }
583
590
  async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
@@ -795,6 +802,23 @@ export class PiSessionService {
795
802
  const active = await this.getActive(ref);
796
803
  return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
797
804
  }
805
+ async reloadSessionRuntime(session) {
806
+ if (this.hasActiveWork(session))
807
+ throw new Error("Stop current session activity before reloading");
808
+ this.publishActivity(session, "reloading resources", "active");
809
+ try {
810
+ await session.reload();
811
+ this.publishActivity(session, "resources reloaded", "idle");
812
+ this.publishStatus(session);
813
+ }
814
+ catch (error) {
815
+ const message = error instanceof Error ? error.message : String(error);
816
+ this.publishActivity(session, "reload failed", "error", message);
817
+ this.events.publish(session.sessionId, { type: "session.error", message });
818
+ this.publishStatus(session);
819
+ throw error;
820
+ }
821
+ }
798
822
  async archive(ref) {
799
823
  const session = await this.getOrOpen(ref);
800
824
  if (this.hasActiveWork(session))
@@ -803,6 +827,69 @@ export class PiSessionService {
803
827
  await this.closeActive(session.sessionId);
804
828
  await this.archiveStore.archive(archiveInput);
805
829
  }
830
+ async archiveMany(refs) {
831
+ const uniqueRefs = uniqueBulkSessionRefs(refs);
832
+ const [archivedRecords, sessionContext] = await Promise.all([
833
+ this.archiveStore.list(),
834
+ this.bulkSessionLookupContext(uniqueRefs),
835
+ ]);
836
+ const failures = [];
837
+ const alreadyArchivedSessionIds = [];
838
+ const planItems = [];
839
+ for (const ref of uniqueRefs) {
840
+ const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
841
+ if (archived !== undefined) {
842
+ alreadyArchivedSessionIds.push(archived.sessionId);
843
+ continue;
844
+ }
845
+ const active = this.activeForLookup(bulkRefToLookup(ref));
846
+ const listed = findListedSessionForBulkRef(sessionContext, ref);
847
+ const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
848
+ if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
849
+ failures.push({ sessionId: resolvedSessionId, error: "Stop current session activity before archiving" });
850
+ continue;
851
+ }
852
+ try {
853
+ if (listed !== undefined) {
854
+ planItems.push({ input: archiveInputFromListEntry(listed) });
855
+ }
856
+ else if (active !== undefined) {
857
+ planItems.push({ input: archiveInputFromActiveSession(active.runtime.session) });
858
+ }
859
+ else {
860
+ failures.push({ sessionId: ref.id, error: "Session not found" });
861
+ }
862
+ }
863
+ catch (error) {
864
+ failures.push({ sessionId: resolvedSessionId, error: errorMessage(error) });
865
+ }
866
+ }
867
+ const readyInputs = [];
868
+ for (const item of planItems) {
869
+ try {
870
+ await this.closeActive(item.input.sessionId);
871
+ readyInputs.push(item.input);
872
+ }
873
+ catch (error) {
874
+ failures.push({ sessionId: item.input.sessionId, error: errorMessage(error) });
875
+ }
876
+ }
877
+ const archivedSessionIds = [...alreadyArchivedSessionIds];
878
+ try {
879
+ const archived = await this.archiveStoreArchiveMany(readyInputs);
880
+ archivedSessionIds.push(...archived.map((record) => record.sessionId));
881
+ }
882
+ catch (error) {
883
+ for (const input of readyInputs)
884
+ failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
885
+ }
886
+ return {
887
+ archived: true,
888
+ archivedSessionIds: uniqueStrings(archivedSessionIds),
889
+ failures,
890
+ generatedAt: new Date().toISOString(),
891
+ };
892
+ }
806
893
  async archiveTree(ref) {
807
894
  const session = await this.getOrOpen(ref);
808
895
  const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
@@ -814,8 +901,7 @@ export class PiSessionService {
814
901
  const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
815
902
  for (const input of archiveInputs)
816
903
  await this.closeActive(input.sessionId);
817
- for (const input of archiveInputs)
818
- await this.archiveStore.archive(input);
904
+ await this.archiveStoreArchiveMany(archiveInputs);
819
905
  return {
820
906
  archived: true,
821
907
  sessionIds: archiveInputs.map((input) => input.sessionId),
@@ -841,6 +927,57 @@ export class PiSessionService {
841
927
  await this.ensureArchivedRecordMoved(record);
842
928
  await this.archiveStore.deleteArchived(record.sessionId);
843
929
  }
930
+ async deleteArchivedMany(refs) {
931
+ if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
932
+ throw new Error("Archive store does not support deletion");
933
+ const uniqueRefs = uniqueBulkSessionRefs(refs);
934
+ const archivedRecords = await this.archiveStore.list();
935
+ const failures = [];
936
+ const planItems = [];
937
+ for (const ref of uniqueRefs) {
938
+ const record = findArchivedRecordForBulkRef(archivedRecords, ref);
939
+ if (record === undefined) {
940
+ failures.push({ sessionId: ref.id, error: "Archived session not found" });
941
+ continue;
942
+ }
943
+ const active = this.activeForLookup({ id: record.sessionId, cwd: record.cwd });
944
+ if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
945
+ failures.push({ sessionId: record.sessionId, error: "Stop current session activity before deleting archived session" });
946
+ continue;
947
+ }
948
+ planItems.push({ record });
949
+ }
950
+ const readyRecords = [];
951
+ for (const item of planItems) {
952
+ try {
953
+ await this.closeActive(item.record.sessionId);
954
+ readyRecords.push(item.record);
955
+ }
956
+ catch (error) {
957
+ failures.push({ sessionId: item.record.sessionId, error: errorMessage(error) });
958
+ }
959
+ }
960
+ const moveFailures = await this.moveLegacyArchivedRecordsForDelete(readyRecords);
961
+ failures.push(...moveFailures);
962
+ const moveFailureIds = new Set(moveFailures.map((failure) => failure.sessionId));
963
+ const deleteIds = readyRecords
964
+ .map((record) => record.sessionId)
965
+ .filter((sessionId) => !moveFailureIds.has(sessionId));
966
+ let deletedSessionIds = [];
967
+ try {
968
+ deletedSessionIds = await this.archiveStoreDeleteArchivedMany(deleteIds);
969
+ }
970
+ catch (error) {
971
+ for (const sessionId of deleteIds)
972
+ failures.push({ sessionId, error: errorMessage(error) });
973
+ }
974
+ return {
975
+ deleted: true,
976
+ deletedSessionIds,
977
+ failures,
978
+ generatedAt: new Date().toISOString(),
979
+ };
980
+ }
844
981
  async reload(ref) {
845
982
  await this.assertWritable(ref);
846
983
  const session = await this.getOrOpen(ref);
@@ -878,6 +1015,75 @@ export class PiSessionService {
878
1015
  // Best-effort shutdown; callers that need errors await closeActive directly.
879
1016
  });
880
1017
  }
1018
+ async bulkSessionLookupContext(refs) {
1019
+ const cwdSet = new Set();
1020
+ let needsAllSessions = false;
1021
+ for (const ref of refs) {
1022
+ if (ref.cwd === undefined)
1023
+ needsAllSessions = true;
1024
+ else
1025
+ cwdSet.add(ref.cwd);
1026
+ }
1027
+ const [sessionsByCwd, allSessions] = await Promise.all([
1028
+ this.listSessionsByCwd([...cwdSet]),
1029
+ needsAllSessions ? this.sessionManager.listAll?.() ?? Promise.resolve([]) : Promise.resolve(undefined),
1030
+ ]);
1031
+ return allSessions === undefined ? { sessionsByCwd } : { sessionsByCwd, allSessions };
1032
+ }
1033
+ async listSessionsByCwd(cwds) {
1034
+ const uniqueCwds = uniqueStrings(cwds);
1035
+ const entries = await Promise.all(uniqueCwds.map(async (cwd) => [cwd, await this.sessionManager.list(cwd)]));
1036
+ return new Map(entries);
1037
+ }
1038
+ async archiveStoreArchiveMany(inputs) {
1039
+ if (inputs.length === 0)
1040
+ return [];
1041
+ if (this.archiveStore.archiveMany !== undefined)
1042
+ return this.archiveStore.archiveMany(inputs);
1043
+ const records = [];
1044
+ for (const input of inputs)
1045
+ records.push(await this.archiveStore.archive(input));
1046
+ return records;
1047
+ }
1048
+ async archiveStoreDeleteArchivedMany(sessionIds) {
1049
+ if (sessionIds.length === 0)
1050
+ return [];
1051
+ if (this.archiveStore.deleteArchivedMany !== undefined)
1052
+ return this.archiveStore.deleteArchivedMany(sessionIds);
1053
+ if (this.archiveStore.deleteArchived === undefined)
1054
+ throw new Error("Archive store does not support deletion");
1055
+ for (const sessionId of sessionIds)
1056
+ await this.archiveStore.deleteArchived(sessionId);
1057
+ return [...sessionIds];
1058
+ }
1059
+ async moveLegacyArchivedRecordsForDelete(records) {
1060
+ const legacyRecords = records.filter((record) => record.archivePath === undefined);
1061
+ if (legacyRecords.length === 0)
1062
+ return [];
1063
+ let sessionsByCwd;
1064
+ try {
1065
+ sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
1066
+ }
1067
+ catch (error) {
1068
+ return legacyRecords.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
1069
+ }
1070
+ const moveInputs = legacyRecords
1071
+ .map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
1072
+ .filter(isDefined)
1073
+ .map(archiveInputFromListEntry);
1074
+ if (moveInputs.length === 0)
1075
+ return [];
1076
+ try {
1077
+ await this.archiveStoreArchiveMany(moveInputs);
1078
+ return [];
1079
+ }
1080
+ catch (error) {
1081
+ const failedIds = new Set(moveInputs.map((input) => input.sessionId));
1082
+ return legacyRecords
1083
+ .filter((record) => failedIds.has(record.sessionId))
1084
+ .map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
1085
+ }
1086
+ }
881
1087
  async cleanupPlan(request) {
882
1088
  const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll?.() ?? [], this.archiveStore.list()]);
883
1089
  return planSessionCleanup({
@@ -922,7 +1128,19 @@ export class PiSessionService {
922
1128
  const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
923
1129
  if (session === undefined)
924
1130
  return record;
925
- return this.archiveStore.archive(archiveInputFromListEntry(session));
1131
+ const [moved] = await this.archiveStoreArchiveMany([archiveInputFromListEntry(session)]);
1132
+ return moved ?? record;
1133
+ }
1134
+ async ensureArchivedRecordsMoved(records) {
1135
+ const legacyRecords = records.filter((record) => record.archivePath === undefined);
1136
+ if (legacyRecords.length === 0)
1137
+ return;
1138
+ const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
1139
+ const moveInputs = legacyRecords
1140
+ .map((record) => sessionsByCwd.get(record.cwd)?.find((candidate) => candidate.id === record.sessionId))
1141
+ .filter(isDefined)
1142
+ .map(archiveInputFromListEntry);
1143
+ await this.archiveStoreArchiveMany(moveInputs);
926
1144
  }
927
1145
  async archiveInputForSession(session) {
928
1146
  const cwd = session.sessionManager.getCwd();
@@ -985,6 +1203,9 @@ export class PiSessionService {
985
1203
  return [...names];
986
1204
  }
987
1205
  async closeActive(sessionId) {
1206
+ const pendingOpens = this.pendingSessionOpenPromises(sessionId);
1207
+ if (pendingOpens.length > 0)
1208
+ await Promise.allSettled(pendingOpens);
988
1209
  const active = this.active.get(sessionId);
989
1210
  if (!active)
990
1211
  return;
@@ -1019,14 +1240,40 @@ export class PiSessionService {
1019
1240
  if (active !== undefined)
1020
1241
  return active;
1021
1242
  const archived = await this.getArchived(ref);
1022
- if (archived?.archivePath !== undefined)
1023
- return this.create(this.sessionManager.open(archived.archivePath), archived.cwd);
1243
+ if (archived?.archivePath !== undefined) {
1244
+ const { archivePath } = archived;
1245
+ return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath));
1246
+ }
1024
1247
  const match = isPiSessionRef(ref)
1025
1248
  ? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
1026
1249
  : (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
1027
1250
  if (!match)
1028
1251
  throw new Error("Session not found");
1029
- return this.create(this.sessionManager.open(match.path), match.cwd);
1252
+ return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path));
1253
+ }
1254
+ openExistingSession(sessionId, cwd, openSessionManager) {
1255
+ const active = this.activeForLookup({ id: sessionId, cwd });
1256
+ if (active !== undefined)
1257
+ return Promise.resolve(active);
1258
+ const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
1259
+ const existing = this.pendingSessionOpens.get(key);
1260
+ if (existing !== undefined)
1261
+ return existing.promise;
1262
+ const pending = {
1263
+ sessionId,
1264
+ promise: this.create(openSessionManager(), cwd),
1265
+ };
1266
+ pending.promise = pending.promise.finally(() => {
1267
+ if (this.pendingSessionOpens.get(key) === pending)
1268
+ this.pendingSessionOpens.delete(key);
1269
+ });
1270
+ this.pendingSessionOpens.set(key, pending);
1271
+ return pending.promise;
1272
+ }
1273
+ pendingSessionOpenPromises(sessionId) {
1274
+ return [...this.pendingSessionOpens.values()]
1275
+ .filter((pending) => sessionId === undefined || pending.sessionId === sessionId)
1276
+ .map((pending) => pending.promise);
1030
1277
  }
1031
1278
  async getArchived(ref) {
1032
1279
  const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
@@ -1047,20 +1294,53 @@ export class PiSessionService {
1047
1294
  }
1048
1295
  return undefined;
1049
1296
  }
1050
- async create(sessionManager, cwd) {
1051
- const runtime = await this.createAgentRuntime(this.createRuntime, { cwd, agentDir: this.agentDir, sessionManager });
1052
- await this.bindSessionExtensions(runtime.session);
1297
+ async create(sessionManager, cwd, options = {}) {
1298
+ const delegationToolsEnabled = options.creationProvenance !== "tracked-subsession"
1299
+ && await sessionAllowsDelegationTools(sessionManager, this.sessionManager);
1300
+ const runtime = await this.createAgentRuntime(this.createRuntime, {
1301
+ cwd,
1302
+ agentDir: this.agentDir,
1303
+ sessionManager,
1304
+ delegationToolsEnabled,
1305
+ ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
1306
+ });
1053
1307
  const active = { runtime, unsubscribe: noop };
1054
- this.bindRuntime(active);
1055
- runtime.setRebindSession(async (session) => {
1056
- await this.bindSessionExtensions(session);
1308
+ try {
1309
+ await this.bindSessionExtensions(runtime.session);
1057
1310
  this.bindRuntime(active);
1058
- await this.recoverSubsessionTrackingForOpenedSession(session);
1059
- });
1060
- this.active.set(runtime.session.sessionId, active);
1061
- await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
1062
- this.publishStatus(runtime.session);
1063
- return active;
1311
+ runtime.setRebindSession(async (session) => {
1312
+ await this.bindSessionExtensions(session);
1313
+ this.bindRuntime(active);
1314
+ await this.recoverSubsessionTrackingForOpenedSession(session);
1315
+ });
1316
+ this.active.set(runtime.session.sessionId, active);
1317
+ await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
1318
+ this.publishStatus(runtime.session);
1319
+ return active;
1320
+ }
1321
+ catch (error) {
1322
+ active.unsubscribe();
1323
+ let removedActive = false;
1324
+ for (const [sessionId, candidate] of this.active.entries()) {
1325
+ if (candidate !== active)
1326
+ continue;
1327
+ this.active.delete(sessionId);
1328
+ this.activities.delete(sessionId);
1329
+ this.clearAuthLossWarningsForSession(sessionId);
1330
+ this.clearCompactionPromptQueue(sessionId);
1331
+ removedActive = true;
1332
+ }
1333
+ if (removedActive) {
1334
+ this.workspaceActivity?.removeSession(runtime.session.sessionId, runtime.session.sessionManager.getCwd());
1335
+ }
1336
+ try {
1337
+ await runtime.session.abort();
1338
+ }
1339
+ finally {
1340
+ await runtime.dispose();
1341
+ }
1342
+ throw error;
1343
+ }
1064
1344
  }
1065
1345
  async bindSessionExtensions(session) {
1066
1346
  await session.bindExtensions({
@@ -1156,10 +1436,15 @@ export class PiSessionService {
1156
1436
  maybeGenerateSessionName(session, firstMessage) {
1157
1437
  if (session.sessionName !== undefined || session.messages.length !== 0 || session.isStreaming || session.isCompacting)
1158
1438
  return;
1439
+ const deterministicName = deterministicSessionName(firstMessage);
1440
+ if (deterministicName !== undefined) {
1441
+ this.applyGeneratedSessionName(session, deterministicName);
1442
+ return;
1443
+ }
1159
1444
  const model = session.model;
1160
1445
  if (model === undefined)
1161
1446
  return;
1162
- void generateShortSessionName(this.modelRegistry, model, firstMessage).then((name) => {
1447
+ void generateShortSessionName(session.agent.streamFn, model, firstMessage).then((name) => {
1163
1448
  this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
1164
1449
  }).catch(() => {
1165
1450
  this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
@@ -1336,6 +1621,7 @@ export class PiSessionService {
1336
1621
  const contextUsage = session.getContextUsage();
1337
1622
  return {
1338
1623
  sessionId: session.sessionId,
1624
+ persisted: sessionFileExists(session.sessionFile),
1339
1625
  ...(model === undefined ? {} : { model }),
1340
1626
  thinkingLevel: session.thinkingLevel,
1341
1627
  isStreaming: session.isStreaming,
@@ -1368,6 +1654,38 @@ function previewResponseFromPlan(plan) {
1368
1654
  ...(plan.skippedBusySessionIds.length === 0 ? {} : { skippedBusySessionIds: plan.skippedBusySessionIds }),
1369
1655
  };
1370
1656
  }
1657
+ function uniqueBulkSessionRefs(refs) {
1658
+ const seen = new Set();
1659
+ const unique = [];
1660
+ for (const ref of refs) {
1661
+ const key = `${ref.cwd ?? ""}\0${ref.id}`;
1662
+ if (seen.has(key))
1663
+ continue;
1664
+ seen.add(key);
1665
+ unique.push(ref);
1666
+ }
1667
+ return unique;
1668
+ }
1669
+ function bulkRefToLookup(ref) {
1670
+ return ref.cwd === undefined ? ref.id : { id: ref.id, cwd: ref.cwd };
1671
+ }
1672
+ function findArchivedRecordForBulkRef(records, ref) {
1673
+ return records.find((record) => (ref.cwd === undefined || record.cwd === ref.cwd) && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
1674
+ }
1675
+ function findListedSessionForBulkRef(context, ref) {
1676
+ if (ref.cwd !== undefined)
1677
+ return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
1678
+ return context.allSessions === undefined ? undefined : findSessionByIdOrPrefix(context.allSessions, ref.id);
1679
+ }
1680
+ function findSessionByIdOrPrefix(sessions, sessionId) {
1681
+ return sessions.find((session) => session.id === sessionId) ?? sessions.find((session) => session.id.startsWith(sessionId));
1682
+ }
1683
+ function uniqueStrings(values) {
1684
+ return [...new Set(values)];
1685
+ }
1686
+ function errorMessage(error) {
1687
+ return error instanceof Error ? error.message : String(error);
1688
+ }
1371
1689
  function modelToClientModel(model) {
1372
1690
  if (model === undefined)
1373
1691
  return {};
@@ -1386,6 +1704,7 @@ function clientSessionFromListEntry(session) {
1386
1704
  id: session.id,
1387
1705
  path: session.path,
1388
1706
  cwd: session.cwd,
1707
+ persisted: true,
1389
1708
  ...(session.name === undefined ? {} : { name: session.name }),
1390
1709
  created: session.created.toISOString(),
1391
1710
  modified: session.modified.toISOString(),
@@ -1514,6 +1833,76 @@ function archivedTimestamp(record) {
1514
1833
  function isDefined(value) {
1515
1834
  return value !== undefined;
1516
1835
  }
1836
+ /**
1837
+ * Resolve the delegation capability from server-owned, persisted session
1838
+ * provenance. A copied marker is not enough: the child header and reciprocal
1839
+ * parent link must identify the exact same session files.
1840
+ */
1841
+ export async function sessionAllowsDelegationTools(sessionManager, managers) {
1842
+ const trackedLink = await verifiedTrackedSubsessionLink(managers, {
1843
+ sessionId: sessionManager.getSessionId(),
1844
+ sessionFile: sessionManager.getSessionFile(),
1845
+ sessionManager,
1846
+ cwd: sessionManager.getCwd(),
1847
+ });
1848
+ return trackedLink === undefined;
1849
+ }
1850
+ async function verifiedTrackedSubsessionLink(managers, session) {
1851
+ // Child markers are only hints; the current child header and reciprocal
1852
+ // parent custom link must agree on the exact ids and files before relinking.
1853
+ const entries = session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch();
1854
+ let marker;
1855
+ for (const entry of entries) {
1856
+ const parsed = parsePersistedChildSubsessionLink(entry);
1857
+ if (parsed?.spawnedSessionId === session.sessionId)
1858
+ marker = parsed;
1859
+ }
1860
+ if (marker === undefined)
1861
+ return undefined;
1862
+ const childSessionFile = nonEmptyString(session.sessionFile);
1863
+ if (childSessionFile === undefined)
1864
+ return undefined;
1865
+ const childHeader = await readSessionHeaderSummary(childSessionFile);
1866
+ if (childHeader?.id !== session.sessionId)
1867
+ return undefined;
1868
+ const parentSessionFile = nonEmptyString(childHeader.parentSession);
1869
+ if (parentSessionFile === undefined)
1870
+ return undefined;
1871
+ const parentHeader = await readSessionHeaderSummary(parentSessionFile);
1872
+ if (parentHeader?.id !== marker.spawnedBySessionId)
1873
+ return undefined;
1874
+ const parentLink = findReciprocalParentSubsessionLink(managers, parentSessionFile, marker.spawnedBySessionId, session.sessionId, childSessionFile);
1875
+ if (parentLink === undefined)
1876
+ return undefined;
1877
+ return {
1878
+ parentSessionId: marker.spawnedBySessionId,
1879
+ childSessionId: session.sessionId,
1880
+ childSessionFile,
1881
+ parentSessionFile,
1882
+ cwd: parentLink.cwd ?? session.cwd,
1883
+ };
1884
+ }
1885
+ function findReciprocalParentSubsessionLink(managers, parentSessionFile, parentSessionId, childSessionId, childSessionFile) {
1886
+ let parentManager;
1887
+ try {
1888
+ parentManager = managers.open(parentSessionFile);
1889
+ }
1890
+ catch {
1891
+ return undefined;
1892
+ }
1893
+ const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
1894
+ for (const entry of entries) {
1895
+ const link = parsePersistedParentSubsessionLink(entry);
1896
+ if (link === undefined)
1897
+ continue;
1898
+ if (link.spawnedBySessionId !== parentSessionId || link.spawnedSessionId !== childSessionId)
1899
+ continue;
1900
+ if (link.spawnedSessionFile === undefined || !sessionPathsEqual(link.spawnedSessionFile, childSessionFile))
1901
+ continue;
1902
+ return link;
1903
+ }
1904
+ return undefined;
1905
+ }
1517
1906
  function trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile) {
1518
1907
  return {
1519
1908
  parentSessionId,
@@ -1579,6 +1968,16 @@ function subsessionHydratedParentKey(parentSessionId, parentSessionFile) {
1579
1968
  function sessionPathsEqual(a, b) {
1580
1969
  return cwdPathsEqual(a, b);
1581
1970
  }
1971
+ function sessionFileExists(sessionFile) {
1972
+ if (sessionFile === undefined || sessionFile === "")
1973
+ return false;
1974
+ try {
1975
+ return statSync(sessionFile).isFile();
1976
+ }
1977
+ catch {
1978
+ return false;
1979
+ }
1980
+ }
1582
1981
  function sessionFileMatches(session, expectedSessionFile) {
1583
1982
  const sessionFile = nonEmptyString(session.sessionFile);
1584
1983
  return sessionFile !== undefined && expectedSessionFile !== undefined && sessionPathsEqual(sessionFile, expectedSessionFile);