@chainingintention/pi-web-cn 1.202607.2 → 1.202607.3-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/client/assets/{CodeViewer-Cyy3_LEk.js → CodeViewer-B9FasHbW.js} +1 -1
  2. package/dist/client/assets/{TerminalPanel-CNsIQZbo.js → TerminalPanel-BSE17h19.js} +1 -1
  3. package/dist/client/assets/{UnifiedDiffViewer-BprnYHmM.js → UnifiedDiffViewer-ChTX4eDR.js} +1 -1
  4. package/dist/client/assets/{index-DB_vEZT7.js → index-sP1tZcYI.js} +194 -194
  5. package/dist/client/index.html +1 -1
  6. package/dist/server/activity/workspaceActivityRoutes.js +7 -1
  7. package/dist/server/activity/workspaceActivityRoutes.js.map +1 -1
  8. package/dist/server/activity/workspaceActivityService.js +70 -46
  9. package/dist/server/activity/workspaceActivityService.js.map +1 -1
  10. package/dist/server/realtime/sessionEventHub.js +24 -13
  11. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  12. package/dist/server/realtime/sessionEventScope.js +41 -0
  13. package/dist/server/realtime/sessionEventScope.js.map +1 -0
  14. package/dist/server/sessiond.js +12 -4
  15. package/dist/server/sessiond.js.map +1 -1
  16. package/dist/server/sessions/authRoutes.js +15 -7
  17. package/dist/server/sessions/authRoutes.js.map +1 -1
  18. package/dist/server/sessions/authService.js +2 -1
  19. package/dist/server/sessions/authService.js.map +1 -1
  20. package/dist/server/sessions/piSessionService.js +162 -118
  21. package/dist/server/sessions/piSessionService.js.map +1 -1
  22. package/dist/server/sessions/sessionCommandService.js +27 -27
  23. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  24. package/dist/server/sessions/sessionRoutes.js +22 -18
  25. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  26. package/dist/server/terminals/terminalRoutes.js +3 -3
  27. package/dist/server/terminals/terminalRoutes.js.map +1 -1
  28. package/dist/server/terminals/terminalService.js +26 -13
  29. package/dist/server/terminals/terminalService.js.map +1 -1
  30. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { open, readFile, writeFile } from "node:fs/promises";
2
2
  import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
3
3
  import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
4
4
  import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
5
+ import { authScopeFromEventScope, eventScopeFromManagementContext, managementContextKey, NORMAL_SESSION_EVENT_SCOPE } from "../realtime/sessionEventScope.js";
5
6
  import { BUILTIN_COMMANDS } from "./builtinCommands.js";
6
7
  import { SessionCommandService } from "./sessionCommandService.js";
7
8
  import { SessionArchiveStore } from "./sessionArchiveStore.js";
@@ -29,8 +30,8 @@ function spawnTargetError(decision) {
29
30
  return new Error("派生会话不在已注册项目中");
30
31
  return new Error(`cwd 必须是此项目的工作区。允许的路径:${decision.allowedCwds.join(", ")}`);
31
32
  }
