@chainingintention/pi-web-cn 1.202607.2 → 1.202607.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{CodeViewer-Cyy3_LEk.js → CodeViewer-CGV4uBeZ.js} +1 -1
- package/dist/client/assets/{TerminalPanel-CNsIQZbo.js → TerminalPanel-CeJN9p9P.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-BprnYHmM.js → UnifiedDiffViewer-BSzsWx-m.js} +1 -1
- package/dist/client/assets/{index-DB_vEZT7.js → index-BA1zmHnv.js} +302 -240
- package/dist/client/index.html +1 -1
- package/dist/config.js +4 -1
- package/dist/config.js.map +1 -1
- package/dist/server/activity/workspaceActivityRoutes.js +7 -1
- package/dist/server/activity/workspaceActivityRoutes.js.map +1 -1
- package/dist/server/activity/workspaceActivityService.js +70 -46
- package/dist/server/activity/workspaceActivityService.js.map +1 -1
- package/dist/server/app.js +4 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/normalAuth.js +223 -0
- package/dist/server/normalAuth.js.map +1 -0
- package/dist/server/realtime/sessionEventHub.js +24 -13
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/realtime/sessionEventScope.js +41 -0
- package/dist/server/realtime/sessionEventScope.js.map +1 -0
- package/dist/server/sessiond.js +12 -4
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/authRoutes.js +15 -7
- package/dist/server/sessions/authRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +2 -1
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +162 -118
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +27 -27
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +22 -18
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/terminals/terminalRoutes.js +3 -3
- package/dist/server/terminals/terminalRoutes.js.map +1 -1
- package/dist/server/terminals/terminalService.js +26 -13
- package/dist/server/terminals/terminalService.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +8 -0
- package/dist/shared/piWebConfigParsing.js +29 -0
- package/dist/shared/piWebConfigParsing.js.map +1 -1
- 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
|
|
45
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
1104
|
+
stop(ref, managementContext) {
|
|
1105
|
+
const active = this.activeForLookup(ref, managementContext);
|
|
1108
1106
|
if (active === undefined)
|
|
1109
1107
|
return;
|
|
1110
|
-
void this.
|
|
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
|
-
|
|
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.
|
|
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
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
this.
|
|
1307
|
-
|
|
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
|
|
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.
|
|
1374
|
+
const exact = this.activeForSessionId(sessionId);
|
|
1372
1375
|
if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
|
|
1373
1376
|
return exact;
|
|
1374
|
-
for (const
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
1554
|
-
this.events.
|
|
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.
|
|
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.
|
|
1655
|
-
this.
|
|
1656
|
-
this.events.
|
|
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
|
|
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();
|