@chainingintention/pi-web-cn 1.202609.2 → 1.202609.4

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 (34) hide show
  1. package/dist/client/assets/{CodeViewer-CY319sHK.js → CodeViewer-ujpWthUP.js} +1 -1
  2. package/dist/client/assets/{TerminalPanel-DvFd2ZkS.js → TerminalPanel-Br4uzklw.js} +1 -1
  3. package/dist/client/assets/{index-Lt_wrMvn.js → index-kNZIQ5Kv.js} +554 -305
  4. package/dist/client/index.html +1 -1
  5. package/dist/server/app.js +1 -1
  6. package/dist/server/app.js.map +1 -1
  7. package/dist/server/machines/machineClient.js +13 -11
  8. package/dist/server/machines/machineClient.js.map +1 -1
  9. package/dist/server/machines/machineRoutes.js +9 -2
  10. package/dist/server/machines/machineRoutes.js.map +1 -1
  11. package/dist/server/normalAuth.js +19 -2
  12. package/dist/server/normalAuth.js.map +1 -1
  13. package/dist/server/sessiond/sessionProxyRoutes.js +19 -14
  14. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  15. package/dist/server/sessiond.js +2 -4
  16. package/dist/server/sessiond.js.map +1 -1
  17. package/dist/server/sessions/piSessionService.js +127 -110
  18. package/dist/server/sessions/piSessionService.js.map +1 -1
  19. package/dist/server/sessions/relaySkill.js +25 -13
  20. package/dist/server/sessions/relaySkill.js.map +1 -1
  21. package/dist/server/sessions/sessionFileHeader.js +41 -0
  22. package/dist/server/sessions/sessionFileHeader.js.map +1 -1
  23. package/dist/server/sessions/sessionNotificationStore.js +2 -0
  24. package/dist/server/sessions/sessionNotificationStore.js.map +1 -1
  25. package/dist/server/sessions/sessionRoutes.js +9 -9
  26. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  27. package/dist/server/sessions/sessionUnreadStore.js +2 -0
  28. package/dist/server/sessions/sessionUnreadStore.js.map +1 -1
  29. package/dist/server/workbench/skillSync.js +7 -1
  30. package/dist/server/workbench/skillSync.js.map +1 -1
  31. package/dist/server/workspaces/fileContentService.js +13 -1
  32. package/dist/server/workspaces/fileContentService.js.map +1 -1
  33. package/package.json +6 -5
  34. package/skills/relay-runner/SKILL.md +245 -0
@@ -1,5 +1,5 @@
1
1
  import { realpathSync, statSync } from "node:fs";
2
- import { readFile, writeFile } from "node:fs/promises";
2
+ import { readFile } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
5
5
  import { projectBrowserMessage } from "../browserMessageProjection.js";
@@ -22,7 +22,7 @@ import { SessionActivityCoordinator } from "./sessionActivityCoordinator.js";
22
22
  import { searchSessionContent } from "./sessionContentSearch.js";
23
23
  import { createLocalOnlyModelRuntime } from "./authService.js";
24
24
  import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
25
- import { readSessionHeaderSummary } from "./sessionFileHeader.js";
25
+ import { clearSessionFileParent, readSessionHeaderSummary } from "./sessionFileHeader.js";
26
26
  import { workbenchAccessHandle } from "../managementEmbed.js";
27
27
  import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS, piWebDataDir } from "../../config.js";
28
28
  import { createScopedSettingsManager, resolveSettingsScopeDirectory } from "./projectSettingsScope.js";
@@ -38,7 +38,7 @@ import { createManagedAgentToolOptions, createManagedPythonToolDefinition } from
38
38
  import { PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR, managementAgentToolNames, withRuntimeCreationEnvironment, writeManagementPermissionSystemPolicy } from "./managementPermissionSystem.js";
39
39
  import { SessionNotificationStore } from "./sessionNotificationStore.js";
40
40
  import { plainTextTheme } from "./plainTextTheme.js";
41
- import { deduplicateBundledRelaySkill, ensureManagedRelaySkill, MANAGED_RELAY_SKILL_DIRECTORY } from "./relaySkill.js";
41
+ import { deduplicateBundledRelaySkills, ensureManagedRelaySkills, MANAGED_RELAY_SKILL_NAMES } from "./relaySkill.js";
42
42
  import { SessionUnreadStore } from "./sessionUnreadStore.js";
43
43
  import { archiveCandidateFromActiveSession, archiveCandidateFromArchivedRecord, archiveCandidateFromListEntry, archiveInputFromActiveSession, archiveInputFromCandidate, archiveInputFromListEntry, clientSessionFromArchivedRecord, compareArchivedRecords, } from "./sessionArchiveMapping.js";
44
44
  import { annotateAssistantThinkingLevel, getBoolean, getProperty, getString, toClientEvent } from "./sessionUiEvents.js";
@@ -203,7 +203,7 @@ export function filterManagedProjectSkills(cwd, base) {
203
203
  export function filterManagedWorkbenchSkills(cwd, base, receipt) {
204
204
  const projectRoot = realpathSync(cwd);
205
205
  const allowedDirectories = new Set([
206
- resolve(projectRoot, MANAGED_RELAY_SKILL_DIRECTORY),
206
+ ...MANAGED_RELAY_SKILL_NAMES.map((name) => resolve(projectRoot, ".pi", "skills", name)),
207
207
  ...receipt.skills.map((skill) => resolve(projectRoot, ".pi", "skills", skill.directory)),
208
208
  ]);
209
209
  const pathAllowed = (path) => [...allowedDirectories].some((directory) => pathInsideRealDirectory(directory, path));
@@ -393,7 +393,7 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
393
393
  });
