@chainingintention/pi-web-cn 1.202609.1 → 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 +132 -154
  18. package/dist/server/sessions/piSessionService.js.map +1 -1
  19. package/dist/server/sessions/relaySkill.js +119 -0
  20. package/dist/server/sessions/relaySkill.js.map +1 -0
  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 +10 -8
  34. package/skills/relay-runner/SKILL.md +245 -0
@@ -1,7 +1,6 @@
1
- import { constants, realpathSync, statSync } from "node:fs";
2
- import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { realpathSync, statSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
4
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
6
5
  import { projectBrowserMessage } from "../browserMessageProjection.js";
7
6
  import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
@@ -23,7 +22,7 @@ import { SessionActivityCoordinator } from "./sessionActivityCoordinator.js";
23
22
  import { searchSessionContent } from "./sessionContentSearch.js";
24
23
  import { createLocalOnlyModelRuntime } from "./authService.js";
25
24
  import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
26
- import { readSessionHeaderSummary } from "./sessionFileHeader.js";
25
+ import { clearSessionFileParent, readSessionHeaderSummary } from "./sessionFileHeader.js";
27
26
  import { workbenchAccessHandle } from "../managementEmbed.js";
28
27
  import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS, piWebDataDir } from "../../config.js";
29
28
  import { createScopedSettingsManager, resolveSettingsScopeDirectory } from "./projectSettingsScope.js";
@@ -39,6 +38,7 @@ import { createManagedAgentToolOptions, createManagedPythonToolDefinition } from
39
38
  import { PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR, managementAgentToolNames, withRuntimeCreationEnvironment, writeManagementPermissionSystemPolicy } from "./managementPermissionSystem.js";
40
39
  import { SessionNotificationStore } from "./sessionNotificationStore.js";
41
40
  import { plainTextTheme } from "./plainTextTheme.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";
@@ -178,8 +178,6 @@ function parsePromptStreamingBehavior(value) {
178
178
  throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
179
179
  }
180
180
  const GLOBAL_AGENT_CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