32
- function authLossWarningKey(sessionId, provider, modelId) {
33
- return `${sessionId}:${provider}/${modelId}`;
33
+ function authLossWarningKey(sessionId, eventScope, provider, modelId) {
34
+ return `${eventScope}:${sessionId}:${provider}/${modelId}`;
34
35
  }
35
36
  function sessionIdFromLookup(ref) {
36
37
  return typeof ref === "string" ? ref : ref.id;
@@ -41,15 +42,8 @@ function isPiSessionRef(ref) {
41
42
  function lookupMatchesActiveSession(ref, active) {
42
43
  return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
43
44
  }
44
- function managementContextKey(context) {
45
- if (context === undefined)
46
- return undefined;
47
- return JSON.stringify({
48
- userId: context.user.id,
49
- rootUserId: context.user.rootUserId,
50
- projects: context.projects.map((project) => project.id).sort(),
51
- sandboxEnv: context.sandbox?.env ?? {},
52
- });
45
+ function activeSessionKey(sessionId, eventScope) {
46
+ return `${eventScope}\0${sessionId}`;
53
47
  }
54
48
  function requirePromptText(value) {
55
49
  if (typeof value !== "string")
@@ -107,10 +101,12 @@ function createRuntimeWithContextAndOneShotInitialModel(createRuntime, managemen
107
101
  });
108
102
  };
109
103
  }
110
- function createDefaultRuntimeFactory(authStorage, modelRegistry, spawn, subsessions) {
104
+ function createDefaultRuntimeFactory(modelRegistry, managementModelRegistry, spawn, subsessions) {
111
105
  return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel }) => {
106
+ const scopedModelRegistry = managementContext === undefined ? modelRegistry : managementModelRegistry;
107
+ const authStorage = scopedModelRegistry.authStorage;
112
108
  if (managementContext !== undefined) {
113
- return createManagementRuntimeFactory(authStorage, modelRegistry, spawn, subsessions, managementContext)({
109
+ return createManagementRuntimeFactory(authStorage, scopedModelRegistry, spawn, subsessions, managementContext)({
114
110
  cwd,
115
111
  agentDir,
116
112
  sessionManager,
@@ -118,7 +114,7 @@ function createDefaultRuntimeFactory(authStorage, modelRegistry, spawn, subsessi
118
114
  ...(initialModel === undefined ? {} : { initialModel }),
119
115
  });
120
116
  }
121
- const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
117
+ const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry: scopedModelRegistry });
122
118
  const customTools = [
123
119
  ...managedAgentToolDefinitions(cwd),
124
120
  ...(spawn === undefined ? [] : [defineTool(createSpawnSessionToolDefinition(cwd, { spawn }))]),
@@ -226,13 +222,14 @@ export class PiSessionService {
226
222
  this.agentDir = deps.agentDir ?? getAgentDir();
227
223
  this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
228
224
  this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
225
+ this.managementModelRegistry = deps.managementModelRegistry ?? this.modelRegistry;
229
226
  this.spawnTargets = deps.spawnTargets;
230
227
  this.logger = deps.logger ?? noopLogger;
231
228
  this.now = deps.now ?? (() => new Date());
232
229
  // Subsessions are a beta capability gated behind their own flag, and they
233
230
  // also require the spawn capability (they share its project-scope resolver).
234
231
  const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
235
- this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
232
+ this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry, this.managementModelRegistry, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
236
233
  spawn: (input) => this.spawnSubsession(input),
237
234
  list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
238
235
  check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
@@ -241,7 +238,7 @@ export class PiSessionService {
241
238
  this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
242
239
  this.workspaceActivity = deps.workspaceActivity;
243
240
  this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
244
- this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, { echoUserMessage: false }), events, {
241
+ this.commandService = new SessionCommandService((sessionId, eventScope) => this.getActiveForCommand(sessionId, eventScope), (sessionId, text, eventScope) => this.submitCommandPrompt(sessionId, text, eventScope), events, {
245
242
  onCompactionStart: (session) => {
246
243
  this.publishActivity(session, "compacting", "active");
247
244
  this.publishStatus(session);
@@ -311,12 +308,12 @@ export class PiSessionService {
311
308
  this.subsessionNotifyArmed.clear();
312
309
  await Promise.all(activeSessions.map(async (active) => {
313
310
  active.unsubscribe();
314
- this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
311
+ this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
315
312
  await active.runtime.session.abort();
316
313
  await active.runtime.dispose();
317
314
  }));
318
315
  }
319
- async list(cwd) {
316
+ async list(cwd, managementContext) {
320
317
  const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
321
318
  const sessionsById = new Map(sessions.map((session) => [session.id, session]));
322
319
  const archivedForCwd = await Promise.all(archivedRecords
@@ -324,7 +321,7 @@ export class PiSessionService {
324
321
  .map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
325
322
  const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
326
323
  const unarchivedSessions = sessions.filter((session) => !archivedById.has(session.id)).map(clientSessionFromListEntry);
327
- this.workspaceActivity?.reconcileSessionActivity(cwd, this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById));
324
+ this.workspaceActivity?.reconcileSessionActivity(cwd, this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById), eventScopeFromManagementContext(managementContext));
328
325
  const archivedSessions = archivedForCwd
329
326
  .sort(compareArchivedRecords)
330
327
  .map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
@@ -351,7 +348,7 @@ export class PiSessionService {
351
348
  };
352
349
  // Broadcast so other clients (and the spawning agent's UI) can add the new
353
350
  // session to their list without a manual reload.
354
- this.events.publishGlobal({ type: "session.created", session: created });
351
+ this.events.publishGlobal({ type: "session.created", session: created }, active.eventScope);
355
352
  return created;
356
353
  }
357
354
  /**
@@ -450,16 +447,10 @@ export class PiSessionService {
450
447
  return parentSessionFile === undefined || trackedLinkParentFileMatches(link, parentSessionFile);
451
448
  }
452
449
  activeChildForSubsessionLink(link) {
453
- const active = this.active.get(link.childSessionId);
454
- if (active === undefined)
455
- return undefined;
456
- return activeSessionFileMatches(active, link.childSessionFile) ? active : undefined;
450
+ return this.activeSessionsForId(link.childSessionId).find((active) => activeSessionFileMatches(active, link.childSessionFile));
457
451
  }
458
452
  activeParentForSubsessionLink(link) {
459
- const active = this.active.get(link.parentSessionId);
460
- if (active === undefined)
461
- return undefined;
462
- return activeSessionFileMatches(active, link.parentSessionFile) ? active : undefined;
453
+ return this.activeSessionsForId(link.parentSessionId).find((active) => activeSessionFileMatches(active, link.parentSessionFile));
463
454
  }
464
455
  subsessionLinkForActiveChild(session) {
465
456
  const childId = session.sessionId;
@@ -512,7 +503,7 @@ export class PiSessionService {
512
503
  }
513
504
  }
514
505
  persistSubsessionChildMarker(parentSessionId, childSessionId) {
515
- const child = this.active.get(childSessionId)?.runtime.session;
506
+ const child = this.activeForSessionId(childSessionId)?.runtime.session;
516
507
  if (child === undefined)
517
508
  return;
518
509
  if (child.sessionManager.appendCustomEntry === undefined)
@@ -528,7 +519,7 @@ export class PiSessionService {
528
519
  const hydrationKey = subsessionHydratedParentKey(parentSessionId, parentSessionFile);
529
520
  if (this.subsessionHydratedParents.has(hydrationKey))
530
521
  return;
531
- const activeParent = this.active.get(parentSessionId);
522
+ const activeParent = this.activeForSessionId(parentSessionId);
532
523
  if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
533
524
  const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
534
525
  await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
@@ -672,7 +663,7 @@ export class PiSessionService {
672
663
  subsessionStatus(session) {
673
664
  if (this.hasActiveWork(session))
674
665
  return "working";
675
- if (this.activities.get(session.sessionId)?.phase === "error")
666
+ if (this.activityForSession(session)?.phase === "error")
676
667
  return "error";
677
668
  return "idle";
678
669
  }
@@ -694,7 +685,7 @@ export class PiSessionService {
694
685
  if (this.subsessionNotifyArmed.get(childId) !== true)
695
686
  return;
696
687
  this.subsessionNotifyArmed.set(childId, false);
697
- const status = this.activities.get(childId)?.phase === "error" ? "error" : "idle";
688
+ const status = this.activityForSession(session)?.phase === "error" ? "error" : "idle";
698
689
  const finalText = finalAssistantText(historyMessages(session));
699
690
  const preview = finalText === "" ? "(no output)" : truncateForNotification(finalText);
700
691
  const text = `Subsession ${childId} stopped working (status: ${status}). Latest output:\n\n${preview}\n\nUse check_subsession with sessionId "${childId}" for its status and latest output, or read_subsession to look through its full transcript.`;
@@ -844,13 +835,14 @@ export class PiSessionService {
844
835
  }
845
836
  submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
846
837
  this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
838
+ const eventScope = this.eventScopeForSession(session);
847
839
  if (behavior === undefined && echoUserMessage)
848
- this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) });
840
+ this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) }, eventScope);
849
841
  const promptOptions = buildPromptOptions(behavior, images);
850
842
  const promptPromise = session.prompt(text, promptOptions).catch((error) => {
851
843
  const message = error instanceof Error ? error.message : String(error);
852
844
  this.publishActivity(session, "error", "error", message);
853
- this.events.publish(session.sessionId, { type: "session.error", message });
845
+ this.events.publish(session.sessionId, { type: "session.error", message }, eventScope);
854
846
  });
855
847
  void promptPromise;
856
848
  return promptPromise;
@@ -862,12 +854,12 @@ export class PiSessionService {
862
854
  this.publishActivity(session, "message queued during compaction", "active");
863
855
  this.publishStatus(session);
864
856
  }
865
- async saveAttachments(ref, attachments, folder) {
857
+ async saveAttachments(ref, attachments, folder, managementContext) {
866
858
  const parsed = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false, allowFileAttachments: true });
867
859
  if (parsed.length === 0)
868
860
  return [];
869
861
  await this.assertWritable(ref);
870
- const active = await this.getActive(ref);
862
+ const active = await this.getActive(ref, managementContext);
871
863
  return saveAttachmentsToWorkspace(active.runtime.cwd, parsed, folder === undefined ? {} : { folder });
872
864
  }
873
865
  async shell(ref, text, managementContext) {
@@ -880,10 +872,11 @@ export class PiSessionService {
880
872
  throw new Error("用法:!<shell command>");
881
873
  if (session.isBashRunning)
882
874
  throw new Error("已有 bash 命令正在运行");
875
+ const eventScope = active.eventScope;
883
876
  this.publishActivity(session, "running bash", "active", command);
884
- this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded });
877
+ this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded }, eventScope);
885
878
  void session.executeBash(command, (chunk) => {
886
- this.events.publish(session.sessionId, { type: "shell.chunk", chunk });
879
+ this.events.publish(session.sessionId, { type: "shell.chunk", chunk }, eventScope);
887
880
  this.publishActivity(session, "running bash", "active", command);
888
881
  this.publishStatus(session);
889
882
  }, { excludeFromContext: isExcluded }).then((result) => {
@@ -894,13 +887,13 @@ export class PiSessionService {
894
887
  cancelled: result.cancelled,
895
888
  truncated: result.truncated,
896
889
  ...(result.fullOutputPath === undefined ? {} : { fullOutputPath: result.fullOutputPath }),
897
- });
890
+ }, eventScope);
898
891
  this.publishActivity(session, "bash complete", result.exitCode === 0 ? "idle" : "error", command);
899
892
  this.publishStatus(session);
900
893
  }).catch((error) => {
901
894
  const message = error instanceof Error ? error.message : String(error);
902
- this.events.publish(session.sessionId, { type: "shell.end", output: message, isError: true });
903
- this.events.publish(session.sessionId, { type: "session.error", message });
895
+ this.events.publish(session.sessionId, { type: "shell.end", output: message, isError: true }, eventScope);
896
+ this.events.publish(session.sessionId, { type: "session.error", message }, eventScope);
904
897
  this.publishActivity(session, "bash failed", "error", message);
905
898
  this.publishStatus(session);
906
899
  });
@@ -908,22 +901,23 @@ export class PiSessionService {
908
901
  async runCommand(ref, text, managementContext) {
909
902
  await this.assertWritable(ref);
910
903
  const active = await this.getActive(ref, managementContext);
911
- return this.commandService.run(active.runtime.session.sessionId, text);
904
+ return this.commandService.run(active.runtime.session.sessionId, text, active.eventScope);
912
905
  }
913
- async respondToCommand(ref, requestId, value) {
906
+ async respondToCommand(ref, requestId, value, managementContext) {
914
907
  await this.assertWritable(ref);
915
- const active = await this.getActive(ref);
916
- return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
908
+ const active = await this.getActive(ref, managementContext);
909
+ return this.commandService.respond(active.runtime.session.sessionId, requestId, value, active.eventScope);
917
910
  }
918
- async archive(ref) {
919
- const session = await this.getOrOpen(ref);
911
+ async archive(ref, managementContext) {
912
+ const active = await this.getActive(ref, managementContext);
913
+ const session = active.runtime.session;
920
914
  if (this.hasActiveWork(session))
921
915
  throw new Error("归档前请先停止当前会话活动");
922
916
  const archiveInput = await this.archiveInputForSession(session);
923
- await this.closeActive(session.sessionId);
917
+ await this.closeActiveSession(active);
924
918
  await this.archiveStore.archive(archiveInput);
925
919
  }
926
- async archiveMany(refs) {
920
+ async archiveMany(refs, managementContext) {
927
921
  const uniqueRefs = uniqueBulkSessionRefs(refs);
928
922
  const [archivedRecords, sessionContext] = await Promise.all([
929
923
  this.archiveStore.list(),
@@ -938,7 +932,7 @@ export class PiSessionService {
938
932
  alreadyArchivedSessionIds.push(archived.sessionId);
939
933
  continue;
940
934
  }
941
- const active = this.activeForLookup(bulkRefToLookup(ref));
935
+ const active = this.activeForLookup(bulkRefToLookup(ref), managementContext);
942
936
  const listed = findListedSessionForBulkRef(sessionContext, ref);
943
937
  const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
944
938
  if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
@@ -947,10 +941,10 @@ export class PiSessionService {
947
941
  }
948
942
  try {
949
943
  if (listed !== undefined) {
950
- planItems.push({ input: archiveInputFromListEntry(listed) });
944
+ planItems.push({ input: archiveInputFromListEntry(listed), ...(active === undefined ? {} : { active }) });
951
945
  }
952
946
  else if (active !== undefined) {
953
- planItems.push({ input: archiveInputFromActiveSession(active.runtime.session) });
947
+ planItems.push({ input: archiveInputFromActiveSession(active.runtime.session), active });
954
948
  }
955
949
  else {
956
950
  failures.push({ sessionId: ref.id, error: "Session not found" });
@@ -963,7 +957,10 @@ export class PiSessionService {
963
957
  const readyInputs = [];
964
958
  for (const item of planItems) {
965
959
  try {
966
- await this.closeActive(item.input.sessionId);
960
+ if (item.active !== undefined)
961
+ await this.closeActiveSession(item.active);
962
+ else
963
+ await this.closeActiveInScope(item.input.sessionId, eventScopeFromManagementContext(managementContext));
967
964
  readyInputs.push(item.input);
968
965
  }
969
966
  catch (error) {
@@ -986,8 +983,8 @@ export class PiSessionService {
986
983
  generatedAt: new Date().toISOString(),
987
984
  };
988
985
  }
989
- async archiveTree(ref) {
990
- const session = await this.getOrOpen(ref);
986
+ async archiveTree(ref, managementContext) {
987
+ const session = await this.getOrOpen(ref, managementContext);
991
988
  const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
992
989
  const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false);
993
990
  const plan = planSessionArchiveTree(root, catalog);
@@ -996,7 +993,7 @@ export class PiSessionService {
996
993
  throw new Error(`归档 ${sessionDisplayName(busy)} 前请先停止当前会话活动`);
997
994
  const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
998
995
  for (const input of archiveInputs)
999
- await this.closeActive(input.sessionId);
996
+ await this.closeActiveInScope(input.sessionId, eventScopeFromManagementContext(managementContext));
1000
997
  await this.archiveStoreArchiveMany(archiveInputs);
1001
998
  return {
1002
999
  archived: true,
@@ -1005,25 +1002,25 @@ export class PiSessionService {
1005
1002
  skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
1006
1003
  };
1007
1004
  }
1008
- async restore(ref) {
1005
+ async restore(ref, managementContext) {
1009
1006
  const archived = await this.getArchived(ref);
1010
1007
  if (archived === undefined)
1011
1008
  throw new Error("未找到会话");
1012
- await this.closeActive(archived.sessionId);
1009
+ await this.closeActiveInScope(archived.sessionId, eventScopeFromManagementContext(managementContext));
1013
1010
  await this.archiveStore.restore(archived.sessionId);
1014
1011
  }
1015
- async deleteArchived(ref) {
1012
+ async deleteArchived(ref, managementContext) {
1016
1013
  const record = await this.getArchived(ref);
1017
1014
  if (record === undefined)
1018
1015
  throw new Error("未找到已归档会话");
1019
1016
  if (this.archiveStore.deleteArchived === undefined)
1020
1017
  throw new Error("归档存储不支持删除");
1021
- await this.closeActive(record.sessionId);
1018
+ await this.closeActiveInScope(record.sessionId, eventScopeFromManagementContext(managementContext));
1022
1019
  if (record.archivePath === undefined)
1023
1020
  await this.ensureArchivedRecordMoved(record);
1024
1021
  await this.archiveStore.deleteArchived(record.sessionId);
1025
1022
  }
1026
- async deleteArchivedMany(refs) {
1023
+ async deleteArchivedMany(refs, managementContext) {
1027
1024
  if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
1028
1025
  throw new Error("Archive store does not support deletion");
1029
1026
  const uniqueRefs = uniqueBulkSessionRefs(refs);
@@ -1036,7 +1033,7 @@ export class PiSessionService {
1036
1033
  failures.push({ sessionId: ref.id, error: "Archived session not found" });
1037
1034
  continue;
1038
1035
  }
1039
- const active = this.activeForLookup({ id: record.sessionId, cwd: record.cwd });
1036
+ const active = this.activeForLookup({ id: record.sessionId, cwd: record.cwd }, managementContext);
1040
1037
  if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
1041
1038
  failures.push({ sessionId: record.sessionId, error: "Stop current session activity before deleting archived session" });
1042
1039
  continue;
@@ -1046,7 +1043,7 @@ export class PiSessionService {
1046
1043
  const readyRecords = [];
1047
1044
  for (const item of planItems) {
1048
1045
  try {
1049
- await this.closeActive(item.record.sessionId);
1046
+ await this.closeActiveInScope(item.record.sessionId, eventScopeFromManagementContext(managementContext));
1050
1047
  readyRecords.push(item.record);
1051
1048
  }
1052
1049
  catch (error) {
@@ -1074,17 +1071,18 @@ export class PiSessionService {
1074
1071
  generatedAt: new Date().toISOString(),
1075
1072
  };
1076
1073
  }
1077
- async reload(ref) {
1074
+ async reload(ref, managementContext) {
1078
1075
  await this.assertWritable(ref);
1079
- const session = await this.getOrOpen(ref);
1076
+ const active = await this.getActive(ref, managementContext);
1077
+ const session = active.runtime.session;
1080
1078
  if (this.hasActiveWork(session))
1081
1079
  throw new Error("重新加载前请先停止当前会话活动");
1082
- await this.closeActive(session.sessionId);
1083
- const reopened = await this.getActive(ref);
1080
+ await this.closeActiveSession(active);
1081
+ const reopened = await this.getActive(ref, managementContext);
1084
1082
  this.publishStatus(reopened.runtime.session);
1085
1083
  }
1086
- async detachParent(ref) {
1087
- const session = await this.getOrOpen(ref);
1084
+ async detachParent(ref, managementContext) {
1085
+ const session = await this.getOrOpen(ref, managementContext);
1088
1086
  const sessionFile = session.sessionFile;
1089
1087
  if (sessionFile === undefined || sessionFile === "")
1090
1088
  throw new Error("会话尚未持久化");
@@ -1092,8 +1090,8 @@ export class PiSessionService {
1092
1090
  clearParentSessionHeader(session.sessionManager);
1093
1091
  this.unregisterSubsession(session.sessionId);
1094
1092
  }
1095
- async abort(ref) {
1096
- const active = this.activeForLookupAny(ref);
1093
+ async abort(ref, managementContext) {
1094
+ const active = this.activeForLookup(ref, managementContext);
1097
1095
  if (active === undefined)
1098
1096
  return;
1099
1097
  const sessionId = active.runtime.session.sessionId;
@@ -1103,11 +1101,11 @@ export class PiSessionService {
1103
1101
  this.publishActivity(active.runtime.session, "stopped", "idle");
1104
1102
  this.publishStatus(active.runtime.session);
1105
1103
  }
1106
- stop(ref) {
1107
- const active = this.activeForLookupAny(ref);
1104
+ stop(ref, managementContext) {
1105
+ const active = this.activeForLookup(ref, managementContext);
1108
1106
  if (active === undefined)
1109
1107
  return;
1110
- void this.closeActive(active.runtime.session.sessionId).catch(() => {
1108
+ void this.closeActiveSession(active).catch(() => {
1111
1109
  // Best-effort shutdown; callers that need errors await closeActive directly.
1112
1110
  });
1113
1111
  }
@@ -1198,8 +1196,7 @@ export class PiSessionService {
1198
1196
  }));
1199
1197
  }
1200
1198
  activeSessionHasWork(sessionId) {
1201
- const active = this.active.get(sessionId);
1202
- return active !== undefined && this.hasActiveWork(active.runtime.session);
1199
+ return this.activeSessionsForId(sessionId).some((active) => this.hasActiveWork(active.runtime.session));
1203
1200
  }
1204
1201
  reconcilableSessionIds(cwd, listedSessionIds, archivedById) {
1205
1202
  const sessionIds = new Set(listedSessionIds);
@@ -1211,7 +1208,7 @@ export class PiSessionService {
1211
1208
  return [...sessionIds];
1212
1209
  }
1213
1210
  async ensureArchivedSessionMoved(record, session) {
1214
- if (session === undefined || this.active.has(record.sessionId))
1211
+ if (session === undefined || this.activeSessionsForId(record.sessionId).length > 0)
1215
1212
  return record;
1216
1213
  try {
1217
1214
  return await this.archiveStore.archive(archiveInputFromListEntry(session));
@@ -1299,12 +1296,19 @@ export class PiSessionService {
1299
1296
  return [...names];
1300
1297
  }
1301
1298
  async closeActive(sessionId) {
1302
- const active = this.active.get(sessionId);
1303
- if (!active)
1304
- return;
1305
- this.active.delete(sessionId);
1306
- this.activities.delete(sessionId);
1307
- this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
1299
+ const activeSessions = this.activeSessionsForId(sessionId);
1300
+ await Promise.all(activeSessions.map((active) => this.closeActiveSession(active)));
1301
+ }
1302
+ async closeActiveInScope(sessionId, eventScope) {
1303
+ const active = this.activeForSessionIdAndScope(sessionId, eventScope);
1304
+ if (active !== undefined)
1305
+ await this.closeActiveSession(active);
1306
+ }
1307
+ async closeActiveSession(active) {
1308
+ const sessionId = active.runtime.session.sessionId;
1309
+ this.active.delete(activeSessionKey(sessionId, active.eventScope));
1310
+ this.activities.delete(activeSessionKey(sessionId, active.eventScope));
1311
+ this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
1308
1312
  this.clearAuthLossWarningsForSession(sessionId);
1309
1313
  this.clearCompactionPromptQueue(sessionId);
1310
1314
  // Disarm subsession notification before teardown so the abort below cannot
@@ -1332,9 +1336,6 @@ export class PiSessionService {
1332
1336
  const active = this.activeForLookup(ref, managementContext);
1333
1337
  if (active !== undefined)
1334
1338
  return active;
1335
- const mismatched = this.activeForLookupAny(ref);
1336
- if (mismatched !== undefined)
1337
- await this.closeActive(mismatched.runtime.session.sessionId);
1338
1339
  const archived = await this.getArchived(ref);
1339
1340
  if (archived?.archivePath !== undefined) {
1340
1341
  return this.create(this.sessionManager.open(archived.archivePath), archived.cwd, managementContext === undefined ? {} : { managementContext });
@@ -1356,11 +1357,13 @@ export class PiSessionService {
1356
1357
  }
1357
1358
  activeForLookup(ref, managementContext) {
1358
1359
  const key = managementContextKey(managementContext);
1360
+ const eventScope = eventScopeFromManagementContext(managementContext);
1359
1361
  const sessionId = sessionIdFromLookup(ref);
1360
- const exact = this.active.get(sessionId);
1362
+ const exact = this.active.get(activeSessionKey(sessionId, eventScope));
1361
1363
  if (exact !== undefined && lookupMatchesActiveSession(ref, exact) && exact.managementContextKey === key)
1362
1364
  return exact;
1363
- for (const [candidateId, active] of this.active.entries()) {
1365
+ for (const active of this.active.values()) {
1366
+ const candidateId = active.runtime.session.sessionId;
1364
1367
  if (candidateId.startsWith(sessionId) && lookupMatchesActiveSession(ref, active) && active.managementContextKey === key)
1365
1368
  return active;
1366
1369
  }
@@ -1368,15 +1371,39 @@ export class PiSessionService {
1368
1371
  }
1369
1372
  activeForLookupAny(ref) {
1370
1373
  const sessionId = sessionIdFromLookup(ref);
1371
- const exact = this.active.get(sessionId);
1374
+ const exact = this.activeForSessionId(sessionId);
1372
1375
  if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
1373
1376
  return exact;
1374
- for (const [candidateId, active] of this.active.entries()) {
1377
+ for (const active of this.active.values()) {
1378
+ const candidateId = active.runtime.session.sessionId;
1375
1379
  if (candidateId.startsWith(sessionId) && lookupMatchesActiveSession(ref, active))
1376
1380
  return active;
1377
1381
  }
1378
1382
  return undefined;
1379
1383
  }
1384
+ getActiveForCommand(sessionId, eventScope) {
1385
+ const active = eventScope === undefined ? this.activeForSessionId(sessionId) : this.activeForSessionIdAndScope(sessionId, eventScope);
1386
+ if (active === undefined)
1387
+ throw new Error(`Session not active: ${sessionId}`);
1388
+ return Promise.resolve(active);
1389
+ }
1390
+ submitCommandPrompt(sessionId, text, eventScope) {
1391
+ const active = this.activeForSessionIdAndScope(sessionId, eventScope);
1392
+ if (active === undefined)
1393
+ throw new Error(`Session not active: ${sessionId}`);
1394
+ return this.submitPrompt(active.runtime.session, text, undefined, [], false);
1395
+ }
1396
+ activeForSessionId(sessionId) {
1397
+ return this.activeSessionsForId(sessionId)[0];
1398
+ }
1399
+ activeForSessionIdAndScope(sessionId, eventScope) {
1400
+ if (eventScope === undefined)
1401
+ return undefined;
1402
+ return this.active.get(activeSessionKey(sessionId, eventScope));
1403
+ }
1404
+ activeSessionsForId(sessionId) {
1405
+ return [...new Set(this.active.values())].filter((active) => active.runtime.session.sessionId === sessionId);
1406
+ }
1380
1407
  async create(sessionManager, cwd, options = {}) {
1381
1408
  const runtime = await this.createAgentRuntime(this.createRuntime, {
1382
1409
  cwd,
@@ -1385,15 +1412,15 @@ export class PiSessionService {
1385
1412
  ...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
1386
1413
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
1387
1414
  });
1415
+ const active = { runtime, unsubscribe: noop, managementContextKey: managementContextKey(options.managementContext), eventScope: eventScopeFromManagementContext(options.managementContext) };
1416
+ this.active.set(activeSessionKey(runtime.session.sessionId, active.eventScope), active);
1388
1417
  await this.bindSessionExtensions(runtime.session);
1389
- const active = { runtime, unsubscribe: noop, managementContextKey: managementContextKey(options.managementContext) };
1390
1418
  this.bindRuntime(active);
1391
1419
  runtime.setRebindSession(async (session) => {
1392
1420
  await this.bindSessionExtensions(session);
1393
1421
  this.bindRuntime(active);
1394
1422
  await this.recoverSubsessionTrackingForOpenedSession(session);
1395
1423
  });
1396
- this.active.set(runtime.session.sessionId, active);
1397
1424
  await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
1398
1425
  this.publishStatus(runtime.session);
1399
1426
  return active;
@@ -1403,7 +1430,7 @@ export class PiSessionService {
1403
1430
  onError: (error) => {
1404
1431
  const message = `${error.extensionPath}: ${error.error}`;
1405
1432
  this.publishActivity(session, "extension error", "error", message);
1406
- this.events.publish(session.sessionId, { type: "session.error", message });
1433
+ this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
1407
1434
  },
1408
1435
  });
1409
1436
  }
@@ -1413,12 +1440,12 @@ export class PiSessionService {
1413
1440
  for (const [sessionId, candidate] of this.active.entries()) {
1414
1441
  if (candidate === active) {
1415
1442
  this.active.delete(sessionId);
1416
- if (sessionId !== session.sessionId)
1417
- this.clearCompactionPromptQueue(sessionId);
1443
+ if (!sessionId.endsWith(`\0${session.sessionId}`))
1444
+ this.clearCompactionPromptQueue(candidate.runtime.session.sessionId);
1418
1445
  }
1419
1446
  }
1420
1447
  active.unsubscribe = session.subscribe((event) => {
1421
- this.events.publish(session.sessionId, toClientEvent(event));
1448
+ this.events.publish(session.sessionId, toClientEvent(event), active.eventScope);
1422
1449
  this.publishActivityForEvent(session, event);
1423
1450
  const eventType = getString(event, "type");
1424
1451
  if (eventType === "compaction_end")
@@ -1428,7 +1455,7 @@ export class PiSessionService {
1428
1455
  this.publishStatus(session);
1429
1456
  this.updateSubsessionTracking(session);
1430
1457
  });
1431
- this.active.set(session.sessionId, active);
1458
+ this.active.set(activeSessionKey(session.sessionId, active.eventScope), active);
1432
1459
  }
1433
1460
  scheduleCompactionQueueDrain(sessionId, delayMs = 0) {
1434
1461
  if (!this.compactionPromptQueues.has(sessionId) || this.compactionDrainTimers.has(sessionId))
@@ -1440,7 +1467,7 @@ export class PiSessionService {
1440
1467
  this.compactionDrainTimers.set(sessionId, timer);
1441
1468
  }
1442
1469
  drainCompactionPromptQueue(sessionId) {
1443
- const active = this.active.get(sessionId);
1470
+ const active = this.activeForSessionId(sessionId);
1444
1471
  if (active === undefined)
1445
1472
  return;
1446
1473
  const { session } = active.runtime;
@@ -1509,20 +1536,24 @@ export class PiSessionService {
1509
1536
  }
1510
1537
  applyAuthChange(change = {}) {
1511
1538
  this.modelRegistry.refresh();
1539
+ this.managementModelRegistry.refresh();
1540
+ const changedScope = change.scope ?? "normal";
1512
1541
  for (const active of this.active.values()) {
1542
+ if (authScopeFromEventScope(active.eventScope) !== changedScope)
1543
+ continue;
1513
1544
  const { session } = active.runtime;
1514
1545
  session.modelRegistry.refresh();
1515
- this.syncCurrentModelAuthWarning(session, change.removedProviderId);
1546
+ this.syncCurrentModelAuthWarning(session, active.eventScope, change.removedProviderId);
1516
1547
  this.publishStatus(session);
1517
1548
  }
1518
1549
  }
1519
- syncCurrentModelAuthWarning(session, removedProviderId) {
1550
+ syncCurrentModelAuthWarning(session, eventScope, removedProviderId) {
1520
1551
  const model = session.model;
1521
1552
  if (model === undefined)
1522
1553
  return;
1523
1554
  if (model.provider === "unknown" && model.id === "unknown")
1524
1555
  return;
1525
- const warningKey = authLossWarningKey(session.sessionId, model.provider, model.id);
1556
+ const warningKey = authLossWarningKey(session.sessionId, eventScope, model.provider, model.id);
1526
1557
  const registered = session.modelRegistry.find(model.provider, model.id);
1527
1558
  if (registered === undefined)
1528
1559
  return;
@@ -1537,12 +1568,11 @@ export class PiSessionService {
1537
1568
  type: "command.output",
1538
1569
  level: "error",
1539
1570
  message: `Authentication for ${model.provider}/${model.id} was removed. Use /model to select another model.`,
1540
- });
1571
+ }, eventScope);
1541
1572
  }
1542
1573
  clearAuthLossWarningsForSession(sessionId) {
1543
- const prefix = `${sessionId}:`;
1544
1574
  for (const key of this.authLossWarnings) {
1545
- if (key.startsWith(prefix))
1575
+ if (key.includes(`:${sessionId}:`))
1546
1576
  this.authLossWarnings.delete(key);
1547
1577
  }
1548
1578
  }
@@ -1550,8 +1580,9 @@ export class PiSessionService {
1550
1580
  const event = session.sessionName === undefined
1551
1581
  ? { type: "session.name", sessionId: session.sessionId }
1552
1582
  : { type: "session.name", sessionId: session.sessionId, name: session.sessionName };
1553
- this.events.publish(session.sessionId, event);
1554
- this.events.publishGlobal(event);
1583
+ const eventScope = this.eventScopeForSession(session);
1584
+ this.events.publish(session.sessionId, event, eventScope);
1585
+ this.events.publishGlobal(event, eventScope);
1555
1586
  }
1556
1587
  publishHeartbeats() {
1557
1588
  for (const active of this.active.values()) {
@@ -1560,7 +1591,7 @@ export class PiSessionService {
1560
1591
  // the session still reports active work transiently, so the event-driven
1561
1592
  // latch may not fire. The heartbeat re-checks once the session settles.
1562
1593
  this.updateSubsessionTracking(session);
1563
- const activity = this.activities.get(session.sessionId);
1594
+ const activity = this.activities.get(activeSessionKey(session.sessionId, active.eventScope));
1564
1595
  if (!this.hasActiveWork(session)) {
1565
1596
  if (activity?.phase === "active")
1566
1597
  this.publishStatus(session);
@@ -1642,29 +1673,42 @@ export class PiSessionService {
1642
1673
  publishActivity(session, label, phase, detail) {
1643
1674
  const at = new Date().toISOString();
1644
1675
  const stored = detail === undefined ? { phase, label, at } : { phase, label, detail, at };
1645
- this.activities.set(session.sessionId, stored);
1676
+ const eventScope = this.eventScopeForSession(session);
1677
+ this.activities.set(activeSessionKey(session.sessionId, eventScope), stored);
1646
1678
  const activity = detail === undefined ? { sessionId: session.sessionId, phase, label, at } : { sessionId: session.sessionId, phase, label, detail, at };
1647
- this.workspaceActivity?.applySessionActivity(session.sessionManager.getCwd(), activity);
1648
- this.events.publish(session.sessionId, { type: "activity.update", activity });
1649
- this.events.publishGlobal({ type: "activity.update", activity });
1679
+ this.workspaceActivity?.applySessionActivity(session.sessionManager.getCwd(), activity, eventScope);
1680
+ this.events.publish(session.sessionId, { type: "activity.update", activity }, eventScope);
1681
+ this.events.publishGlobal({ type: "activity.update", activity }, eventScope);
1650
1682
  }
1651
1683
  publishStatus(session) {
1652
1684
  const status = this.statusFromSession(session);
1653
1685
  this.clearStaleActiveActivity(session);
1654
- this.workspaceActivity?.applySessionStatus(session.sessionManager.getCwd(), status);
1655
- this.events.publish(session.sessionId, { type: "status.update", status });
1656
- this.events.publishGlobal({ type: "status.update", status });
1686
+ const eventScope = this.eventScopeForSession(session);
1687
+ this.workspaceActivity?.applySessionStatus(session.sessionManager.getCwd(), status, eventScope);
1688
+ this.events.publish(session.sessionId, { type: "status.update", status }, eventScope);
1689
+ this.events.publishGlobal({ type: "status.update", status }, eventScope);
1657
1690
  }
1658
1691
  clearStaleActiveActivity(session) {
1659
- const current = this.activities.get(session.sessionId);
1692
+ const eventScope = this.eventScopeForSession(session);
1693
+ const current = this.activities.get(activeSessionKey(session.sessionId, eventScope));
1660
1694
  if (current?.phase !== "active" || this.hasActiveWork(session))
1661
1695
  return;
1662
1696
  const at = new Date().toISOString();
1663
1697
  const stored = { phase: "idle", label: "idle", at };
1664
- this.activities.set(session.sessionId, stored);
1698
+ this.activities.set(activeSessionKey(session.sessionId, eventScope), stored);
1665
1699
  const activity = { sessionId: session.sessionId, ...stored };
1666
- this.events.publish(session.sessionId, { type: "activity.update", activity });
1667
- this.events.publishGlobal({ type: "activity.update", activity });
1700
+ this.events.publish(session.sessionId, { type: "activity.update", activity }, eventScope);
1701
+ this.events.publishGlobal({ type: "activity.update", activity }, eventScope);
1702
+ }
1703
+ activityForSession(session) {
1704
+ return this.activities.get(activeSessionKey(session.sessionId, this.eventScopeForSession(session)));
1705
+ }
1706
+ eventScopeForSession(session) {
1707
+ for (const active of this.active.values()) {
1708
+ if (active.runtime.session === session)
1709
+ return active.eventScope;
1710
+ }
1711
+ return NORMAL_SESSION_EVENT_SCOPE;
1668
1712
  }
1669
1713
  statusFromSession(session) {
1670
1714
  const stats = session.getSessionStats();