394
394
  const resourceLoaderOptions = {
395
395
  ...piWebResourceLoaderOptions(appendSystemPromptSections),
396
- skillsOverride: (base) => deduplicateBundledRelaySkill(cwd, base),
396
+ skillsOverride: (base) => deduplicateBundledRelaySkills(cwd, base),
397
397
  };
398
398
  const services = await createAgentSessionServices({
399
399
  cwd,
@@ -424,7 +424,7 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
424
424
  }
425
425
  function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd) {
426
426
  return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled = true }) => {
427
- await ensureManagedRelaySkill(cwd);
427
+ await ensureManagedRelaySkills(cwd);
428
428
  const auditIdentity = await resolveManagementAuditIdentity(cwd, managementContext, managementProjectIdForCwd, logger);
429
429
  const accessHandle = workbenchAccessHandle(managementContext);
430
430
  const state = workbench === undefined ? undefined : workbench.accessStates.require(accessHandle);
@@ -602,6 +602,9 @@ export class PiSessionService {
602
602
  * child that works again (and stops again) notifies the parent each time.
603
603
  */
604
604
  this.subsessionNotifyArmed = new Map();
605
+ this.scopedNotificationStores = new Map();
606
+ this.scopedUnreadStores = new Map();
607
+ this.sessionEventScopes = new WeakMap();
605
608
  this.notificationGenerationBySession = new WeakMap();
606
609
  this.idleSessionFileResolutions = new WeakMap();
607
610
  /** The parked extension Promise resolvers behind the store's open dialogs. */
@@ -697,7 +700,8 @@ export class PiSessionService {
697
700
  async cleanupPreview(request) {
698
701
  return previewResponseFromPlan(await this.cleanupPlan(request));
699
702
  }
700
- async cleanup(request) {
703
+ async cleanup(request, context) {
704
+ const scope = eventScopeFromManagementContext(context);
701
705
  const plan = await this.cleanupPlan(request);
702
706
  if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
703
707
  throw new Error("Archive store does not support deletion");
@@ -711,24 +715,24 @@ export class PiSessionService {
711
715
  skippedBusySessionIds.add(input.sessionId);
712
716
  continue;
713
717
  }
714
- await this.closeActive(input.sessionId, { kind: "clear", reason: "archive" });
718
+ await this.closeActive(input.sessionId, { kind: "clear", reason: "archive" }, scope);
715
719
  readyArchiveInputs.push(input);
716
720
  }
717
721
  await this.archiveStoreArchiveMany(readyArchiveInputs);
718
722
  archiveInputs.push(...readyArchiveInputs);
719
- await this.forgetUnreadSessions(readyArchiveInputs);
723
+ await this.forgetUnreadSessions(readyArchiveInputs, scope);
720
724
  for (const record of plan.deleteRecords) {
721
725
  if (this.activeSessionHasWork(record.sessionId)) {
722
726
  skippedBusySessionIds.add(record.sessionId);
723
727
  continue;
724
728
  }
725
- await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
729
+ await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" }, scope);
726
730
  readyDeleteRecords.push(record);
727
731
  }
728
732
  await this.ensureArchivedRecordsMoved(readyDeleteRecords);
729
733
  const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
730
734
  deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
731
- await this.forgetUnreadSessions(deleteRecords);
735
+ await this.forgetUnreadSessions(deleteRecords, scope);
732
736
  return summarizeSessionCleanupExecution({
733
737
  archiveInputs,
734
738
  deleteRecords,
@@ -767,6 +771,8 @@ export class PiSessionService {
767
771
  this.subsessionHydratedParents.clear();
768
772
  this.subsessionNotifyArmed.clear();
769
773
  this.notificationStore.clearAll("service-dispose");
774
+ for (const store of this.scopedNotificationStores.values())
775
+ store.clearAll("service-dispose");
770
776
  await Promise.all(activeSessions.map(async (active) => {
771
777
  active.unsubscribe();
772
778
  this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
@@ -831,13 +837,13 @@ export class PiSessionService {
831
837
  .map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
832
838
  const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
833
839
  for (const record of archivedForCwd) {
834
- this.publishNotificationMutations(this.notificationStore.clearSession(record.sessionId, "archive-reconcile"));
840
+ this.publishNotificationMutations(this.notificationsForScope(eventScopeFromManagementContext(managementContext)).clearSession(record.sessionId, "archive-reconcile"));
835
841
  }
836
842
  const unarchivedSessions = sessions.filter((session) => !archivedById.has(session.id)).map(clientSessionFromListEntry);
837
843
  const reconcilableSessionIds = this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById);
838
844
  this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds, eventScopeFromManagementContext(managementContext));
839
845
  if (managementContext === undefined) {
840
- await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
846
+ await this.publishUnreadMutations(this.unreadForScope(eventScopeFromManagementContext(managementContext)).reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
841
847
  }
842
848
  const archivedSessions = archivedForCwd
843
849
  .sort(compareArchivedRecords)
@@ -946,7 +952,7 @@ export class PiSessionService {
946
952
  ...(parentSessionFile === undefined ? {} : { parentSessionFile }),
947
953
  cwd: decision.cwd,
948
954
  };
949
- await this.registerVerifiedSubsession(link);
955
+ await this.registerVerifiedSubsession(link, eventScopeFromManagementContext(input.managementContext));
950
956
  this.persistSubsessionLink(link);
951
957
  this.persistSubsessionChildMarker(input.parentSessionId, created.id);
952
958
  await this.prompt(created.id, input.prompt, undefined, undefined, {
@@ -1321,7 +1327,7 @@ export class PiSessionService {
1321
1327
  return undefined;
1322
1328
  return sessionFileMatches(session, link.childSessionFile) ? link : undefined;
1323
1329
  }
1324
- async registerVerifiedSubsession(link) {
1330
+ async registerVerifiedSubsession(link, scope = NORMAL_SESSION_EVENT_SCOPE) {
1325
1331
  const { childSessionId, parentSessionId } = link;
1326
1332
  const previousParentId = this.subsessionParents.get(childSessionId);
1327
1333
  if (previousParentId !== undefined && previousParentId !== parentSessionId) {
@@ -1338,7 +1344,7 @@ export class PiSessionService {
1338
1344
  if (!this.subsessionNotifyArmed.has(childSessionId))
1339
1345
  this.subsessionNotifyArmed.set(childSessionId, false);
1340
1346
  const cwd = this.cwdForVerifiedSubsession(link);
1341
- await this.publishUnreadMutations(this.unreadStore.excludeSession(childSessionId, cwd));
1347
+ await this.publishUnreadMutations(this.unreadForScope(scope).excludeSession(childSessionId, cwd));
1342
1348
  }
1343
1349
  cwdForVerifiedSubsession(link) {
1344
1350
  const activeCwd = this.activeChildForSubsessionLink(link)?.runtime.session.sessionManager.getCwd();
@@ -1401,7 +1407,7 @@ export class PiSessionService {
1401
1407
  const activeParent = this.activeForSessionId(parentSessionId);
1402
1408
  if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
1403
1409
  const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
1404
- const complete = await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
1410
+ const complete = await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile, activeParent.eventScope);
1405
1411
  if (complete)
1406
1412
  this.subsessionHydratedParents.add(hydrationKey);
1407
1413
  return;
@@ -1421,7 +1427,7 @@ export class PiSessionService {
1421
1427
  if (complete)
1422
1428
  this.subsessionHydratedParents.add(hydrationKey);
1423
1429
  }
1424
- async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile) {
1430
+ async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile, scope = NORMAL_SESSION_EVENT_SCOPE) {
1425
1431
  // Parent custom links are the authoritative recovery record: verify the
1426
1432
  // exact live child file/header before tracking. Do not negatively cache a
1427
1433
  // scan while a candidate child is temporarily unavailable.
@@ -1436,7 +1442,7 @@ export class PiSessionService {
1436
1442
  complete = false;
1437
1443
  continue;
1438
1444
  }
1439
- await this.registerVerifiedSubsession(verified);
1445
+ await this.registerVerifiedSubsession(verified, scope);
1440
1446
  }
1441
1447
  return complete;
1442
1448
  }
@@ -1457,7 +1463,7 @@ export class PiSessionService {
1457
1463
  const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
1458
1464
  if (link === undefined)
1459
1465
  return;
1460
- await this.registerVerifiedSubsession(link);
1466
+ await this.registerVerifiedSubsession(link, this.eventScopeForSession(session));
1461
1467
  }
1462
1468
  verifiedSubsessionLinkFromOpenedChild(session) {
1463
1469
  return verifiedTrackedSubsessionLink(this.sessionManager, {
@@ -1995,7 +2001,7 @@ export class PiSessionService {
1995
2001
  const archiveInput = await this.archiveInputForSession(session);
1996
2002
  await this.closeActiveSession(active);
1997
2003
  await this.archiveStore.archive(archiveInput);
1998
- await this.forgetUnreadSessions([archiveInput]);
2004
+ await this.forgetUnreadSessions([archiveInput], active.eventScope);
1999
2005
  });
2000
2006
  }
2001
2007
  async archiveMany(refs, managementContext) {
@@ -2011,7 +2017,7 @@ export class PiSessionService {
2011
2017
  for (const ref of uniqueRefs) {
2012
2018
  const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
2013
2019
  if (archived !== undefined) {
2014
- this.publishNotificationMutations(this.notificationStore.clearSession(archived.sessionId, "archive"));
2020
+ this.publishNotificationMutations(this.notificationsForScope(eventScopeFromManagementContext(managementContext)).clearSession(archived.sessionId, "archive"));
2015
2021
  alreadyArchivedSessionIds.push(archived.sessionId);
2016
2022
  unreadArchivedIdentities.push(archived);
2017
2023
  continue;
@@ -2075,7 +2081,7 @@ export class PiSessionService {
2075
2081
  failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
2076
2082
  }
2077
2083
  });
2078
- await this.forgetUnreadSessions(unreadArchivedIdentities);
2084
+ await this.forgetUnreadSessions(unreadArchivedIdentities, eventScopeFromManagementContext(managementContext));
2079
2085
  return {
2080
2086
  archived: true,
2081
2087
  archivedSessionIds: uniqueStrings(archivedSessionIds),
@@ -2098,13 +2104,13 @@ export class PiSessionService {
2098
2104
  })), `归档 ${sessionDisplayName(session)} 前请先停止当前会话活动`, async () => {
2099
2105
  for (const target of plan.targets) {
2100
2106
  if (target.archived)
2101
- this.publishNotificationMutations(this.notificationStore.clearSession(target.id, "archive"));
2107
+ this.publishNotificationMutations(this.notificationsForScope(eventScopeFromManagementContext(managementContext)).clearSession(target.id, "archive"));
2102
2108
  }
2103
2109
  for (const input of archiveInputs)
2104
2110
  await this.closeActiveInScope(input.sessionId, eventScopeFromManagementContext(managementContext));
2105
2111
  await this.archiveStoreArchiveMany(archiveInputs);
2106
2112
  });
2107
- await this.forgetUnreadSessions(plan.targets.map((target) => ({ sessionId: target.id, cwd: target.cwd })));
2113
+ await this.forgetUnreadSessions(plan.targets.map((target) => ({ sessionId: target.id, cwd: target.cwd })), eventScopeFromManagementContext(managementContext));
2108
2114
  return {
2109
2115
  archived: true,
2110
2116
  sessionIds: archiveInputs.map((input) => input.sessionId),
@@ -2118,7 +2124,7 @@ export class PiSessionService {
2118
2124
  throw new Error("未找到会话");
2119
2125
  await this.closeActiveInScope(archived.sessionId, eventScopeFromManagementContext(managementContext));
2120
2126
  await this.archiveStore.restore(archived.sessionId);
2121
- await this.forgetUnreadSessions([archived]);
2127
+ await this.forgetUnreadSessions([archived], eventScopeFromManagementContext(managementContext));
2122
2128
  }
2123
2129
  async deleteArchived(ref, managementContext) {
2124
2130
  const record = await this.getArchived(ref);
@@ -2130,7 +2136,7 @@ export class PiSessionService {
2130
2136
  if (record.archivePath === undefined)
2131
2137
  await this.ensureArchivedRecordMoved(record);
2132
2138
  await this.archiveStore.deleteArchived(record.sessionId);
2133
- await this.forgetUnreadSessions([record]);
2139
+ await this.forgetUnreadSessions([record], eventScopeFromManagementContext(managementContext));
2134
2140
  }
2135
2141
  async deleteArchivedMany(refs, managementContext) {
2136
2142
  if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
@@ -2177,7 +2183,7 @@ export class PiSessionService {
2177
2183
  failures.push({ sessionId, error: errorMessage(error) });
2178
2184
  }
2179
2185
  const deletedIdSet = new Set(deletedSessionIds);
2180
- await this.forgetUnreadSessions(readyRecords.filter((record) => deletedIdSet.has(record.sessionId)));
2186
+ await this.forgetUnreadSessions(readyRecords.filter((record) => deletedIdSet.has(record.sessionId)), eventScopeFromManagementContext(managementContext));
2181
2187
  return {
2182
2188
  deleted: true,
2183
2189
  deletedSessionIds,
@@ -2202,19 +2208,19 @@ export class PiSessionService {
2202
2208
  await this.closeActiveSession(active, priorGeneration === undefined ? CLEAR_RUNTIME_NOTIFICATIONS : DEFER_RUNTIME_NOTIFICATIONS);
2203
2209
  candidateGeneration = priorGeneration === undefined
2204
2210
  ? undefined
2205
- : this.notificationStore.beginReplacement(priorGeneration, { sessionId, cwd });
2211
+ : this.notificationsForSession(session).beginReplacement(priorGeneration, { sessionId, cwd });
2206
2212
  const reopened = await this.openExistingSession(sessionId, cwd, () => this.sessionManager.open(sessionFile), {
2207
2213
  ...(managementContext === undefined ? {} : { managementContext }),
2208
2214
  ...(candidateGeneration === undefined ? {} : { notificationGeneration: candidateGeneration }),
2209
2215
  });
2210
2216
  if (candidateGeneration !== undefined) {
2211
- this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
2217
+ this.publishNotificationMutations(this.notificationsForSession(session).commitReplacement(candidateGeneration));
2212
2218
  }
2213
2219
  return reopened.runtime.session;
2214
2220
  }
2215
2221
  catch (error) {
2216
2222
  if (candidateGeneration !== undefined) {
2217
- this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration));
2223
+ this.publishNotificationMutations(this.notificationsForSession(session).abortReplacement(candidateGeneration));
2218
2224
  }
2219
2225
  throw error;
2220
2226
  }
@@ -2226,11 +2232,11 @@ export class PiSessionService {
2226
2232
  const sessionFile = session.sessionFile;
2227
2233
  if (sessionFile === undefined || sessionFile === "")
2228
2234
  throw new Error("会话尚未持久化");
2229
- await clearParentSession(sessionFile);
2235
+ clearSessionFileParent(sessionFile);
2230
2236
  this.sessionManager.invalidateSessionFile?.(sessionFile);
2231
2237
  clearParentSessionHeader(session.sessionManager);
2232
2238
  this.unregisterSubsession(session.sessionId);
2233
- await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }]);
2239
+ await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }], this.eventScopeForSession(session));
2234
2240
  }
2235
2241
  async abort(ref, managementContext) {
2236
2242
  const active = this.activeForLookup(ref, managementContext);
@@ -2278,7 +2284,7 @@ export class PiSessionService {
2278
2284
  return;
2279
2285
  }
2280
2286
  if (isPiSessionRef(ref)) {
2281
- this.publishNotificationMutations(this.notificationStore.clearSessionIdentity(ref.id, canonicalizeStoredCwd(ref.cwd), "runtime-close"));
2287
+ this.publishNotificationMutations(this.notificationsForScope(eventScopeFromManagementContext(managementContext)).clearSessionIdentity(ref.id, canonicalizeStoredCwd(ref.cwd), "runtime-close"));
2282
2288
  }
2283
2289
  }
2284
2290
  async bulkSessionLookupContext(refs) {
@@ -2455,17 +2461,17 @@ export class PiSessionService {
2455
2461
  }
2456
2462
  return [...names];
2457
2463
  }
2458
- async closeActive(sessionId, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
2464
+ async closeActive(sessionId, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS, eventScope = NORMAL_SESSION_EVENT_SCOPE) {
2459
2465
  for (const startup of this.startupSessions.values()) {
2460
- if (startup.session.sessionId === sessionId)
2466
+ if (startup.session.sessionId === sessionId && startup.eventScope === eventScope)
2461
2467
  this.endSessionExtensionDialogs(startup.session);
2462
2468
  }
2463
2469
  const pendingOpens = this.pendingSessionOpenPromises(sessionId);
2464
2470
  if (pendingOpens.length > 0)
2465
2471
  await Promise.allSettled(pendingOpens);
2466
- const activeSessions = this.activeSessionsForId(sessionId);
2472
+ const activeSessions = this.activeSessionsForId(sessionId).filter((active) => active.eventScope === eventScope);
2467
2473
  if (activeSessions.length === 0 && notificationPolicy.kind === "clear") {
2468
- this.publishNotificationMutations(this.notificationStore.clearSession(sessionId, notificationPolicy.reason));
2474
+ this.publishNotificationMutations(this.notificationsForScope(eventScope).clearSession(sessionId, notificationPolicy.reason));
2469
2475
  return;
2470
2476
  }
2471
2477
  await Promise.all(activeSessions.map((active) => this.closeActiveSession(active, notificationPolicy)));
@@ -2476,16 +2482,17 @@ export class PiSessionService {
2476
2482
  await this.closeActiveSession(active, notificationPolicy);
2477
2483
  }
2478
2484
  else if (notificationPolicy.kind === "clear") {
2479
- this.publishNotificationMutations(this.notificationStore.clearSession(sessionId, notificationPolicy.reason));
2485
+ this.publishNotificationMutations(this.notificationsForScope(eventScope).clearSession(sessionId, notificationPolicy.reason));
2480
2486
  }
2481
2487
  }
2482
2488
  async closeActiveSession(active, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
2483
2489
  const sessionId = active.runtime.session.sessionId;
2484
2490
  if (notificationPolicy.kind === "clear") {
2485
2491
  const generation = this.notificationGenerationBySession.get(active.runtime.session);
2492
+ const store = this.notificationsForScope(active.eventScope);
2486
2493
  const mutations = generation === undefined
2487
- ? this.notificationStore.clearSession(sessionId, notificationPolicy.reason)
2488
- : this.notificationStore.clearGeneration(generation, notificationPolicy.reason);
2494
+ ? store.clearSession(sessionId, notificationPolicy.reason)
2495
+ : store.clearGeneration(generation, notificationPolicy.reason);
2489
2496
  this.publishNotificationMutations(mutations);
2490
2497
  }
2491
2498
  this.forgetUnreadActivity(active.runtime.session);
@@ -2718,6 +2725,7 @@ export class PiSessionService {
2718
2725
  }
2719
2726
  }
2720
2727
  bindRuntime(active, session = active.runtime.session) {
2728
+ this.sessionEventScopes.set(session, active.eventScope);
2721
2729
  this.runtimeBySession.set(session, active.runtime);
2722
2730
  active.unsubscribe();
2723
2731
  for (const [sessionId, candidate] of this.active.entries()) {
@@ -3225,6 +3233,9 @@ export class PiSessionService {
3225
3233
  return this.activities.get(activeSessionKey(session.sessionId, this.eventScopeForSession(session)));
3226
3234
  }
3227
3235
  eventScopeForSession(session) {
3236
+ const boundScope = this.sessionEventScopes.get(session);
3237
+ if (boundScope !== undefined)
3238
+ return boundScope;
3228
3239
  for (const active of this.active.values()) {
3229
3240
  if (active.runtime.session === session)
3230
3241
  return active.eventScope;
@@ -3299,41 +3310,60 @@ export class PiSessionService {
3299
3310
  hasQueuedMessageText(session, text) {
3300
3311
  return queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)).some((message) => message.text === text);
3301
3312
  }
3302
- notificationCatalog() {
3303
- return this.notificationStore.catalogSnapshot();
3313
+ notificationsForScope(scope) {
3314
+ if (scope === NORMAL_SESSION_EVENT_SCOPE)
3315
+ return this.notificationStore;
3316
+ let store = this.scopedNotificationStores.get(scope);
3317
+ if (store === undefined) {
3318
+ store = new SessionNotificationStore({ eventScope: scope, now: this.now });
3319
+ this.scopedNotificationStores.set(scope, store);
3320
+ }
3321
+ return store;
3322
+ }
3323
+ notificationsForSession(session) {
3324
+ return this.notificationsForScope(this.eventScopeForSession(session));
3304
3325
  }
3305
- async unreadCatalog() {
3326
+ unreadForScope(scope) {
3327
+ if (scope === NORMAL_SESSION_EVENT_SCOPE)
3328
+ return this.unreadStore;
3329
+ let store = this.scopedUnreadStores.get(scope);
3330
+ if (store === undefined) {
3331
+ store = new SessionUnreadStore({ eventScope: scope, now: this.now });
3332
+ this.scopedUnreadStores.set(scope, store);
3333
+ }
3334
+ return store;
3335
+ }
3336
+ notificationCatalog(context) {
3337
+ return this.notificationsForScope(eventScopeFromManagementContext(context)).catalogSnapshot();
3338
+ }
3339
+ async unreadCatalog(context) {
3306
3340
  await this.publishUnreadMutations([]);
3307
- return this.unreadStore.durableCatalogSnapshot();
3341
+ return this.unreadForScope(eventScopeFromManagementContext(context)).durableCatalogSnapshot();
3308
3342
  }
3309
- /** Scope-filtered unread view used only by the daemon-owned status tree. */
3343
+ /** The status tree and HTTP catalog must read the same scoped store. */
3310
3344
  async unreadCatalogForScope(eventScope) {
3311
- const catalog = await this.unreadCatalog();
3312
- if (eventScope === NORMAL_SESSION_EVENT_SCOPE)
3313
- return catalog;
3314
- const visible = new Set([...this.active.values()]
3315
- .filter((active) => active.eventScope === eventScope)
3316
- .map((active) => active.runtime.session.sessionId));
3317
- return { ...catalog, sessions: catalog.sessions.filter((session) => visible.has(session.sessionId)) };
3318
- }
3319
- async acknowledgeUnread(sessionId, request) {
3320
- const result = this.unreadStore.acknowledge(sessionId, {
3345
+ await this.publishUnreadMutations([]);
3346
+ return this.unreadForScope(eventScope).durableCatalogSnapshot();
3347
+ }
3348
+ async acknowledgeUnread(sessionId, request, context) {
3349
+ const store = this.unreadForScope(eventScopeFromManagementContext(context));
3350
+ const result = store.acknowledge(sessionId, {
3321
3351
  ...request,
3322
3352
  cwd: canonicalizeStoredCwd(request.cwd),
3323
3353
  });
3324
3354
  await this.publishUnreadMutations(result.mutations);
3325
- return this.unreadStore.durableCatalogSnapshot();
3355
+ return store.durableCatalogSnapshot();
3326
3356
  }
3327
- notificationInbox(ref) {
3328
- return this.notificationStore.inboxSnapshot(ref.id, canonicalizeStoredCwd(ref.cwd));
3357
+ notificationInbox(ref, context) {
3358
+ return this.notificationsForScope(eventScopeFromManagementContext(context)).inboxSnapshot(ref.id, canonicalizeStoredCwd(ref.cwd));
3329
3359
  }
3330
- dismissNotification(ref, request) {
3331
- const result = this.notificationStore.dismissNotification(ref.id, canonicalizeStoredCwd(ref.cwd), request.daemonInstanceId, request.notificationId);
3360
+ dismissNotification(ref, request, context) {
3361
+ const result = this.notificationsForScope(eventScopeFromManagementContext(context)).dismissNotification(ref.id, canonicalizeStoredCwd(ref.cwd), request.daemonInstanceId, request.notificationId);
3332
3362
  this.publishNotificationMutations(result.mutations);
3333
3363
  return result.snapshot;
3334
3364
  }
3335
- dismissAllNotifications(ref, request) {
3336
- const result = this.notificationStore.dismissAll(ref.id, canonicalizeStoredCwd(ref.cwd), request.daemonInstanceId, request.throughOrder, request.throughOverflowWatermark);
3365
+ dismissAllNotifications(ref, request, context) {
3366
+ const result = this.notificationsForScope(eventScopeFromManagementContext(context)).dismissAll(ref.id, canonicalizeStoredCwd(ref.cwd), request.daemonInstanceId, request.throughOrder, request.throughOverflowWatermark);
3337
3367
  this.publishNotificationMutations(result.mutations);
3338
3368
  return result.snapshot;
3339
3369
  }
@@ -3354,20 +3384,20 @@ export class PiSessionService {
3354
3384
  try {
3355
3385
  await session.reload(priorGeneration === undefined ? undefined : {
3356
3386
  beforeSessionStart: () => {
3357
- candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
3387
+ candidateGeneration = this.notificationsForSession(session).beginReplacement(priorGeneration, notificationIdentityForSession(session));
3358
3388
  this.notificationGenerationBySession.set(session, candidateGeneration);
3359
3389
  this.replaceSessionNotificationContext(session, candidateGeneration);
3360
3390
  },
3361
3391
  });
3362
3392
  if (candidateGeneration !== undefined) {
3363
- this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
3393
+ this.publishNotificationMutations(this.notificationsForSession(session).commitReplacement(candidateGeneration));
3364
3394
  }
3365
3395
  this.publishActivity(session, "资源已重新加载", "idle");
3366
3396
  this.publishStatus(session);
3367
3397
  }
3368
3398
  catch (error) {
3369
3399
  if (candidateGeneration !== undefined) {
3370
- this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
3400
+ this.publishNotificationMutations(this.notificationsForSession(session).abortReplacement(candidateGeneration, "candidate"));
3371
3401
  this.notificationGenerationBySession.set(session, candidateGeneration);
3372
3402
  }
3373
3403
  const message = error instanceof Error ? error.message : String(error);
@@ -3429,6 +3459,8 @@ export class PiSessionService {
3429
3459
  }),
3430
3460
  };
3431
3461
  let boundSession = runtime.session;
3462
+ this.sessionEventScopes.set(boundSession, active.eventScope);
3463
+ const notificationStore = this.notificationsForScope(active.eventScope);
3432
3464
  let notificationGeneration = options.notificationGeneration;
3433
3465
  let notificationOwnership = options.notifications === "disabled"
3434
3466
  ? "disabled"
@@ -3437,13 +3469,13 @@ export class PiSessionService {
3437
3469
  : "external";
3438
3470
  if (notificationOwnership === "registered") {
3439
3471
  const notificationIdentity = notificationIdentityForSession(runtime.session);
3440
- const existingCandidate = this.notificationStore.beginReplacementForSession(notificationIdentity.sessionId, notificationIdentity.cwd);
3472
+ const existingCandidate = notificationStore.beginReplacementForSession(notificationIdentity.sessionId, notificationIdentity.cwd);
3441
3473
  if (existingCandidate !== undefined) {
3442
3474
  notificationGeneration = existingCandidate;
3443
3475
  notificationOwnership = "replacement";
3444
3476
  }
3445
3477
  else {
3446
- const registration = this.notificationStore.registerSession(notificationIdentity.sessionId, notificationIdentity.cwd);
3478
+ const registration = notificationStore.registerSession(notificationIdentity.sessionId, notificationIdentity.cwd);
3447
3479
  notificationGeneration = registration.generation;
3448
3480
  this.publishNotificationMutations(registration.mutations);
3449
3481
  }
@@ -3452,7 +3484,7 @@ export class PiSessionService {
3452
3484
  this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
3453
3485
  try {
3454
3486
  if (options.creationProvenance === "tracked-subsession") {
3455
- await this.publishUnreadMutations(this.unreadStore.excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
3487
+ await this.publishUnreadMutations(this.unreadForScope(active.eventScope).excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
3456
3488
  }
3457
3489
  else {
3458
3490
  await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
@@ -3461,13 +3493,14 @@ export class PiSessionService {
3461
3493
  await this.bindSessionExtensions(runtime.session, notificationGeneration, active);
3462
3494
  this.bindRuntime(active);
3463
3495
  runtime.setRebindSession(async (session) => {
3496
+ this.sessionEventScopes.set(session, active.eventScope);
3464
3497
  const priorGeneration = notificationGeneration;
3465
3498
  let candidateGeneration;
3466
3499
  try {
3467
3500
  await this.prepareUnreadRuntimeRebind(boundSession, session);
3468
3501
  await this.recoverSubsessionTrackingForOpenedSession(session);
3469
3502
  if (priorGeneration !== undefined) {
3470
- candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
3503
+ candidateGeneration = notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
3471
3504
  this.notificationGenerationBySession.set(session, candidateGeneration);
3472
3505
  }
3473
3506
  this.bindRuntime(active, session);
@@ -3478,13 +3511,13 @@ export class PiSessionService {
3478
3511
  boundSession = session;
3479
3512
  await this.bindSessionExtensions(session, candidateGeneration, active);
3480
3513
  if (candidateGeneration !== undefined) {
3481
- this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
3514
+ this.publishNotificationMutations(notificationStore.commitReplacement(candidateGeneration));
3482
3515
  notificationGeneration = candidateGeneration;
3483
3516
  }
3484
3517
  }
3485
3518
  catch (error) {
3486
3519
  if (candidateGeneration !== undefined) {
3487
- this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
3520
+ this.publishNotificationMutations(notificationStore.abortReplacement(candidateGeneration, "candidate"));
3488
3521
  notificationGeneration = candidateGeneration;
3489
3522
  this.notificationGenerationBySession.set(session, candidateGeneration);
3490
3523
  }
@@ -3493,7 +3526,7 @@ export class PiSessionService {
3493
3526
  });
3494
3527
  this.active.set(activeSessionKey(runtime.session.sessionId, active.eventScope), active);
3495
3528
  if (notificationOwnership === "replacement" && notificationGeneration !== undefined) {
3496
- this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
3529
+ this.publishNotificationMutations(notificationStore.commitReplacement(notificationGeneration));
3497
3530
  notificationOwnership = "external";
3498
3531
  }
3499
3532
  this.publishStatus(runtime.session);
@@ -3502,10 +3535,10 @@ export class PiSessionService {
3502
3535
  catch (error) {
3503
3536
  if (notificationGeneration !== undefined) {
3504
3537
  if (notificationOwnership === "registered") {
3505
- this.publishNotificationMutations(this.notificationStore.clearSession(runtime.session.sessionId, "initialization-failed"));
3538
+ this.publishNotificationMutations(notificationStore.clearSession(runtime.session.sessionId, "initialization-failed"));
3506
3539
  }
3507
3540
  else if (notificationOwnership === "replacement") {
3508
- this.publishNotificationMutations(this.notificationStore.abortReplacement(notificationGeneration));
3541
+ this.publishNotificationMutations(notificationStore.abortReplacement(notificationGeneration));
3509
3542
  }
3510
3543
  }
3511
3544
  active.unsubscribe();
@@ -3538,16 +3571,18 @@ export class PiSessionService {
3538
3571
  }
3539
3572
  sessionUiContext(session, generation) {
3540
3573
  const baseUiContext = session.extensionRunner.getUIContext();
3574
+ const eventScope = this.eventScopeForSession(session);
3575
+ const notificationStore = this.notificationsForScope(eventScope);
3541
3576
  const notify = (message, type) => {
3542
3577
  if (generation === undefined) {
3543
3578
  this.events.publish(session.sessionId, {
3544
3579
  type: "command.output",
3545
3580
  level: type === "error" ? "error" : "info",
3546
3581
  message,
3547
- });
3582
+ }, eventScope);
3548
3583
  return;
3549
3584
  }
3550
- const added = this.notificationStore.addNotification(generation, message, type);
3585
+ const added = notificationStore.addNotification(generation, message, type);
3551
3586
  this.publishNotificationMutations(added.mutations);
3552
3587
  if (added.notification === undefined)
3553
3588
  return;
@@ -3556,7 +3591,7 @@ export class PiSessionService {
3556
3591
  level: type === "error" ? "error" : "info",
3557
3592
  message,
3558
3593
  notificationId: added.notification.id,
3559
- });
3594
+ }, eventScope);
3560
3595
  };
3561
3596
  // PI WEB owns the browser-facing dialog, notification, and text-formatting
3562
3597
  // boundaries: the three dialog primitives park daemon-held Promises that
@@ -3585,35 +3620,32 @@ export class PiSessionService {
3585
3620
  }
3586
3621
  publishNotificationMutations(mutations) {
3587
3622
  for (const mutation of mutations) {
3588
- const scopes = new Set(this.activeSessionsForId(mutation.sessionId).map((active) => active.eventScope));
3589
- if (scopes.size === 0)
3590
- scopes.add(NORMAL_SESSION_EVENT_SCOPE);
3591
- for (const scope of scopes) {
3592
- this.events.publish(mutation.sessionId, mutation.inboxEvent, scope);
3593
- this.events.publishGlobal(mutation.summaryEvent, scope);
3594
- }
3623
+ const scope = mutation.eventScope ?? NORMAL_SESSION_EVENT_SCOPE;
3624
+ this.events.publish(mutation.sessionId, mutation.inboxEvent, scope);
3625
+ this.events.publishGlobal(mutation.summaryEvent, scope);
3595
3626
  }
3596
3627
  }
3597
3628
  async prepareUnreadRuntimeRebind(previous, next) {
3598
3629
  const previousCwd = canonicalizeStoredCwd(previous.sessionManager.getCwd());
3599
- this.unreadStore.forgetActivity(previous.sessionId, previousCwd);
3630
+ const store = this.unreadForScope(this.eventScopeForSession(previous));
3631
+ store.forgetActivity(previous.sessionId, previousCwd);
3600
3632
  const nextCwd = canonicalizeStoredCwd(next.sessionManager.getCwd());
3601
3633
  if (previous.sessionId === next.sessionId && cwdPathsEqual(previousCwd, nextCwd))
3602
3634
  return;
3603
- await this.publishUnreadMutations(this.unreadStore.forgetSession(previous.sessionId, previousCwd));
3635
+ await this.publishUnreadMutations(store.forgetSession(previous.sessionId, previousCwd));
3604
3636
  }
3605
3637
  forgetUnreadActivity(session) {
3606
- this.unreadStore.forgetActivity(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()));
3638
+ this.unreadForScope(this.eventScopeForSession(session)).forgetActivity(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()));
3607
3639
  }
3608
- async forgetUnreadSessions(identities) {
3640
+ async forgetUnreadSessions(identities, scope = NORMAL_SESSION_EVENT_SCOPE) {
3609
3641
  const mutations = [];
3610
3642
  for (const identity of identities) {
3611
- mutations.push(...this.unreadStore.forgetSession(identity.sessionId, canonicalizeStoredCwd(identity.cwd)));
3643
+ mutations.push(...this.unreadForScope(scope).forgetSession(identity.sessionId, canonicalizeStoredCwd(identity.cwd)));
3612
3644
  }
3613
3645
  await this.publishUnreadMutations(mutations);
3614
3646
  }
3615
3647
  observeUnreadActivityState(session) {
3616
- const mutations = this.unreadStore.observeActivityState(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()), this.hasActiveWork(session));
3648
+ const mutations = this.unreadForScope(this.eventScopeForSession(session)).observeActivityState(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()), this.hasActiveWork(session));
3617
3649
  if (mutations.length === 0)
3618
3650
  return;
3619
3651
  void this.publishUnreadMutations(mutations).catch(() => undefined);
@@ -3622,11 +3654,7 @@ export class PiSessionService {
3622
3654
  if (mutations.length > 0) {
3623
3655
  const scopes = new Set();
3624
3656
  for (const mutation of mutations) {
3625
- const activeScopes = this.activeSessionsForId(mutation.event.sessionId).map((active) => active.eventScope);
3626
- if (activeScopes.length === 0)
3627
- scopes.add(NORMAL_SESSION_EVENT_SCOPE);
3628
- else
3629
- activeScopes.forEach((scope) => scopes.add(scope));
3657
+ scopes.add(mutation.eventScope ?? NORMAL_SESSION_EVENT_SCOPE);
3630
3658
  }
3631
3659
  scopes.forEach((scope) => this.onUnreadChanged?.(scope));
3632
3660
  }
@@ -3665,8 +3693,10 @@ export class PiSessionService {
3665
3693
  let publishedCount = 0;
3666
3694
  try {
3667
3695
  await this.unreadStore.flush();
3696
+ for (const store of this.scopedUnreadStores.values())
3697
+ await store.flush();
3668
3698
  for (const mutation of batch) {
3669
- this.events.publishGlobal(mutation.event);
3699
+ this.events.publishGlobal(mutation.event, mutation.eventScope ?? NORMAL_SESSION_EVENT_SCOPE);
3670
3700
  publishedCount += 1;
3671
3701
  }
3672
3702
  }
@@ -4001,19 +4031,6 @@ async function sessionFileHeaderMatches(sessionFile, expected) {
4001
4031
  return true;
4002
4032
  return header.parentSession !== undefined && sessionPathsEqual(header.parentSession, expected.parentSessionFile);
4003
4033
  }
4004
- async function clearParentSession(sessionFile) {
4005
- const content = await readFile(sessionFile, "utf8");
4006
- const newlineIndex = content.indexOf("\n");
4007
- const firstLine = newlineIndex === -1 ? content : content.slice(0, newlineIndex);
4008
- const rest = newlineIndex === -1 ? "" : content.slice(newlineIndex);
4009
- const header = JSON.parse(firstLine);
4010
- if (!isRecord(header) || header["type"] !== "session")
4011
- throw new Error("Invalid session file header");
4012
- if (header["parentSession"] === undefined)
4013
- return;
4014
- delete header["parentSession"];
4015
- await writeFile(sessionFile, `${JSON.stringify(header)}${rest}`, "utf8");
4016
- }
4017
4034
  function clearParentSessionHeader(sessionManager) {
4018
4035
  const header = sessionManager.getHeader?.();
4019
4036
  if (header !== undefined && header !== null)