181
- const MANAGED_RELAY_SKILL_DIRECTORY = join(".pi", "skills", "relay");
182
- const BUNDLED_RELAY_SKILL_PATH = fileURLToPath(new URL("../../../skills/relay/SKILL.md", import.meta.url));
183
181
  export function filterManagedGlobalContextFiles(cwd, agentDir, base) {
184
182
  return {
185
183
  agentsFiles: base.agentsFiles.filter((file) => {
@@ -202,29 +200,10 @@ export function filterManagedProjectSkills(cwd, base) {
202
200
  diagnostics: filterManagedSkillDiagnostics(base.diagnostics, pathAllowed),
203
201
  };
204
202
  }
205
- export async function ensureManagedRelaySkill(cwd, bundledSkillPath = BUNDLED_RELAY_SKILL_PATH) {
206
- const projectRoot = realpathSync(cwd);
207
- const piDirectory = resolve(projectRoot, ".pi");
208
- const skillsDirectory = join(piDirectory, "skills");
209
- const skillDirectory = resolve(projectRoot, MANAGED_RELAY_SKILL_DIRECTORY);
210
- const skillPath = join(skillDirectory, "SKILL.md");
211
- for (const directory of [piDirectory, skillsDirectory, skillDirectory]) {
212
- await ensureManagedSkillDirectory(projectRoot, directory);
213
- }
214
- try {
215
- await copyFile(bundledSkillPath, skillPath, constants.COPYFILE_EXCL);
216
- }
217
- catch (error) {
218
- if (!isNodeError(error, "EEXIST"))
219
- throw error;
220
- }
221
- assertManagedSkillPath(projectRoot, skillPath, "file");
222
- return skillPath;
223
- }
224
203
  export function filterManagedWorkbenchSkills(cwd, base, receipt) {
225
204
  const projectRoot = realpathSync(cwd);
226
205
  const allowedDirectories = new Set([
227
- resolve(projectRoot, MANAGED_RELAY_SKILL_DIRECTORY),
206
+ ...MANAGED_RELAY_SKILL_NAMES.map((name) => resolve(projectRoot, ".pi", "skills", name)),
228
207
  ...receipt.skills.map((skill) => resolve(projectRoot, ".pi", "skills", skill.directory)),
229
208
  ]);
230
209
  const pathAllowed = (path) => [...allowedDirectories].some((directory) => pathInsideRealDirectory(directory, path));
@@ -245,27 +224,6 @@ function pathInsideRealDirectory(directory, path) {
245
224
  return false;
246
225
  }
247
226
  }
248
- async function ensureManagedSkillDirectory(projectRoot, path) {
249
- try {
250
- await mkdir(path);
251
- }
252
- catch (error) {
253
- if (!isNodeError(error, "EEXIST"))
254
- throw error;
255
- }
256
- assertManagedSkillPath(projectRoot, path, "directory");
257
- }
258
- function assertManagedSkillPath(projectRoot, path, expectedType) {
259
- const realPath = realpathSync(path);
260
- const stats = statSync(realPath);
261
- const hasExpectedType = expectedType === "directory" ? stats.isDirectory() : stats.isFile();
262
- if (!pathInsideOrEqual(projectRoot, realPath) || !hasExpectedType) {
263
- throw new Error(`Managed skill path is invalid: ${path}`);
264
- }
265
- }
266
- function isNodeError(error, code) {
267
- return error instanceof Error && "code" in error && error.code === code;
268
- }
269
227
  function pathInsideOrEqual(parent, child) {
270
228
  const normalizedParent = resolve(parent);
271
229
  const normalizedChild = resolve(child);
@@ -433,13 +391,16 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
433
391
  globalAgentDir: agentDir,
434
392
  mode: "normal",
435
393
  });
436
- const resourceLoaderOptions = piWebResourceLoaderOptions(appendSystemPromptSections);
394
+ const resourceLoaderOptions = {
395
+ ...piWebResourceLoaderOptions(appendSystemPromptSections),
396
+ skillsOverride: (base) => deduplicateBundledRelaySkills(cwd, base),
397
+ };
437
398
  const services = await createAgentSessionServices({
438
399
  cwd,
439
400
  agentDir,
440
401
  modelRuntime: scopedModelRuntime,
441
402
  settingsManager,
442
- ...(resourceLoaderOptions === undefined ? {} : { resourceLoaderOptions }),
403
+ resourceLoaderOptions,
443
404
  });
444
405
  const modelOptions = await resolveSessionModelOptions({
445
406
  services,
@@ -463,7 +424,7 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
463
424
  }
464
425
  function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd) {
465
426
  return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled = true }) => {
466
- await ensureManagedRelaySkill(cwd);
427
+ await ensureManagedRelaySkills(cwd);
467
428
  const auditIdentity = await resolveManagementAuditIdentity(cwd, managementContext, managementProjectIdForCwd, logger);
468
429
  const accessHandle = workbenchAccessHandle(managementContext);
469
430
  const state = workbench === undefined ? undefined : workbench.accessStates.require(accessHandle);
@@ -641,6 +602,9 @@ export class PiSessionService {
641
602
  * child that works again (and stops again) notifies the parent each time.
642
603
  */
643
604
  this.subsessionNotifyArmed = new Map();
605
+ this.scopedNotificationStores = new Map();
606
+ this.scopedUnreadStores = new Map();
607
+ this.sessionEventScopes = new WeakMap();
644
608
  this.notificationGenerationBySession = new WeakMap();
645
609
  this.idleSessionFileResolutions = new WeakMap();
646
610
  /** The parked extension Promise resolvers behind the store's open dialogs. */
@@ -736,7 +700,8 @@ export class PiSessionService {
736
700
  async cleanupPreview(request) {
737
701
  return previewResponseFromPlan(await this.cleanupPlan(request));
738
702
  }
739
- async cleanup(request) {
703
+ async cleanup(request, context) {
704
+ const scope = eventScopeFromManagementContext(context);
740
705
  const plan = await this.cleanupPlan(request);
741
706
  if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
742
707
  throw new Error("Archive store does not support deletion");
@@ -750,24 +715,24 @@ export class PiSessionService {
750
715
  skippedBusySessionIds.add(input.sessionId);
751
716
  continue;
752
717
  }
753
- await this.closeActive(input.sessionId, { kind: "clear", reason: "archive" });
718
+ await this.closeActive(input.sessionId, { kind: "clear", reason: "archive" }, scope);
754
719
  readyArchiveInputs.push(input);
755
720
  }
756
721
  await this.archiveStoreArchiveMany(readyArchiveInputs);
757
722
  archiveInputs.push(...readyArchiveInputs);
758
- await this.forgetUnreadSessions(readyArchiveInputs);
723
+ await this.forgetUnreadSessions(readyArchiveInputs, scope);
759
724
  for (const record of plan.deleteRecords) {
760
725
  if (this.activeSessionHasWork(record.sessionId)) {
761
726
  skippedBusySessionIds.add(record.sessionId);
762
727
  continue;
763
728
  }
764
- await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
729
+ await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" }, scope);
765
730
  readyDeleteRecords.push(record);
766
731
  }
767
732
  await this.ensureArchivedRecordsMoved(readyDeleteRecords);
768
733
  const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
769
734
  deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
770
- await this.forgetUnreadSessions(deleteRecords);
735
+ await this.forgetUnreadSessions(deleteRecords, scope);
771
736
  return summarizeSessionCleanupExecution({
772
737
  archiveInputs,
773
738
  deleteRecords,
@@ -806,6 +771,8 @@ export class PiSessionService {
806
771
  this.subsessionHydratedParents.clear();
807
772
  this.subsessionNotifyArmed.clear();
808
773
  this.notificationStore.clearAll("service-dispose");
774
+ for (const store of this.scopedNotificationStores.values())
775
+ store.clearAll("service-dispose");
809
776
  await Promise.all(activeSessions.map(async (active) => {
810
777
  active.unsubscribe();
811
778
  this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
@@ -870,13 +837,13 @@ export class PiSessionService {
870
837
  .map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
871
838
  const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
872
839
  for (const record of archivedForCwd) {
873
- this.publishNotificationMutations(this.notificationStore.clearSession(record.sessionId, "archive-reconcile"));
840
+ this.publishNotificationMutations(this.notificationsForScope(eventScopeFromManagementContext(managementContext)).clearSession(record.sessionId, "archive-reconcile"));
874
841
  }
875
842
  const unarchivedSessions = sessions.filter((session) => !archivedById.has(session.id)).map(clientSessionFromListEntry);
876
843
  const reconcilableSessionIds = this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById);
877
844
  this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds, eventScopeFromManagementContext(managementContext));
878
845
  if (managementContext === undefined) {
879
- await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
846
+ await this.publishUnreadMutations(this.unreadForScope(eventScopeFromManagementContext(managementContext)).reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
880
847
  }
881
848
  const archivedSessions = archivedForCwd
882
849
  .sort(compareArchivedRecords)
@@ -985,7 +952,7 @@ export class PiSessionService {
985
952
  ...(parentSessionFile === undefined ? {} : { parentSessionFile }),
986
953
  cwd: decision.cwd,
987
954
  };
988
- await this.registerVerifiedSubsession(link);
955
+ await this.registerVerifiedSubsession(link, eventScopeFromManagementContext(input.managementContext));
989
956
  this.persistSubsessionLink(link);
990
957
  this.persistSubsessionChildMarker(input.parentSessionId, created.id);
991
958
  await this.prompt(created.id, input.prompt, undefined, undefined, {
@@ -1360,7 +1327,7 @@ export class PiSessionService {
1360
1327
  return undefined;
1361
1328
  return sessionFileMatches(session, link.childSessionFile) ? link : undefined;
1362
1329
  }
1363
- async registerVerifiedSubsession(link) {
1330
+ async registerVerifiedSubsession(link, scope = NORMAL_SESSION_EVENT_SCOPE) {
1364
1331
  const { childSessionId, parentSessionId } = link;
1365
1332
  const previousParentId = this.subsessionParents.get(childSessionId);
1366
1333
  if (previousParentId !== undefined && previousParentId !== parentSessionId) {
@@ -1377,7 +1344,7 @@ export class PiSessionService {
1377
1344
  if (!this.subsessionNotifyArmed.has(childSessionId))
1378
1345
  this.subsessionNotifyArmed.set(childSessionId, false);
1379
1346
  const cwd = this.cwdForVerifiedSubsession(link);
1380
- await this.publishUnreadMutations(this.unreadStore.excludeSession(childSessionId, cwd));
1347
+ await this.publishUnreadMutations(this.unreadForScope(scope).excludeSession(childSessionId, cwd));
1381
1348
  }
1382
1349
  cwdForVerifiedSubsession(link) {
1383
1350
  const activeCwd = this.activeChildForSubsessionLink(link)?.runtime.session.sessionManager.getCwd();
@@ -1440,7 +1407,7 @@ export class PiSessionService {
1440
1407
  const activeParent = this.activeForSessionId(parentSessionId);
1441
1408
  if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
1442
1409
  const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
1443
- 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);
1444
1411
  if (complete)
1445
1412
  this.subsessionHydratedParents.add(hydrationKey);
1446
1413
  return;
@@ -1460,7 +1427,7 @@ export class PiSessionService {
1460
1427
  if (complete)
1461
1428
  this.subsessionHydratedParents.add(hydrationKey);
1462
1429
  }
1463
- async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile) {
1430
+ async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile, scope = NORMAL_SESSION_EVENT_SCOPE) {
1464
1431
  // Parent custom links are the authoritative recovery record: verify the
1465
1432
  // exact live child file/header before tracking. Do not negatively cache a
1466
1433
  // scan while a candidate child is temporarily unavailable.
@@ -1475,7 +1442,7 @@ export class PiSessionService {
1475
1442
  complete = false;
1476
1443
  continue;
1477
1444
  }
1478
- await this.registerVerifiedSubsession(verified);
1445
+ await this.registerVerifiedSubsession(verified, scope);
1479
1446
  }
1480
1447
  return complete;
1481
1448
  }
@@ -1496,7 +1463,7 @@ export class PiSessionService {
1496
1463
  const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
1497
1464
  if (link === undefined)
1498
1465
  return;
1499
- await this.registerVerifiedSubsession(link);
1466
+ await this.registerVerifiedSubsession(link, this.eventScopeForSession(session));
1500
1467
  }
1501
1468
  verifiedSubsessionLinkFromOpenedChild(session) {
1502
1469
  return verifiedTrackedSubsessionLink(this.sessionManager, {
@@ -2034,7 +2001,7 @@ export class PiSessionService {
2034
2001
  const archiveInput = await this.archiveInputForSession(session);
2035
2002
  await this.closeActiveSession(active);
2036
2003
  await this.archiveStore.archive(archiveInput);
2037
- await this.forgetUnreadSessions([archiveInput]);
2004
+ await this.forgetUnreadSessions([archiveInput], active.eventScope);
2038
2005
  });
2039
2006
  }
2040
2007
  async archiveMany(refs, managementContext) {
@@ -2050,7 +2017,7 @@ export class PiSessionService {
2050
2017
  for (const ref of uniqueRefs) {
2051
2018
  const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
2052
2019
  if (archived !== undefined) {
2053
- this.publishNotificationMutations(this.notificationStore.clearSession(archived.sessionId, "archive"));
2020
+ this.publishNotificationMutations(this.notificationsForScope(eventScopeFromManagementContext(managementContext)).clearSession(archived.sessionId, "archive"));
2054
2021
  alreadyArchivedSessionIds.push(archived.sessionId);
2055
2022
  unreadArchivedIdentities.push(archived);
2056
2023
  continue;
@@ -2114,7 +2081,7 @@ export class PiSessionService {
2114
2081
  failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
2115
2082
  }
2116
2083
  });
2117
- await this.forgetUnreadSessions(unreadArchivedIdentities);
2084
+ await this.forgetUnreadSessions(unreadArchivedIdentities, eventScopeFromManagementContext(managementContext));
2118
2085
  return {
2119
2086
  archived: true,
2120
2087
  archivedSessionIds: uniqueStrings(archivedSessionIds),
@@ -2137,13 +2104,13 @@ export class PiSessionService {
2137
2104
  })), `归档 ${sessionDisplayName(session)} 前请先停止当前会话活动`, async () => {
2138
2105
  for (const target of plan.targets) {
2139
2106
  if (target.archived)
2140
- this.publishNotificationMutations(this.notificationStore.clearSession(target.id, "archive"));
2107
+ this.publishNotificationMutations(this.notificationsForScope(eventScopeFromManagementContext(managementContext)).clearSession(target.id, "archive"));
2141
2108
  }
2142
2109
  for (const input of archiveInputs)
2143
2110
  await this.closeActiveInScope(input.sessionId, eventScopeFromManagementContext(managementContext));
2144
2111
  await this.archiveStoreArchiveMany(archiveInputs);
2145
2112
  });
2146
- 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));
2147
2114
  return {
2148
2115
  archived: true,
2149
2116
  sessionIds: archiveInputs.map((input) => input.sessionId),
@@ -2157,7 +2124,7 @@ export class PiSessionService {
2157
2124
  throw new Error("未找到会话");
2158
2125
  await this.closeActiveInScope(archived.sessionId, eventScopeFromManagementContext(managementContext));
2159
2126
  await this.archiveStore.restore(archived.sessionId);
2160
- await this.forgetUnreadSessions([archived]);
2127
+ await this.forgetUnreadSessions([archived], eventScopeFromManagementContext(managementContext));
2161
2128
  }
2162
2129
  async deleteArchived(ref, managementContext) {
2163
2130
  const record = await this.getArchived(ref);
@@ -2169,7 +2136,7 @@ export class PiSessionService {
2169
2136
  if (record.archivePath === undefined)
2170
2137
  await this.ensureArchivedRecordMoved(record);
2171
2138
  await this.archiveStore.deleteArchived(record.sessionId);
2172
- await this.forgetUnreadSessions([record]);
2139
+ await this.forgetUnreadSessions([record], eventScopeFromManagementContext(managementContext));
2173
2140
  }
2174
2141
  async deleteArchivedMany(refs, managementContext) {
2175
2142
  if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
@@ -2216,7 +2183,7 @@ export class PiSessionService {
2216
2183
  failures.push({ sessionId, error: errorMessage(error) });
2217
2184
  }
2218
2185
  const deletedIdSet = new Set(deletedSessionIds);
2219
- await this.forgetUnreadSessions(readyRecords.filter((record) => deletedIdSet.has(record.sessionId)));
2186
+ await this.forgetUnreadSessions(readyRecords.filter((record) => deletedIdSet.has(record.sessionId)), eventScopeFromManagementContext(managementContext));
2220
2187
  return {
2221
2188
  deleted: true,
2222
2189
  deletedSessionIds,
@@ -2241,19 +2208,19 @@ export class PiSessionService {
2241
2208
  await this.closeActiveSession(active, priorGeneration === undefined ? CLEAR_RUNTIME_NOTIFICATIONS : DEFER_RUNTIME_NOTIFICATIONS);
2242
2209
  candidateGeneration = priorGeneration === undefined
2243
2210
  ? undefined
2244
- : this.notificationStore.beginReplacement(priorGeneration, { sessionId, cwd });
2211
+ : this.notificationsForSession(session).beginReplacement(priorGeneration, { sessionId, cwd });
2245
2212
  const reopened = await this.openExistingSession(sessionId, cwd, () => this.sessionManager.open(sessionFile), {
2246
2213
  ...(managementContext === undefined ? {} : { managementContext }),
2247
2214
  ...(candidateGeneration === undefined ? {} : { notificationGeneration: candidateGeneration }),
2248
2215
  });
2249
2216
  if (candidateGeneration !== undefined) {
2250
- this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
2217
+ this.publishNotificationMutations(this.notificationsForSession(session).commitReplacement(candidateGeneration));
2251
2218
  }
2252
2219
  return reopened.runtime.session;
2253
2220
  }
2254
2221
  catch (error) {
2255
2222
  if (candidateGeneration !== undefined) {
2256
- this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration));
2223
+ this.publishNotificationMutations(this.notificationsForSession(session).abortReplacement(candidateGeneration));
2257
2224
  }
2258
2225
  throw error;
2259
2226
  }
@@ -2265,11 +2232,11 @@ export class PiSessionService {
2265
2232
  const sessionFile = session.sessionFile;
2266
2233
  if (sessionFile === undefined || sessionFile === "")
2267
2234
  throw new Error("会话尚未持久化");
2268
- await clearParentSession(sessionFile);
2235
+ clearSessionFileParent(sessionFile);
2269
2236
  this.sessionManager.invalidateSessionFile?.(sessionFile);
2270
2237
  clearParentSessionHeader(session.sessionManager);
2271
2238
  this.unregisterSubsession(session.sessionId);
2272
- await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }]);
2239
+ await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }], this.eventScopeForSession(session));
2273
2240
  }
2274
2241
  async abort(ref, managementContext) {
2275
2242
  const active = this.activeForLookup(ref, managementContext);
@@ -2317,7 +2284,7 @@ export class PiSessionService {
2317
2284
  return;
2318
2285
  }
2319
2286
  if (isPiSessionRef(ref)) {
2320
- 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"));
2321
2288
  }
2322
2289
  }
2323
2290
  async bulkSessionLookupContext(refs) {
@@ -2494,17 +2461,17 @@ export class PiSessionService {
2494
2461
  }
2495
2462
  return [...names];
2496
2463
  }
2497
- async closeActive(sessionId, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
2464
+ async closeActive(sessionId, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS, eventScope = NORMAL_SESSION_EVENT_SCOPE) {
2498
2465
  for (const startup of this.startupSessions.values()) {
2499
- if (startup.session.sessionId === sessionId)
2466
+ if (startup.session.sessionId === sessionId && startup.eventScope === eventScope)
2500
2467
  this.endSessionExtensionDialogs(startup.session);
2501
2468
  }
2502
2469
  const pendingOpens = this.pendingSessionOpenPromises(sessionId);
2503
2470
  if (pendingOpens.length > 0)
2504
2471
  await Promise.allSettled(pendingOpens);
2505
- const activeSessions = this.activeSessionsForId(sessionId);
2472
+ const activeSessions = this.activeSessionsForId(sessionId).filter((active) => active.eventScope === eventScope);
2506
2473
  if (activeSessions.length === 0 && notificationPolicy.kind === "clear") {
2507
- this.publishNotificationMutations(this.notificationStore.clearSession(sessionId, notificationPolicy.reason));
2474
+ this.publishNotificationMutations(this.notificationsForScope(eventScope).clearSession(sessionId, notificationPolicy.reason));
2508
2475
  return;
2509
2476
  }
2510
2477
  await Promise.all(activeSessions.map((active) => this.closeActiveSession(active, notificationPolicy)));
@@ -2515,16 +2482,17 @@ export class PiSessionService {
2515
2482
  await this.closeActiveSession(active, notificationPolicy);
2516
2483
  }
2517
2484
  else if (notificationPolicy.kind === "clear") {
2518
- this.publishNotificationMutations(this.notificationStore.clearSession(sessionId, notificationPolicy.reason));
2485
+ this.publishNotificationMutations(this.notificationsForScope(eventScope).clearSession(sessionId, notificationPolicy.reason));
2519
2486
  }
2520
2487
  }
2521
2488
  async closeActiveSession(active, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
2522
2489
  const sessionId = active.runtime.session.sessionId;
2523
2490
  if (notificationPolicy.kind === "clear") {
2524
2491
  const generation = this.notificationGenerationBySession.get(active.runtime.session);
2492
+ const store = this.notificationsForScope(active.eventScope);
2525
2493
  const mutations = generation === undefined
2526
- ? this.notificationStore.clearSession(sessionId, notificationPolicy.reason)
2527
- : this.notificationStore.clearGeneration(generation, notificationPolicy.reason);
2494
+ ? store.clearSession(sessionId, notificationPolicy.reason)
2495
+ : store.clearGeneration(generation, notificationPolicy.reason);
2528
2496
  this.publishNotificationMutations(mutations);
2529
2497
  }
2530
2498
  this.forgetUnreadActivity(active.runtime.session);
@@ -2757,6 +2725,7 @@ export class PiSessionService {
2757
2725
  }
2758
2726
  }
2759
2727
  bindRuntime(active, session = active.runtime.session) {
2728
+ this.sessionEventScopes.set(session, active.eventScope);
2760
2729
  this.runtimeBySession.set(session, active.runtime);
2761
2730
  active.unsubscribe();
2762
2731
  for (const [sessionId, candidate] of this.active.entries()) {
@@ -3264,6 +3233,9 @@ export class PiSessionService {
3264
3233
  return this.activities.get(activeSessionKey(session.sessionId, this.eventScopeForSession(session)));
3265
3234
  }
3266
3235
  eventScopeForSession(session) {
3236
+ const boundScope = this.sessionEventScopes.get(session);
3237
+ if (boundScope !== undefined)
3238
+ return boundScope;
3267
3239
  for (const active of this.active.values()) {
3268
3240
  if (active.runtime.session === session)
3269
3241
  return active.eventScope;
@@ -3338,41 +3310,60 @@ export class PiSessionService {
3338
3310
  hasQueuedMessageText(session, text) {
3339
3311
  return queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)).some((message) => message.text === text);
3340
3312
  }
3341
- notificationCatalog() {
3342
- 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));
3343
3325
  }
3344
- 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) {
3345
3340
  await this.publishUnreadMutations([]);
3346
- return this.unreadStore.durableCatalogSnapshot();
3341
+ return this.unreadForScope(eventScopeFromManagementContext(context)).durableCatalogSnapshot();
3347
3342
  }
3348
- /** 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. */
3349
3344
  async unreadCatalogForScope(eventScope) {
3350
- const catalog = await this.unreadCatalog();
3351
- if (eventScope === NORMAL_SESSION_EVENT_SCOPE)
3352
- return catalog;
3353
- const visible = new Set([...this.active.values()]
3354
- .filter((active) => active.eventScope === eventScope)
3355
- .map((active) => active.runtime.session.sessionId));
3356
- return { ...catalog, sessions: catalog.sessions.filter((session) => visible.has(session.sessionId)) };
3357
- }
3358
- async acknowledgeUnread(sessionId, request) {
3359
- 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, {
3360
3351
  ...request,
3361
3352
  cwd: canonicalizeStoredCwd(request.cwd),
3362
3353
  });
3363
3354
  await this.publishUnreadMutations(result.mutations);
3364
- return this.unreadStore.durableCatalogSnapshot();
3355
+ return store.durableCatalogSnapshot();
3365
3356
  }
3366
- notificationInbox(ref) {
3367
- 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));
3368
3359
  }
3369
- dismissNotification(ref, request) {
3370
- 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);
3371
3362
  this.publishNotificationMutations(result.mutations);
3372
3363
  return result.snapshot;
3373
3364
  }
3374
- dismissAllNotifications(ref, request) {
3375
- 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);
3376
3367
  this.publishNotificationMutations(result.mutations);
3377
3368
  return result.snapshot;
3378
3369
  }
@@ -3393,20 +3384,20 @@ export class PiSessionService {
3393
3384
  try {
3394
3385
  await session.reload(priorGeneration === undefined ? undefined : {
3395
3386
  beforeSessionStart: () => {
3396
- candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
3387
+ candidateGeneration = this.notificationsForSession(session).beginReplacement(priorGeneration, notificationIdentityForSession(session));
3397
3388
  this.notificationGenerationBySession.set(session, candidateGeneration);
3398
3389
  this.replaceSessionNotificationContext(session, candidateGeneration);
3399
3390
  },
3400
3391
  });
3401
3392
  if (candidateGeneration !== undefined) {
3402
- this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
3393
+ this.publishNotificationMutations(this.notificationsForSession(session).commitReplacement(candidateGeneration));
3403
3394
  }
3404
3395
  this.publishActivity(session, "资源已重新加载", "idle");
3405
3396
  this.publishStatus(session);
3406
3397
  }
3407
3398
  catch (error) {
3408
3399
  if (candidateGeneration !== undefined) {
3409
- this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
3400
+ this.publishNotificationMutations(this.notificationsForSession(session).abortReplacement(candidateGeneration, "candidate"));
3410
3401
  this.notificationGenerationBySession.set(session, candidateGeneration);
3411
3402
  }
3412
3403
  const message = error instanceof Error ? error.message : String(error);
@@ -3468,6 +3459,8 @@ export class PiSessionService {
3468
3459
  }),
3469
3460
  };
3470
3461
  let boundSession = runtime.session;
3462
+ this.sessionEventScopes.set(boundSession, active.eventScope);
3463
+ const notificationStore = this.notificationsForScope(active.eventScope);
3471
3464
  let notificationGeneration = options.notificationGeneration;
3472
3465
  let notificationOwnership = options.notifications === "disabled"
3473
3466
  ? "disabled"
@@ -3476,13 +3469,13 @@ export class PiSessionService {
3476
3469
  : "external";
3477
3470
  if (notificationOwnership === "registered") {
3478
3471
  const notificationIdentity = notificationIdentityForSession(runtime.session);
3479
- const existingCandidate = this.notificationStore.beginReplacementForSession(notificationIdentity.sessionId, notificationIdentity.cwd);
3472
+ const existingCandidate = notificationStore.beginReplacementForSession(notificationIdentity.sessionId, notificationIdentity.cwd);
3480
3473
  if (existingCandidate !== undefined) {
3481
3474
  notificationGeneration = existingCandidate;
3482
3475
  notificationOwnership = "replacement";
3483
3476
  }
3484
3477
  else {
3485
- const registration = this.notificationStore.registerSession(notificationIdentity.sessionId, notificationIdentity.cwd);
3478
+ const registration = notificationStore.registerSession(notificationIdentity.sessionId, notificationIdentity.cwd);
3486
3479
  notificationGeneration = registration.generation;
3487
3480
  this.publishNotificationMutations(registration.mutations);
3488
3481
  }
@@ -3491,7 +3484,7 @@ export class PiSessionService {
3491
3484
  this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
3492
3485
  try {
3493
3486
  if (options.creationProvenance === "tracked-subsession") {
3494
- 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())));
3495
3488
  }
3496
3489
  else {
3497
3490
  await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
@@ -3500,13 +3493,14 @@ export class PiSessionService {
3500
3493
  await this.bindSessionExtensions(runtime.session, notificationGeneration, active);
3501
3494
  this.bindRuntime(active);
3502
3495
  runtime.setRebindSession(async (session) => {
3496
+ this.sessionEventScopes.set(session, active.eventScope);
3503
3497
  const priorGeneration = notificationGeneration;
3504
3498
  let candidateGeneration;
3505
3499
  try {
3506
3500
  await this.prepareUnreadRuntimeRebind(boundSession, session);
3507
3501
  await this.recoverSubsessionTrackingForOpenedSession(session);
3508
3502
  if (priorGeneration !== undefined) {
3509
- candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
3503
+ candidateGeneration = notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
3510
3504
  this.notificationGenerationBySession.set(session, candidateGeneration);
3511
3505
  }
3512
3506
  this.bindRuntime(active, session);
@@ -3517,13 +3511,13 @@ export class PiSessionService {
3517
3511
  boundSession = session;
3518
3512
  await this.bindSessionExtensions(session, candidateGeneration, active);
3519
3513
  if (candidateGeneration !== undefined) {
3520
- this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
3514
+ this.publishNotificationMutations(notificationStore.commitReplacement(candidateGeneration));
3521
3515
  notificationGeneration = candidateGeneration;
3522
3516
  }
3523
3517
  }
3524
3518
  catch (error) {
3525
3519
  if (candidateGeneration !== undefined) {
3526
- this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
3520
+ this.publishNotificationMutations(notificationStore.abortReplacement(candidateGeneration, "candidate"));
3527
3521
  notificationGeneration = candidateGeneration;
3528
3522
  this.notificationGenerationBySession.set(session, candidateGeneration);
3529
3523
  }
@@ -3532,7 +3526,7 @@ export class PiSessionService {
3532
3526
  });
3533
3527
  this.active.set(activeSessionKey(runtime.session.sessionId, active.eventScope), active);
3534
3528
  if (notificationOwnership === "replacement" && notificationGeneration !== undefined) {
3535
- this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
3529
+ this.publishNotificationMutations(notificationStore.commitReplacement(notificationGeneration));
3536
3530
  notificationOwnership = "external";
3537
3531
  }
3538
3532
  this.publishStatus(runtime.session);
@@ -3541,10 +3535,10 @@ export class PiSessionService {
3541
3535
  catch (error) {
3542
3536
  if (notificationGeneration !== undefined) {
3543
3537
  if (notificationOwnership === "registered") {
3544
- this.publishNotificationMutations(this.notificationStore.clearSession(runtime.session.sessionId, "initialization-failed"));
3538
+ this.publishNotificationMutations(notificationStore.clearSession(runtime.session.sessionId, "initialization-failed"));
3545
3539
  }
3546
3540
  else if (notificationOwnership === "replacement") {
3547
- this.publishNotificationMutations(this.notificationStore.abortReplacement(notificationGeneration));
3541
+ this.publishNotificationMutations(notificationStore.abortReplacement(notificationGeneration));
3548
3542
  }
3549
3543
  }
3550
3544
  active.unsubscribe();
@@ -3577,16 +3571,18 @@ export class PiSessionService {
3577
3571
  }
3578
3572
  sessionUiContext(session, generation) {
3579
3573
  const baseUiContext = session.extensionRunner.getUIContext();
3574
+ const eventScope = this.eventScopeForSession(session);
3575
+ const notificationStore = this.notificationsForScope(eventScope);
3580
3576
  const notify = (message, type) => {
3581
3577
  if (generation === undefined) {
3582
3578
  this.events.publish(session.sessionId, {
3583
3579
  type: "command.output",
3584
3580
  level: type === "error" ? "error" : "info",
3585
3581
  message,
3586
- });
3582
+ }, eventScope);
3587
3583
  return;
3588
3584
  }
3589
- const added = this.notificationStore.addNotification(generation, message, type);
3585
+ const added = notificationStore.addNotification(generation, message, type);
3590
3586
  this.publishNotificationMutations(added.mutations);
3591
3587
  if (added.notification === undefined)
3592
3588
  return;
@@ -3595,7 +3591,7 @@ export class PiSessionService {
3595
3591
  level: type === "error" ? "error" : "info",
3596
3592
  message,
3597
3593
  notificationId: added.notification.id,
3598
- });
3594
+ }, eventScope);
3599
3595
  };
3600
3596
  // PI WEB owns the browser-facing dialog, notification, and text-formatting
3601
3597
  // boundaries: the three dialog primitives park daemon-held Promises that
@@ -3624,35 +3620,32 @@ export class PiSessionService {
3624
3620
  }
3625
3621
  publishNotificationMutations(mutations) {
3626
3622
  for (const mutation of mutations) {
3627
- const scopes = new Set(this.activeSessionsForId(mutation.sessionId).map((active) => active.eventScope));
3628
- if (scopes.size === 0)
3629
- scopes.add(NORMAL_SESSION_EVENT_SCOPE);
3630
- for (const scope of scopes) {
3631
- this.events.publish(mutation.sessionId, mutation.inboxEvent, scope);
3632
- this.events.publishGlobal(mutation.summaryEvent, scope);
3633
- }
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);
3634
3626
  }
3635
3627
  }
3636
3628
  async prepareUnreadRuntimeRebind(previous, next) {
3637
3629
  const previousCwd = canonicalizeStoredCwd(previous.sessionManager.getCwd());
3638
- this.unreadStore.forgetActivity(previous.sessionId, previousCwd);
3630
+ const store = this.unreadForScope(this.eventScopeForSession(previous));
3631
+ store.forgetActivity(previous.sessionId, previousCwd);
3639
3632
  const nextCwd = canonicalizeStoredCwd(next.sessionManager.getCwd());
3640
3633
  if (previous.sessionId === next.sessionId && cwdPathsEqual(previousCwd, nextCwd))
3641
3634
  return;
3642
- await this.publishUnreadMutations(this.unreadStore.forgetSession(previous.sessionId, previousCwd));
3635
+ await this.publishUnreadMutations(store.forgetSession(previous.sessionId, previousCwd));
3643
3636
  }
3644
3637
  forgetUnreadActivity(session) {
3645
- this.unreadStore.forgetActivity(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()));
3638
+ this.unreadForScope(this.eventScopeForSession(session)).forgetActivity(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()));
3646
3639
  }
3647
- async forgetUnreadSessions(identities) {
3640
+ async forgetUnreadSessions(identities, scope = NORMAL_SESSION_EVENT_SCOPE) {
3648
3641
  const mutations = [];
3649
3642
  for (const identity of identities) {
3650
- mutations.push(...this.unreadStore.forgetSession(identity.sessionId, canonicalizeStoredCwd(identity.cwd)));
3643
+ mutations.push(...this.unreadForScope(scope).forgetSession(identity.sessionId, canonicalizeStoredCwd(identity.cwd)));
3651
3644
  }
3652
3645
  await this.publishUnreadMutations(mutations);
3653
3646
  }
3654
3647
  observeUnreadActivityState(session) {
3655
- 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));
3656
3649
  if (mutations.length === 0)
3657
3650
  return;
3658
3651
  void this.publishUnreadMutations(mutations).catch(() => undefined);
@@ -3661,11 +3654,7 @@ export class PiSessionService {
3661
3654
  if (mutations.length > 0) {
3662
3655
  const scopes = new Set();
3663
3656
  for (const mutation of mutations) {
3664
- const activeScopes = this.activeSessionsForId(mutation.event.sessionId).map((active) => active.eventScope);
3665
- if (activeScopes.length === 0)
3666
- scopes.add(NORMAL_SESSION_EVENT_SCOPE);
3667
- else
3668
- activeScopes.forEach((scope) => scopes.add(scope));
3657
+ scopes.add(mutation.eventScope ?? NORMAL_SESSION_EVENT_SCOPE);
3669
3658
  }
3670
3659
  scopes.forEach((scope) => this.onUnreadChanged?.(scope));
3671
3660
  }
@@ -3704,8 +3693,10 @@ export class PiSessionService {
3704
3693
  let publishedCount = 0;
3705
3694
  try {
3706
3695
  await this.unreadStore.flush();
3696
+ for (const store of this.scopedUnreadStores.values())
3697
+ await store.flush();
3707
3698
  for (const mutation of batch) {
3708
- this.events.publishGlobal(mutation.event);
3699
+ this.events.publishGlobal(mutation.event, mutation.eventScope ?? NORMAL_SESSION_EVENT_SCOPE);
3709
3700
  publishedCount += 1;
3710
3701
  }
3711
3702
  }
@@ -4040,19 +4031,6 @@ async function sessionFileHeaderMatches(sessionFile, expected) {
4040
4031
  return true;
4041
4032
  return header.parentSession !== undefined && sessionPathsEqual(header.parentSession, expected.parentSessionFile);
4042
4033
  }
4043
- async function clearParentSession(sessionFile) {
4044
- const content = await readFile(sessionFile, "utf8");
4045
- const newlineIndex = content.indexOf("\n");
4046
- const firstLine = newlineIndex === -1 ? content : content.slice(0, newlineIndex);
4047
- const rest = newlineIndex === -1 ? "" : content.slice(newlineIndex);
4048
- const header = JSON.parse(firstLine);
4049
- if (!isRecord(header) || header["type"] !== "session")
4050
- throw new Error("Invalid session file header");
4051
- if (header["parentSession"] === undefined)
4052
- return;
4053
- delete header["parentSession"];
4054
- await writeFile(sessionFile, `${JSON.stringify(header)}${rest}`, "utf8");
4055
- }
4056
4034
  function clearParentSessionHeader(sessionManager) {
4057
4035
  const header = sessionManager.getHeader?.();
4058
4036
  if (header !== undefined && header !== null)