@chainingintention/pi-web-cn 1.202607.10 → 1.202608.2
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/README.md +9 -9
- package/dist/cli.js +27 -12
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-5vQNxhKQ.js → CodeViewer-j37b7YHz.js} +1 -1
- package/dist/client/assets/{TerminalPanel-Ctz1zP84.js → TerminalPanel-DqDiXDrk.js} +15 -15
- package/dist/client/assets/{UnifiedDiffViewer-DXLMHyO2.js → UnifiedDiffViewer-AQTSNI1y.js} +3 -3
- package/dist/client/assets/index-DDuQX9_Y.js +4101 -0
- package/dist/client/index.html +1 -1
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -1
- package/dist/nativeServices/servicePlan.js +3 -2
- package/dist/nativeServices/servicePlan.js.map +1 -1
- package/dist/nativeServices/serviceProbe.js +7 -5
- package/dist/nativeServices/serviceProbe.js.map +1 -1
- package/dist/pi-web-plugins/info/infoInternals.js +223 -0
- package/dist/pi-web-plugins/info/pi-web-plugin.js +19 -22
- package/dist/pi-web-plugins/relays/markdownDocument.js +81 -0
- package/dist/pi-web-plugins/relays/package.json +9 -0
- package/dist/pi-web-plugins/relays/pi-web-plugin.js +44 -0
- package/dist/pi-web-plugins/relays/relayDiscovery.js +96 -0
- package/dist/pi-web-plugins/relays/relaysPanelElement.js +327 -0
- package/dist/pi-web-plugins/relays/vendor/README.md +24 -0
- package/dist/pi-web-plugins/relays/vendor/marked.esm.js +76 -0
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +100 -100
- package/dist/pi-web-plugins/updates/updatesLogic.js +8 -5
- package/dist/plugin-api.d.ts +5 -1
- package/dist/server/configRoutes.js +11 -0
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/diagnostics/nodePtyNativeModule.js +36 -0
- package/dist/server/diagnostics/nodePtyNativeModule.js.map +1 -0
- package/dist/server/git/gitService.js +176 -5
- package/dist/server/git/gitService.js.map +1 -1
- package/dist/server/piWebStatus.js +7 -4
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +25 -4
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessiond/sessionDaemonStartup.js +0 -42
- package/dist/server/sessiond/sessionDaemonStartup.js.map +1 -1
- package/dist/server/sessiond/sessionServiceDependencies.js +31 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -0
- package/dist/server/sessions/askUserTool.js +105 -0
- package/dist/server/sessions/askUserTool.js.map +1 -0
- package/dist/server/sessions/authProviderOptions.js +26 -29
- package/dist/server/sessions/authProviderOptions.js.map +1 -1
- package/dist/server/sessions/authRoutes.js +9 -1
- package/dist/server/sessions/authRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +143 -33
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/globalProviderPolicy.js +167 -0
- package/dist/server/sessions/globalProviderPolicy.js.map +1 -0
- package/dist/server/sessions/modelCatalogRefresher.js +192 -0
- package/dist/server/sessions/modelCatalogRefresher.js.map +1 -0
- package/dist/server/sessions/oauthLoginFlowService.js +181 -56
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/parentSessionLocator.js +75 -0
- package/dist/server/sessions/parentSessionLocator.js.map +1 -0
- package/dist/server/sessions/pendingAskStore.js +278 -0
- package/dist/server/sessions/pendingAskStore.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +1329 -214
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/plainTextTheme.js +112 -0
- package/dist/server/sessions/plainTextTheme.js.map +1 -0
- package/dist/server/sessions/projectAuthService.js +3 -3
- package/dist/server/sessions/projectAuthService.js.map +1 -1
- package/dist/server/sessions/sessionActivityCoordinator.js +38 -0
- package/dist/server/sessions/sessionActivityCoordinator.js.map +1 -0
- package/dist/server/sessions/sessionCommandService.js +60 -5
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionContentSearch.js +74 -0
- package/dist/server/sessions/sessionContentSearch.js.map +1 -0
- package/dist/server/sessions/sessionFileHeader.js +45 -0
- package/dist/server/sessions/sessionFileHeader.js.map +1 -0
- package/dist/server/sessions/sessionNotificationStore.js +484 -0
- package/dist/server/sessions/sessionNotificationStore.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +265 -1
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionTreeProjection.js +250 -0
- package/dist/server/sessions/sessionTreeProjection.js.map +1 -0
- package/dist/server/sessions/sessionUnreadStore.js +520 -0
- package/dist/server/sessions/sessionUnreadStore.js.map +1 -0
- package/dist/server/sessions/spawnSubsessionTool.js +10 -12
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/sessions/spawnTargetResolver.js +3 -16
- package/dist/server/sessions/spawnTargetResolver.js.map +1 -1
- package/dist/server/terminals/terminalService.js +8 -2
- package/dist/server/terminals/terminalService.js.map +1 -1
- package/dist/server/workspaces/gitWorktreeDiscovery.js +9 -0
- package/dist/server/workspaces/gitWorktreeDiscovery.js.map +1 -1
- package/dist/server/workspaces/projectWorkspaceCwds.js +22 -0
- package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -0
- package/dist/server/workspaces/workspaceService.js +15 -2
- package/dist/server/workspaces/workspaceService.js.map +1 -1
- package/dist/shared/activity.js +9 -1
- package/dist/shared/activity.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +434 -8
- package/dist/shared/apiTypes.js +26 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +9 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +14 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/thinkingLevels.d.ts +2 -0
- package/dist/shared/thinkingLevels.js +25 -0
- package/dist/shared/thinkingLevels.js.map +1 -1
- package/docs/config.md +76 -9
- package/docs/plugins.md +72 -16
- package/install.sh +1 -1
- package/package.json +10 -8
- package/dist/client/assets/index-Jmsbve6u.js +0 -3173
- package/dist/server/sessions/sessionArchiveMigration.js +0 -590
- package/dist/server/sessions/sessionArchiveMigration.js.map +0 -1
|
@@ -1,31 +1,61 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, ModelRuntime, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
5
6
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
6
7
|
import { eventScopeFromManagementContext, managementContextKey, NORMAL_SESSION_EVENT_SCOPE } from "../realtime/sessionEventScope.js";
|
|
7
8
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
8
9
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
10
|
+
import { projectSessionTree } from "./sessionTreeProjection.js";
|
|
9
11
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
10
12
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
11
13
|
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
12
14
|
import { computeEditPreview } from "./editPreview.js";
|
|
13
15
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
14
16
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
17
|
+
import { ASK_USER_ANSWERS_CUSTOM_TYPE, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
15
18
|
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
|
16
19
|
import { SessionPinStore } from "./sessionPinStore.js";
|
|
20
|
+
import { SessionActivityCoordinator } from "./sessionActivityCoordinator.js";
|
|
21
|
+
import { searchSessionContent } from "./sessionContentSearch.js";
|
|
17
22
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
23
|
+
import { readSessionHeaderSummary } from "./sessionFileHeader.js";
|
|
24
|
+
import { countOutOfListingChildren, locateOutOfListingParents } from "./parentSessionLocator.js";
|
|
25
|
+
import { siblingWorkspaceCwds } from "../workspaces/projectWorkspaceCwds.js";
|
|
18
26
|
import { piWebDataDir } from "../../config.js";
|
|
19
27
|
import { createScopedSettingsManager, resolveSettingsScopeDirectory } from "./projectSettingsScope.js";
|
|
28
|
+
import { createAskUserToolDefinition } from "./askUserTool.js";
|
|
29
|
+
import { PendingAskStore, renderAskUserAnswersText } from "./pendingAskStore.js";
|
|
20
30
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
21
31
|
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
22
32
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
23
33
|
import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
24
34
|
import { createManagedAgentToolOptions, createManagedPythonToolDefinition } from "./managementAgentTools.js";
|
|
25
35
|
import { PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR, managementAgentToolNames, withRuntimeCreationEnvironment, writeManagementPermissionSystemPolicy } from "./managementPermissionSystem.js";
|
|
36
|
+
import { SessionNotificationStore } from "./sessionNotificationStore.js";
|
|
37
|
+
import { plainTextTheme } from "./plainTextTheme.js";
|
|
38
|
+
import { SessionUnreadStore } from "./sessionUnreadStore.js";
|
|
26
39
|
import { archiveCandidateFromActiveSession, archiveCandidateFromArchivedRecord, archiveCandidateFromListEntry, archiveInputFromActiveSession, archiveInputFromCandidate, archiveInputFromListEntry, clientSessionFromArchivedRecord, compareArchivedRecords, } from "./sessionArchiveMapping.js";
|
|
27
40
|
import { getBoolean, getProperty, getString, toClientEvent } from "./sessionUiEvents.js";
|
|
28
41
|
const noopLogger = { info() { } };
|
|
42
|
+
const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
|
|
43
|
+
/**
|
|
44
|
+
* User-facing names for the two phases of session startup PI WEB can prove it
|
|
45
|
+
* is inside: it awaits exactly one call for each, so the phase is a fact rather
|
|
46
|
+
* than a guess. Deliberately free of internal symbol names and file paths.
|
|
47
|
+
*/
|
|
48
|
+
const STARTUP_PHASE_RUNTIME = "正在启动 Pi 会话";
|
|
49
|
+
const STARTUP_PHASE_EXTENSIONS = "正在加载会话扩展";
|
|
50
|
+
/**
|
|
51
|
+
* Appended to whichever phase is running when a background provider catalog
|
|
52
|
+
* refresh happens to be in flight. It is stated as a concurrent fact, never as
|
|
53
|
+
* the cause: PI WEB can verify that a refresh is running, but not that this
|
|
54
|
+
* particular startup is waiting on it.
|
|
55
|
+
*/
|
|
56
|
+
const STARTUP_CONCURRENT_CATALOG_REFRESH = "服务商模型列表正在刷新";
|
|
57
|
+
const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
|
|
58
|
+
const MAX_PENDING_UNREAD_MUTATIONS = SESSION_UNREAD_LIMIT + 1;
|
|
29
59
|
function noop() {
|
|
30
60
|
// Intentionally empty default unsubscribe callback.
|
|
31
61
|
}
|
|
@@ -49,6 +79,37 @@ function lookupMatchesActiveSession(ref, active) {
|
|
|
49
79
|
function activeSessionKey(sessionId, eventScope) {
|
|
50
80
|
return `${eventScope}\0${sessionId}`;
|
|
51
81
|
}
|
|
82
|
+
function sessionTreeNavigationOptions(request) {
|
|
83
|
+
switch (request.summary.mode) {
|
|
84
|
+
case "none":
|
|
85
|
+
return { summarize: false };
|
|
86
|
+
case "default":
|
|
87
|
+
return { summarize: true };
|
|
88
|
+
case "custom": {
|
|
89
|
+
const customInstructions = request.summary.instructions.trim();
|
|
90
|
+
if (customInstructions === "")
|
|
91
|
+
throw new Error("Custom branch-summary instructions are required");
|
|
92
|
+
if (customInstructions.length > SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH) {
|
|
93
|
+
throw new Error(`Custom branch-summary instructions must be at most ${String(SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH)} characters`);
|
|
94
|
+
}
|
|
95
|
+
return { summarize: true, customInstructions };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function decrementWeakCount(counts, key) {
|
|
100
|
+
const remaining = (counts.get(key) ?? 1) - 1;
|
|
101
|
+
if (remaining <= 0)
|
|
102
|
+
counts.delete(key);
|
|
103
|
+
else
|
|
104
|
+
counts.set(key, remaining);
|
|
105
|
+
}
|
|
106
|
+
function decrementMapCount(counts, key) {
|
|
107
|
+
const remaining = (counts.get(key) ?? 1) - 1;
|
|
108
|
+
if (remaining <= 0)
|
|
109
|
+
counts.delete(key);
|
|
110
|
+
else
|
|
111
|
+
counts.set(key, remaining);
|
|
112
|
+
}
|
|
52
113
|
function requirePromptText(value) {
|
|
53
114
|
if (typeof value !== "string")
|
|
54
115
|
throw new Error("提示文本为必填项");
|
|
@@ -81,6 +142,109 @@ function pathInsideOrEqual(parent, child) {
|
|
|
81
142
|
const childRelativePath = relative(normalizedParent, normalizedChild);
|
|
82
143
|
return childRelativePath === "" || (!childRelativePath.startsWith("..") && !isAbsolute(childRelativePath));
|
|
83
144
|
}
|
|
145
|
+
const CLEAR_RUNTIME_NOTIFICATIONS = { kind: "clear", reason: "runtime-close" };
|
|
146
|
+
const DEFER_RUNTIME_NOTIFICATIONS = { kind: "defer" };
|
|
147
|
+
function resourceDiagnosticToWarning(diagnostic, source) {
|
|
148
|
+
return {
|
|
149
|
+
severity: diagnostic.type === "error" ? "error" : "warning",
|
|
150
|
+
message: diagnostic.message,
|
|
151
|
+
source,
|
|
152
|
+
...(diagnostic.path === undefined ? {} : { path: diagnostic.path }),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function runtimeDiagnosticToWarning(diagnostic) {
|
|
156
|
+
return { severity: diagnostic.type, message: diagnostic.message, source: "runtime" };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Compute the live warnings for a runtime by re-reading its current resource
|
|
160
|
+
* loader diagnostics, extension load errors, and runtime setup diagnostics.
|
|
161
|
+
*
|
|
162
|
+
* This mimics the TUI recomputing warnings on every (re)bind: it reads the
|
|
163
|
+
* runtime's current state rather than a cached snapshot, so a rebuilt runtime
|
|
164
|
+
* yields fresh warnings. Runtimes without SDK services (e.g. test fakes)
|
|
165
|
+
* contribute no warnings.
|
|
166
|
+
*/
|
|
167
|
+
export function collectRuntimeWarnings(runtime) {
|
|
168
|
+
const warnings = [];
|
|
169
|
+
for (const diagnostic of runtime.diagnostics ?? [])
|
|
170
|
+
warnings.push(runtimeDiagnosticToWarning(diagnostic));
|
|
171
|
+
const resourceLoader = runtime.services?.resourceLoader;
|
|
172
|
+
if (resourceLoader !== undefined) {
|
|
173
|
+
for (const diagnostic of resourceLoader.getSkills().diagnostics)
|
|
174
|
+
warnings.push(resourceDiagnosticToWarning(diagnostic, "skill"));
|
|
175
|
+
for (const diagnostic of resourceLoader.getPrompts().diagnostics)
|
|
176
|
+
warnings.push(resourceDiagnosticToWarning(diagnostic, "prompt"));
|
|
177
|
+
for (const diagnostic of resourceLoader.getThemes().diagnostics)
|
|
178
|
+
warnings.push(resourceDiagnosticToWarning(diagnostic, "theme"));
|
|
179
|
+
for (const error of resourceLoader.getExtensions().errors) {
|
|
180
|
+
warnings.push({ severity: "error", message: `${error.path}: ${error.error}`, source: "extension", path: error.path });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return warnings;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Verbatim TUI wording for the Anthropic subscription-auth billing notice. Kept
|
|
187
|
+
* character-for-character in sync with `ANTHROPIC_SUBSCRIPTION_AUTH_WARNING` in
|
|
188
|
+
* the SDK's interactive mode so the browser shows the same message the TUI does.
|
|
189
|
+
*/
|
|
190
|
+
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
|
|
191
|
+
/** Mirror of the SDK TUI `isAnthropicSubscriptionAuthKey` (subscription API keys start with `sk-ant-oat`). */
|
|
192
|
+
function isAnthropicSubscriptionAuthKey(apiKey) {
|
|
193
|
+
return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Dismiss id for the Anthropic subscription-auth billing notice. This is `pi`'s
|
|
197
|
+
* own `WarningSettings` key verbatim (`anthropicExtraUsage`): we carry the
|
|
198
|
+
* coupling `pi` already defines rather than inventing a parallel vocabulary, and
|
|
199
|
+
* {@link dismissSessionWarning} maps it back to `setWarnings`.
|
|
200
|
+
*/
|
|
201
|
+
const ANTHROPIC_EXTRA_USAGE_DISMISS_ID = "anthropicExtraUsage";
|
|
202
|
+
/**
|
|
203
|
+
* Port of the TUI `maybeWarnAboutAnthropicSubscriptionAuth` gate/trigger, computed
|
|
204
|
+
* live from the session's current model, stored Anthropic credential, and warning
|
|
205
|
+
* settings. Returns the billing warning when the active provider is `anthropic`
|
|
206
|
+
* and auth is a subscription credential (stored `oauth`, or an `sk-ant-oat` API
|
|
207
|
+
* key), unless suppressed via `getWarnings().anthropicExtraUsage === false`.
|
|
208
|
+
*
|
|
209
|
+
* The stored credential is read synchronously (matching the TUI's `oauth` branch
|
|
210
|
+
* and the documented `sk-ant-oat` key trigger) so warnings stay part of the
|
|
211
|
+
* synchronous live status computation.
|
|
212
|
+
*/
|
|
213
|
+
export function anthropicSubscriptionWarning(session, authPath) {
|
|
214
|
+
if (session.settingsManager.getWarnings().anthropicExtraUsage === false)
|
|
215
|
+
return undefined;
|
|
216
|
+
if (session.model?.provider !== "anthropic")
|
|
217
|
+
return undefined;
|
|
218
|
+
const credential = readStoredCredential("anthropic", authPath);
|
|
219
|
+
if (credential === undefined)
|
|
220
|
+
return undefined;
|
|
221
|
+
const isSubscriptionAuth = credential.type === "oauth"
|
|
222
|
+
? true
|
|
223
|
+
: isAnthropicSubscriptionAuthKey(credential.key);
|
|
224
|
+
if (!isSubscriptionAuth)
|
|
225
|
+
return undefined;
|
|
226
|
+
return {
|
|
227
|
+
severity: "warning",
|
|
228
|
+
message: ANTHROPIC_SUBSCRIPTION_AUTH_WARNING,
|
|
229
|
+
source: "anthropic",
|
|
230
|
+
dismiss: { id: ANTHROPIC_EXTRA_USAGE_DISMISS_ID },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Durably suppress a dismissable session warning by mapping its opaque dismiss
|
|
235
|
+
* id back to the concrete `pi` suppression it represents. Only known ids are
|
|
236
|
+
* honored; unknown ids throw so a stale/forged client cannot silently no-op.
|
|
237
|
+
*
|
|
238
|
+
* This is the single place provider-specific suppression lives: the wire type,
|
|
239
|
+
* parser, and UI stay agnostic. Adding a future dismissable warning is a
|
|
240
|
+
* server-only change here plus a `dismiss` id on its producer.
|
|
241
|
+
*/
|
|
242
|
+
export function dismissSessionWarning(session, dismissId) {
|
|
243
|
+
if (dismissId !== ANTHROPIC_EXTRA_USAGE_DISMISS_ID) {
|
|
244
|
+
throw new Error(`Unknown session warning dismiss id: ${dismissId}`);
|
|
245
|
+
}
|
|
246
|
+
session.settingsManager.setWarnings({ ...session.settingsManager.getWarnings(), anthropicExtraUsage: false });
|
|
247
|
+
}
|
|
84
248
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
85
249
|
if (!(options.sessionManager instanceof SessionManager))
|
|
86
250
|
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
@@ -109,11 +273,11 @@ function createRuntimeWithContextAndOneShotInitialModel(createRuntime, managemen
|
|
|
109
273
|
});
|
|
110
274
|
};
|
|
111
275
|
}
|
|
112
|
-
function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRuntime, resolveSettingsScope, spawn, subsessions) {
|
|
276
|
+
function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRuntime, resolveSettingsScope, spawn, subsessions, askUser) {
|
|
113
277
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel, delegationToolsEnabled = true }) => {
|
|
114
278
|
const scopedModelRuntime = managementContext === undefined ? await normalModelRuntimeForCwd(cwd) : await managementModelRuntime();
|
|
115
279
|
if (managementContext !== undefined) {
|
|
116
|
-
return createManagementRuntimeFactory(scopedModelRuntime, resolveSettingsScope, spawn, subsessions, managementContext)({
|
|
280
|
+
return createManagementRuntimeFactory(scopedModelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext)({
|
|
117
281
|
cwd,
|
|
118
282
|
agentDir,
|
|
119
283
|
sessionManager,
|
|
@@ -128,7 +292,7 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
|
|
|
128
292
|
globalAgentDir: agentDir,
|
|
129
293
|
});
|
|
130
294
|
const services = await createAgentSessionServices({ cwd, agentDir, modelRuntime: scopedModelRuntime, settingsManager });
|
|
131
|
-
const customTools = createPiWebCustomToolDefinitions(cwd, delegationToolsEnabled, spawn, subsessions);
|
|
295
|
+
const customTools = createPiWebCustomToolDefinitions(cwd, delegationToolsEnabled, spawn, subsessions, askUser);
|
|
132
296
|
const result = await createAgentSessionFromServices({
|
|
133
297
|
services,
|
|
134
298
|
sessionManager,
|
|
@@ -139,7 +303,7 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
|
|
|
139
303
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
140
304
|
};
|
|
141
305
|
}
|
|
142
|
-
function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, managementContext) {
|
|
306
|
+
function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext) {
|
|
143
307
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled = true }) => {
|
|
144
308
|
const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
|
|
145
309
|
return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
|
|
@@ -163,6 +327,7 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
|
|
|
163
327
|
createManagedPythonToolDefinition(cwd, managementContext),
|
|
164
328
|
...(delegationToolsEnabled && spawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn }))] : []),
|
|
165
329
|
...(delegationToolsEnabled && subsessions !== undefined ? createSubsessionToolDefinitions(cwd, subsessions).map((tool) => defineTool(tool)) : []),
|
|
330
|
+
...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
|
|
166
331
|
];
|
|
167
332
|
const options = {
|
|
168
333
|
services,
|
|
@@ -177,11 +342,12 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
|
|
|
177
342
|
});
|
|
178
343
|
};
|
|
179
344
|
}
|
|
180
|
-
export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions) {
|
|
345
|
+
export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions, askUser) {
|
|
181
346
|
return [
|
|
182
347
|
...managedAgentToolDefinitions(cwd),
|
|
183
348
|
...(delegationEnabled && spawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn }))] : []),
|
|
184
349
|
...(delegationEnabled && subsessions !== undefined ? createSubsessionToolDefinitions(cwd, subsessions).map((tool) => defineTool(tool)) : []),
|
|
350
|
+
...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
|
|
185
351
|
];
|
|
186
352
|
}
|
|
187
353
|
function managedAgentToolDefinitions(cwd, options) {
|
|
@@ -221,8 +387,18 @@ export class PiSessionService {
|
|
|
221
387
|
constructor(events, deps) {
|
|
222
388
|
this.events = events;
|
|
223
389
|
this.active = new Map();
|
|
390
|
+
this.runtimeBySession = new WeakMap();
|
|
224
391
|
this.pendingSessionOpens = new Map();
|
|
225
|
-
this.activities = new
|
|
392
|
+
this.activities = new SessionActivityCoordinator();
|
|
393
|
+
/** Runtime-identity gate held while Pi may await abandoned-branch summarization. */
|
|
394
|
+
this.treeNavigations = new WeakSet();
|
|
395
|
+
/** Counts async operations that may append an entry before they settle. */
|
|
396
|
+
this.sessionEntryMutationCounts = new WeakMap();
|
|
397
|
+
/** Runtime/session-identity reservations for operations that must not overlap tree navigation. */
|
|
398
|
+
this.treeExclusiveRuntimeOperationCounts = new WeakMap();
|
|
399
|
+
this.treeExclusiveSessionOperationCounts = new Map();
|
|
400
|
+
this.deferredSubsessionNotifications = new WeakMap();
|
|
401
|
+
this.deferredGeneratedSessionNames = new WeakMap();
|
|
226
402
|
this.compactionPromptQueues = new Map();
|
|
227
403
|
this.compactionDrainTimers = new Map();
|
|
228
404
|
this.authLossWarnings = new Set();
|
|
@@ -234,12 +410,32 @@ export class PiSessionService {
|
|
|
234
410
|
this.subsessionLinks = new Map();
|
|
235
411
|
/** Parent id/file identities whose persisted links have already been loaded. */
|
|
236
412
|
this.subsessionHydratedParents = new Set();
|
|
413
|
+
/** Session file path -> its parsed header. Headers are written once, so successful reads are cached. */
|
|
414
|
+
this.sessionHeaderCache = new Map();
|
|
237
415
|
/**
|
|
238
416
|
* Tracked subsession id -> whether a completion notification is armed.
|
|
239
417
|
* Armed when the child starts working; firing on completion disarms it so a
|
|
240
418
|
* child that works again (and stops again) notifies the parent each time.
|
|
241
419
|
*/
|
|
242
420
|
this.subsessionNotifyArmed = new Map();
|
|
421
|
+
this.notificationGenerationBySession = new WeakMap();
|
|
422
|
+
this.pendingUnreadMutations = [];
|
|
423
|
+
this.unreadPublicationFlushRequested = false;
|
|
424
|
+
this.unreadPublicationStopped = false;
|
|
425
|
+
/**
|
|
426
|
+
* Read a session file header, memoized per path. Pi writes the header once at
|
|
427
|
+
* session creation, so a successful read stays valid for the process lifetime;
|
|
428
|
+
* failures are not cached so a session file that appears later is picked up.
|
|
429
|
+
*/
|
|
430
|
+
this.readCachedSessionHeader = async (sessionFile) => {
|
|
431
|
+
const cached = this.sessionHeaderCache.get(sessionFile);
|
|
432
|
+
if (cached !== undefined)
|
|
433
|
+
return cached;
|
|
434
|
+
const header = await readSessionHeaderSummary(sessionFile);
|
|
435
|
+
if (header !== undefined)
|
|
436
|
+
this.sessionHeaderCache.set(sessionFile, header);
|
|
437
|
+
return header;
|
|
438
|
+
};
|
|
243
439
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
244
440
|
this.pinStore = deps.pinStore ?? new SessionPinStore(join(deps.dataDir ?? piWebDataDir(), "session-pins.json"));
|
|
245
441
|
this.agentDir = deps.agentDir ?? getAgentDir();
|
|
@@ -261,8 +457,15 @@ export class PiSessionService {
|
|
|
261
457
|
? resolveDefaultModelRuntime
|
|
262
458
|
: () => Promise.resolve(managementModelRuntime);
|
|
263
459
|
this.spawnTargets = deps.spawnTargets;
|
|
460
|
+
this.projectWorkspaces = deps.projectWorkspaces;
|
|
264
461
|
this.logger = deps.logger ?? noopLogger;
|
|
265
462
|
this.now = deps.now ?? (() => new Date());
|
|
463
|
+
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
464
|
+
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
465
|
+
this.pendingAskStore = deps.pendingAskStore ?? new PendingAskStore();
|
|
466
|
+
this.catalogRefreshStatus = deps.catalogRefreshStatus;
|
|
467
|
+
this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
|
|
468
|
+
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
266
469
|
// Subsessions are a beta capability gated behind their own flag, and they
|
|
267
470
|
// also require the spawn capability (they share its project-scope resolver).
|
|
268
471
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
@@ -271,20 +474,33 @@ export class PiSessionService {
|
|
|
271
474
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
272
475
|
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
273
476
|
read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
|
|
274
|
-
});
|
|
477
|
+
}, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined);
|
|
275
478
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
276
479
|
this.workspaceActivity = deps.workspaceActivity;
|
|
277
480
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
278
481
|
this.commandService = new SessionCommandService((sessionId, eventScope) => this.getActiveForCommand(sessionId, eventScope), (sessionId, text, eventScope) => this.submitCommandPrompt(sessionId, text, eventScope), events, {
|
|
279
482
|
onCompactionStart: (session) => {
|
|
280
|
-
this.
|
|
483
|
+
this.beginSessionEntryMutation(session, "compact the session");
|
|
484
|
+
this.publishActivity(session, "正在压缩上下文", "active");
|
|
281
485
|
this.publishStatus(session);
|
|
282
486
|
},
|
|
283
487
|
onCompactionEnd: (session, result, detail) => {
|
|
284
|
-
this.
|
|
488
|
+
this.endSessionEntryMutation(session);
|
|
489
|
+
this.publishActivity(session, result === "success" ? "上下文压缩完成" : "上下文压缩失败", result === "success" ? "idle" : "error", detail);
|
|
285
490
|
this.publishStatus(session);
|
|
286
491
|
},
|
|
287
492
|
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
493
|
+
getSessionTree: (session) => {
|
|
494
|
+
if (typeof session.sessionManager.getTree !== "function" || typeof session.navigateTree !== "function")
|
|
495
|
+
return undefined;
|
|
496
|
+
return projectSessionTree(session.sessionManager.getTree(), session.sessionManager.getLeafId());
|
|
497
|
+
},
|
|
498
|
+
hasActiveWork: (session) => this.hasActiveWork(session),
|
|
499
|
+
isTreeNavigationActive: (session) => this.treeNavigations.has(session),
|
|
500
|
+
runSessionReplacement: (session, operation) => {
|
|
501
|
+
const runtime = this.activeRuntimeForSession(session);
|
|
502
|
+
return this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, ...(runtime === undefined ? {} : { runtime }) }], "Stop current session activity before replacing the session", operation);
|
|
503
|
+
},
|
|
288
504
|
}, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
|
|
289
505
|
}
|
|
290
506
|
activeCount() {
|
|
@@ -316,22 +532,24 @@ export class PiSessionService {
|
|
|
316
532
|
skippedBusySessionIds.add(input.sessionId);
|
|
317
533
|
continue;
|
|
318
534
|
}
|
|
319
|
-
await this.closeActive(input.sessionId);
|
|
535
|
+
await this.closeActive(input.sessionId, { kind: "clear", reason: "archive" });
|
|
320
536
|
readyArchiveInputs.push(input);
|
|
321
537
|
}
|
|
322
538
|
await this.archiveStoreArchiveMany(readyArchiveInputs);
|
|
323
539
|
archiveInputs.push(...readyArchiveInputs);
|
|
540
|
+
await this.forgetUnreadSessions(readyArchiveInputs);
|
|
324
541
|
for (const record of plan.deleteRecords) {
|
|
325
542
|
if (this.activeSessionHasWork(record.sessionId)) {
|
|
326
543
|
skippedBusySessionIds.add(record.sessionId);
|
|
327
544
|
continue;
|
|
328
545
|
}
|
|
329
|
-
await this.closeActive(record.sessionId);
|
|
546
|
+
await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
|
|
330
547
|
readyDeleteRecords.push(record);
|
|
331
548
|
}
|
|
332
549
|
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
333
550
|
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
334
551
|
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
552
|
+
await this.forgetUnreadSessions(deleteRecords);
|
|
335
553
|
return summarizeSessionCleanupExecution({
|
|
336
554
|
archiveInputs,
|
|
337
555
|
deleteRecords,
|
|
@@ -341,12 +559,18 @@ export class PiSessionService {
|
|
|
341
559
|
});
|
|
342
560
|
}
|
|
343
561
|
async dispose() {
|
|
562
|
+
this.unreadPublicationStopped = true;
|
|
563
|
+
this.clearUnreadPublicationRetry();
|
|
344
564
|
clearInterval(this.heartbeat);
|
|
345
565
|
this.clearCompactionDrainTimers();
|
|
346
566
|
const pendingOpens = this.pendingSessionOpenPromises();
|
|
347
567
|
if (pendingOpens.length > 0)
|
|
348
568
|
await Promise.allSettled(pendingOpens);
|
|
349
569
|
const activeSessions = Array.from(new Set(this.active.values()));
|
|
570
|
+
for (const active of activeSessions) {
|
|
571
|
+
this.forgetUnreadActivity(active.runtime.session);
|
|
572
|
+
this.pendingAskStore.forgetSession(active.runtime.session.sessionId);
|
|
573
|
+
}
|
|
350
574
|
this.active.clear();
|
|
351
575
|
this.pendingSessionOpens.clear();
|
|
352
576
|
this.activities.clear();
|
|
@@ -357,12 +581,15 @@ export class PiSessionService {
|
|
|
357
581
|
this.subsessionLinks.clear();
|
|
358
582
|
this.subsessionHydratedParents.clear();
|
|
359
583
|
this.subsessionNotifyArmed.clear();
|
|
584
|
+
this.sessionHeaderCache.clear();
|
|
585
|
+
this.notificationStore.clearAll("service-dispose");
|
|
360
586
|
await Promise.all(activeSessions.map(async (active) => {
|
|
361
587
|
active.unsubscribe();
|
|
362
588
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
|
|
363
589
|
await active.runtime.session.abort();
|
|
364
590
|
await active.runtime.dispose();
|
|
365
591
|
}));
|
|
592
|
+
await this.publishUnreadMutations([]);
|
|
366
593
|
}
|
|
367
594
|
async list(cwd, managementContext) {
|
|
368
595
|
return (await this.listWorkspaceSessions(cwd, managementContext)).sessions;
|
|
@@ -371,15 +598,26 @@ export class PiSessionService {
|
|
|
371
598
|
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
372
599
|
if (normalizedQuery === "")
|
|
373
600
|
return this.list(cwd, managementContext);
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return
|
|
381
|
-
|
|
382
|
-
|
|
601
|
+
const { sessions } = await this.searchCandidates(cwd, normalizedQuery, managementContext);
|
|
602
|
+
return sessions;
|
|
603
|
+
}
|
|
604
|
+
async searchContent(cwd, query, managementContext) {
|
|
605
|
+
const normalizedQuery = query.trim();
|
|
606
|
+
if (normalizedQuery === "")
|
|
607
|
+
return { results: [], matchCount: 0, truncated: false };
|
|
608
|
+
const maxMatches = 200;
|
|
609
|
+
const { listing, sessions } = await this.searchCandidates(cwd, normalizedQuery.toLocaleLowerCase(), managementContext);
|
|
610
|
+
const results = [];
|
|
611
|
+
let matchCount = 0;
|
|
612
|
+
for (const session of sessions) {
|
|
613
|
+
const remaining = maxMatches - results.reduce((count, result) => count + result.matches.length, 0);
|
|
614
|
+
const messages = this.searchableSessionHistory(session, listing, managementContext);
|
|
615
|
+
const view = searchSessionContent(messages, normalizedQuery, Math.max(1, remaining));
|
|
616
|
+
matchCount += view.matchCount;
|
|
617
|
+
if (remaining > 0 && view.matches.length > 0)
|
|
618
|
+
results.push({ session, matches: view.matches.slice(0, remaining) });
|
|
619
|
+
}
|
|
620
|
+
return { results, matchCount, truncated: matchCount > maxMatches };
|
|
383
621
|
}
|
|
384
622
|
async listPinned(cwd, managementContext) {
|
|
385
623
|
const listing = await this.listWorkspaceSessions(cwd, managementContext);
|
|
@@ -408,18 +646,81 @@ export class PiSessionService {
|
|
|
408
646
|
.filter((record) => record.cwd === cwd)
|
|
409
647
|
.map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
|
|
410
648
|
const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
|
|
649
|
+
for (const record of archivedForCwd) {
|
|
650
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(record.sessionId, "archive-reconcile"));
|
|
651
|
+
}
|
|
411
652
|
const unarchivedSessions = sessions.filter((session) => !archivedById.has(session.id)).map(clientSessionFromListEntry);
|
|
412
|
-
|
|
653
|
+
const reconcilableSessionIds = this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById);
|
|
654
|
+
this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds, eventScopeFromManagementContext(managementContext));
|
|
655
|
+
if (managementContext === undefined) {
|
|
656
|
+
await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
|
|
657
|
+
}
|
|
413
658
|
const archivedSessions = archivedForCwd
|
|
414
659
|
.sort(compareArchivedRecords)
|
|
415
660
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
416
661
|
.filter(isDefined);
|
|
417
662
|
return {
|
|
418
|
-
sessions: [...unarchivedSessions, ...archivedSessions],
|
|
663
|
+
sessions: await this.withRelatedSessionsElsewhere([...unarchivedSessions, ...archivedSessions], cwd),
|
|
419
664
|
entriesById: new Map(sessions.map((session) => [session.id, session])),
|
|
420
665
|
archivedById,
|
|
421
666
|
};
|
|
422
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* Annotate a listing with the session relationships that cross workspace
|
|
670
|
+
* boundaries: where an out-of-listing parent lives, and how many children a
|
|
671
|
+
* listed session has in sibling workspaces.
|
|
672
|
+
*
|
|
673
|
+
* Both directions are best-effort. An unreadable parent header or a sibling
|
|
674
|
+
* workspace that cannot be listed leaves the session unannotated rather than
|
|
675
|
+
* failing the listing, and the browser falls back to its generic states.
|
|
676
|
+
*/
|
|
677
|
+
async withRelatedSessionsElsewhere(sessions, cwd) {
|
|
678
|
+
const [parentLocations, childCounts] = await Promise.all([
|
|
679
|
+
locateOutOfListingParents(sessions, cwd, this.readCachedSessionHeader),
|
|
680
|
+
this.countChildrenInSiblingWorkspaces(sessions, cwd),
|
|
681
|
+
]);
|
|
682
|
+
return sessions.map((session) => {
|
|
683
|
+
const parent = session.parentSessionPath === undefined ? undefined : parentLocations.get(session.parentSessionPath);
|
|
684
|
+
const childrenElsewhere = childCounts.get(session.path);
|
|
685
|
+
if (parent === undefined && childrenElsewhere === undefined)
|
|
686
|
+
return session;
|
|
687
|
+
const annotated = { ...session };
|
|
688
|
+
if (parent !== undefined) {
|
|
689
|
+
annotated.parentSessionId = parent.parentSessionId;
|
|
690
|
+
annotated.parentSessionCwd = parent.parentSessionCwd;
|
|
691
|
+
}
|
|
692
|
+
if (childrenElsewhere !== undefined)
|
|
693
|
+
annotated.childSessionsElsewhere = childrenElsewhere;
|
|
694
|
+
return annotated;
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Count children of the listed sessions that live in other workspaces of the
|
|
699
|
+
* same project.
|
|
700
|
+
*
|
|
701
|
+
* Only sibling workspaces are scanned: agents may only spawn into workspaces
|
|
702
|
+
* of the spawning session's own project, so that bounds where a child can be.
|
|
703
|
+
* Listing is skipped entirely when no project-workspace locator is configured
|
|
704
|
+
* or the cwd belongs to no registered project.
|
|
705
|
+
*/
|
|
706
|
+
async countChildrenInSiblingWorkspaces(sessions, cwd) {
|
|
707
|
+
if (this.projectWorkspaces === undefined || sessions.length === 0)
|
|
708
|
+
return new Map();
|
|
709
|
+
try {
|
|
710
|
+
const siblingCwds = await siblingWorkspaceCwds(this.projectWorkspaces, cwd);
|
|
711
|
+
if (siblingCwds.length === 0)
|
|
712
|
+
return new Map();
|
|
713
|
+
const listings = await Promise.all(siblingCwds.map(async (siblingCwd) => {
|
|
714
|
+
const entries = await this.sessionManager.list(siblingCwd);
|
|
715
|
+
return entries.flatMap((entry) => entry.parentSessionPath === undefined ? [] : [entry.parentSessionPath]);
|
|
716
|
+
}));
|
|
717
|
+
return countOutOfListingChildren(sessions, listings.flat());
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
this.logger.info({ cwd, error: error instanceof Error ? error.message : String(error) }, "failed to count child sessions in sibling workspaces");
|
|
721
|
+
return new Map();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
423
724
|
async start(cwd, options = {}) {
|
|
424
725
|
if (options.managementContext === undefined)
|
|
425
726
|
await this.normalModelRuntimeForCwd(cwd);
|
|
@@ -430,6 +731,8 @@ export class PiSessionService {
|
|
|
430
731
|
...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
|
|
431
732
|
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
432
733
|
...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
|
|
734
|
+
...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
|
|
735
|
+
startupIntent: "create",
|
|
433
736
|
});
|
|
434
737
|
const { session } = active.runtime;
|
|
435
738
|
const created = {
|
|
@@ -491,13 +794,100 @@ export class PiSessionService {
|
|
|
491
794
|
...(parentSessionFile === undefined ? {} : { parentSessionFile }),
|
|
492
795
|
cwd: decision.cwd,
|
|
493
796
|
};
|
|
494
|
-
this.registerVerifiedSubsession(link);
|
|
797
|
+
await this.registerVerifiedSubsession(link);
|
|
495
798
|
this.persistSubsessionLink(link);
|
|
496
799
|
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
497
800
|
await this.prompt(created.id, input.prompt);
|
|
498
801
|
this.logger.info({ parentSessionId: input.parentSessionId, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_subsession started a tracked child session");
|
|
499
802
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
500
803
|
}
|
|
804
|
+
/**
|
|
805
|
+
* Register the question set an agent wants the user to answer as the session's
|
|
806
|
+
* open ask. Deliberately does not wait for the user: `ask_user` terminates the
|
|
807
|
+
* run and the submitted answers come back later as a follow-up message.
|
|
808
|
+
*
|
|
809
|
+
* Rejected question sets throw {@link PendingAskValidationError}, which the
|
|
810
|
+
* agent loop reports to the model as an error tool result.
|
|
811
|
+
*/
|
|
812
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- async so a rejected question set becomes a rejection rather than a synchronous throw from a promise-returning method.
|
|
813
|
+
async openAsk(input) {
|
|
814
|
+
const result = this.pendingAskStore.open(input);
|
|
815
|
+
// A supersede closes the earlier ask, so the browsers watching it must hear
|
|
816
|
+
// that before they hear about its replacement.
|
|
817
|
+
if (result.superseded !== undefined)
|
|
818
|
+
this.publishAskClosed(input.sessionId, result.superseded);
|
|
819
|
+
this.events.publish(input.sessionId, { type: "ask.opened", ask: result.ask });
|
|
820
|
+
this.publishStatusForSessionId(input.sessionId);
|
|
821
|
+
return result;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Record the user's answers to the session's open ask and hand them to the
|
|
825
|
+
* model. The answers travel as a system-authored custom message rather than a
|
|
826
|
+
* user message, so they are not attributed to the human in the transcript;
|
|
827
|
+
* they still wake an idle session (`triggerTurn`) and queue behind in-flight
|
|
828
|
+
* work (`deliverAs: "followUp"`), which is how the run that `ask_user`
|
|
829
|
+
* terminated continues.
|
|
830
|
+
*/
|
|
831
|
+
async submitAsk(ref, askId, submission) {
|
|
832
|
+
await this.assertWritable(ref);
|
|
833
|
+
const session = await this.getOrOpen(ref);
|
|
834
|
+
// Checked before the store closes the ask so a refused delivery cannot
|
|
835
|
+
// discard answers the user already submitted.
|
|
836
|
+
this.assertTreeNavigationInactive(session, "answer questions");
|
|
837
|
+
return this.closeAsk(session, this.pendingAskStore.submit(session.sessionId, askId, submission));
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Close the open ask without answers. The model is still told, naming every
|
|
841
|
+
* question as unanswered: it was promised a follow-up message and would
|
|
842
|
+
* otherwise wait for one that never comes.
|
|
843
|
+
*/
|
|
844
|
+
async cancelAsk(ref, askId) {
|
|
845
|
+
await this.assertWritable(ref);
|
|
846
|
+
const session = await this.getOrOpen(ref);
|
|
847
|
+
this.assertTreeNavigationInactive(session, "dismiss questions");
|
|
848
|
+
return this.closeAsk(session, this.pendingAskStore.cancel(session.sessionId, askId));
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Publish and deliver a closed ask. A stale close is reported rather than
|
|
852
|
+
* thrown: losing the race against a supersede, another browser, or a session
|
|
853
|
+
* that went away is ordinary, and the returned status tells the browser what
|
|
854
|
+
* the session's open ask is now.
|
|
855
|
+
*/
|
|
856
|
+
async closeAsk(session, result) {
|
|
857
|
+
if (result.status === "stale")
|
|
858
|
+
return { result: "stale", sessionStatus: this.statusFromSession(session) };
|
|
859
|
+
const { outcome } = result;
|
|
860
|
+
this.publishAskClosed(session.sessionId, outcome);
|
|
861
|
+
await this.runSessionEntryMutation(session, "deliver answers to your questions", () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: true, deliverAs: "followUp" }));
|
|
862
|
+
this.publishStatus(session);
|
|
863
|
+
return { result: "closed", outcome, sessionStatus: this.statusFromSession(session) };
|
|
864
|
+
}
|
|
865
|
+
publishAskClosed(sessionId, outcome) {
|
|
866
|
+
this.events.publish(sessionId, { type: "ask.closed", askId: outcome.askId, reason: outcome.reason });
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Void the session's open ask because the user sent a chat message instead of
|
|
870
|
+
* answering it. Every browser closes the card as cancelled, and the model is
|
|
871
|
+
* told — without being woken — so the notice rides into the turn the message
|
|
872
|
+
* itself triggers rather than becoming a turn of its own.
|
|
873
|
+
*/
|
|
874
|
+
async voidOpenAskForUserMessage(session) {
|
|
875
|
+
const outcome = this.pendingAskStore.cancelOpen(session.sessionId);
|
|
876
|
+
if (outcome === undefined)
|
|
877
|
+
return;
|
|
878
|
+
this.publishAskClosed(session.sessionId, outcome);
|
|
879
|
+
await this.runSessionEntryMutation(session, "void the open questions", () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: false, deliverAs: "followUp" }));
|
|
880
|
+
this.publishStatus(session);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Publish status for a session known only by id, as the ask tools are: they
|
|
884
|
+
* run inside the session's own runtime, so the active entry is the session.
|
|
885
|
+
*/
|
|
886
|
+
publishStatusForSessionId(sessionId) {
|
|
887
|
+
const session = this.active.get(sessionId)?.runtime.session;
|
|
888
|
+
if (session !== undefined)
|
|
889
|
+
this.publishStatus(session);
|
|
890
|
+
}
|
|
501
891
|
/** Summaries of the tracked subsessions spawned by `parentSessionId`. */
|
|
502
892
|
async listSubsessions(parentSessionId, parentSessionFile) {
|
|
503
893
|
const parentFile = nonEmptyString(parentSessionFile);
|
|
@@ -560,7 +950,7 @@ export class PiSessionService {
|
|
|
560
950
|
return undefined;
|
|
561
951
|
return sessionFileMatches(session, link.childSessionFile) ? link : undefined;
|
|
562
952
|
}
|
|
563
|
-
registerVerifiedSubsession(link) {
|
|
953
|
+
async registerVerifiedSubsession(link) {
|
|
564
954
|
const { childSessionId, parentSessionId } = link;
|
|
565
955
|
const previousParentId = this.subsessionParents.get(childSessionId);
|
|
566
956
|
if (previousParentId !== undefined && previousParentId !== parentSessionId) {
|
|
@@ -576,6 +966,24 @@ export class PiSessionService {
|
|
|
576
966
|
this.subsessionLinks.set(childSessionId, link);
|
|
577
967
|
if (!this.subsessionNotifyArmed.has(childSessionId))
|
|
578
968
|
this.subsessionNotifyArmed.set(childSessionId, false);
|
|
969
|
+
const cwd = this.cwdForVerifiedSubsession(link);
|
|
970
|
+
await this.publishUnreadMutations(this.unreadStore.excludeSession(childSessionId, cwd));
|
|
971
|
+
}
|
|
972
|
+
cwdForVerifiedSubsession(link) {
|
|
973
|
+
const activeCwd = this.activeChildForSubsessionLink(link)?.runtime.session.sessionManager.getCwd();
|
|
974
|
+
const linkedCwd = nonEmptyString(activeCwd) ?? nonEmptyString(link.cwd);
|
|
975
|
+
if (linkedCwd !== undefined)
|
|
976
|
+
return canonicalizeStoredCwd(linkedCwd);
|
|
977
|
+
const childSessionFile = link.childSessionFile;
|
|
978
|
+
if (childSessionFile !== undefined) {
|
|
979
|
+
try {
|
|
980
|
+
return canonicalizeStoredCwd(this.sessionManager.open(childSessionFile).getCwd());
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
throw new Error("Could not resolve cwd for verified tracked sub-session", { cause: error });
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
throw new Error("Could not resolve cwd for verified tracked sub-session");
|
|
579
987
|
}
|
|
580
988
|
unregisterSubsession(childSessionId) {
|
|
581
989
|
const parentSessionId = this.subsessionParents.get(childSessionId);
|
|
@@ -622,40 +1030,44 @@ export class PiSessionService {
|
|
|
622
1030
|
const activeParent = this.activeForSessionId(parentSessionId);
|
|
623
1031
|
if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
|
|
624
1032
|
const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
|
|
625
|
-
await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
626
|
-
|
|
1033
|
+
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
1034
|
+
if (complete)
|
|
1035
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
627
1036
|
return;
|
|
628
1037
|
}
|
|
629
1038
|
if (parentSessionFile === undefined)
|
|
630
1039
|
return;
|
|
631
|
-
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId)
|
|
632
|
-
this.subsessionHydratedParents.add(hydrationKey);
|
|
1040
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId)
|
|
633
1041
|
return;
|
|
634
|
-
}
|
|
635
1042
|
let parentManager;
|
|
636
1043
|
try {
|
|
637
1044
|
parentManager = this.sessionManager.open(parentSessionFile);
|
|
638
1045
|
}
|
|
639
1046
|
catch {
|
|
640
|
-
this.subsessionHydratedParents.add(hydrationKey);
|
|
641
1047
|
return;
|
|
642
1048
|
}
|
|
643
|
-
await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
|
|
644
|
-
|
|
1049
|
+
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
|
|
1050
|
+
if (complete)
|
|
1051
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
645
1052
|
}
|
|
646
1053
|
async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile) {
|
|
647
1054
|
// Parent custom links are the authoritative recovery record: verify the
|
|
648
|
-
// exact live child file/header before tracking.
|
|
1055
|
+
// exact live child file/header before tracking. Do not negatively cache a
|
|
1056
|
+
// scan while a candidate child is temporarily unavailable.
|
|
649
1057
|
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
1058
|
+
let complete = true;
|
|
650
1059
|
for (const entry of entries) {
|
|
651
1060
|
const link = parsePersistedParentSubsessionLink(entry);
|
|
652
|
-
if (link
|
|
1061
|
+
if (link?.spawnedBySessionId !== parentSessionId)
|
|
653
1062
|
continue;
|
|
654
1063
|
const verified = await this.verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link);
|
|
655
|
-
if (verified === undefined)
|
|
1064
|
+
if (verified === undefined) {
|
|
1065
|
+
complete = false;
|
|
656
1066
|
continue;
|
|
657
|
-
|
|
1067
|
+
}
|
|
1068
|
+
await this.registerVerifiedSubsession(verified);
|
|
658
1069
|
}
|
|
1070
|
+
return complete;
|
|
659
1071
|
}
|
|
660
1072
|
async verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link) {
|
|
661
1073
|
if (parentSessionFile === undefined)
|
|
@@ -674,7 +1086,7 @@ export class PiSessionService {
|
|
|
674
1086
|
const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
|
|
675
1087
|
if (link === undefined)
|
|
676
1088
|
return;
|
|
677
|
-
this.registerVerifiedSubsession(link);
|
|
1089
|
+
await this.registerVerifiedSubsession(link);
|
|
678
1090
|
}
|
|
679
1091
|
verifiedSubsessionLinkFromOpenedChild(session) {
|
|
680
1092
|
return verifiedTrackedSubsessionLink(this.sessionManager, {
|
|
@@ -749,12 +1161,12 @@ export class PiSessionService {
|
|
|
749
1161
|
this.subsessionNotifyArmed.set(childId, false);
|
|
750
1162
|
const status = this.activityForSession(session)?.phase === "error" ? "error" : "idle";
|
|
751
1163
|
const finalText = finalAssistantText(historyMessages(session));
|
|
752
|
-
const
|
|
1164
|
+
const outputSection = formatSubsessionNotificationOutput(childId, finalText);
|
|
753
1165
|
const workingIds = this.workingSubsessionIds(link.parentSessionId);
|
|
754
1166
|
const next = workingIds.length === 0
|
|
755
1167
|
? "No other tracked subsessions are working."
|
|
756
1168
|
: `Still working: ${workingIds.join(", ")}. Continue working, or call yield_to_subsessions alone and last at the next join point. Further completion notices arrive automatically; do not poll.`;
|
|
757
|
-
const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n
|
|
1169
|
+
const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n${outputSection}`;
|
|
758
1170
|
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
|
759
1171
|
}
|
|
760
1172
|
async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
|
|
@@ -783,11 +1195,16 @@ export class PiSessionService {
|
|
|
783
1195
|
async notifyParentOfSubsession(parentId, childId, text) {
|
|
784
1196
|
try {
|
|
785
1197
|
const session = await this.getOrOpenParentForSubsession(parentId, childId);
|
|
786
|
-
|
|
787
|
-
|
|
1198
|
+
if (this.treeNavigations.has(session)) {
|
|
1199
|
+
const pending = this.deferredSubsessionNotifications.get(session) ?? [];
|
|
1200
|
+
pending.push({ parentId, childId, text });
|
|
1201
|
+
this.deferredSubsessionNotifications.set(session, pending);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
await this.deliverSubsessionNotification(session, { parentId, childId, text });
|
|
788
1205
|
}
|
|
789
1206
|
catch (error) {
|
|
790
|
-
this.
|
|
1207
|
+
this.logSubsessionNotificationFailure(parentId, childId, error);
|
|
791
1208
|
}
|
|
792
1209
|
}
|
|
793
1210
|
async messages(ref, page, managementContext) {
|
|
@@ -797,9 +1214,38 @@ export class PiSessionService {
|
|
|
797
1214
|
async status(ref, managementContext) {
|
|
798
1215
|
return this.statusFromSession(await this.getOrOpen(ref, managementContext));
|
|
799
1216
|
}
|
|
1217
|
+
async searchCandidates(cwd, normalizedQuery, managementContext) {
|
|
1218
|
+
const listing = await this.listWorkspaceSessions(cwd, managementContext);
|
|
1219
|
+
const matches = await Promise.all(listing.sessions.map(async (session) => {
|
|
1220
|
+
const entry = listing.entriesById.get(session.id);
|
|
1221
|
+
const archived = listing.archivedById.get(session.id);
|
|
1222
|
+
const archivedText = archived?.archivePath === undefined ? "" : await readArchivedSessionText(archived.archivePath);
|
|
1223
|
+
const haystack = [session.id, session.name ?? "", session.firstMessage, entry?.allMessagesText ?? "", archivedText].join("\n").toLocaleLowerCase();
|
|
1224
|
+
return haystack.includes(normalizedQuery) ? session : undefined;
|
|
1225
|
+
}));
|
|
1226
|
+
return { listing, sessions: matches.filter(isDefined) };
|
|
1227
|
+
}
|
|
1228
|
+
searchableSessionHistory(session, listing, managementContext) {
|
|
1229
|
+
const eventScope = eventScopeFromManagementContext(managementContext);
|
|
1230
|
+
const active = this.activeForSessionIdAndScope(session.id, eventScope);
|
|
1231
|
+
if (active !== undefined && cwdPathsEqual(active.runtime.cwd, session.cwd))
|
|
1232
|
+
return historyMessages(active.runtime.session);
|
|
1233
|
+
const path = listing.archivedById.get(session.id)?.archivePath ?? listing.entriesById.get(session.id)?.path;
|
|
1234
|
+
return path === undefined ? [] : historyMessagesFromManager(this.sessionManager.open(path));
|
|
1235
|
+
}
|
|
1236
|
+
async streamSnapshot(ref, managementContext) {
|
|
1237
|
+
const session = await this.getOrOpen(ref, managementContext);
|
|
1238
|
+
const eventScope = this.eventScopeForSession(session);
|
|
1239
|
+
const seq = this.events.currentSeq(session.sessionId, eventScope);
|
|
1240
|
+
const streamingMessage = session.state.streamingMessage;
|
|
1241
|
+
const partial = streamingMessage === undefined || streamingMessage === null
|
|
1242
|
+
? null
|
|
1243
|
+
: projectBrowserMessage(streamingMessage);
|
|
1244
|
+
return { seq, partial };
|
|
1245
|
+
}
|
|
800
1246
|
async availableModels(ref, managementContext) {
|
|
801
1247
|
const session = await this.getOrOpen(ref, managementContext);
|
|
802
|
-
await session.modelRuntime.
|
|
1248
|
+
await session.modelRuntime.refresh();
|
|
803
1249
|
const models = session.scopedModels.length > 0
|
|
804
1250
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
805
1251
|
: await session.modelRuntime.getAvailable();
|
|
@@ -808,7 +1254,7 @@ export class PiSessionService {
|
|
|
808
1254
|
async setModel(ref, provider, modelId, managementContext) {
|
|
809
1255
|
await this.assertWritable(ref);
|
|
810
1256
|
const session = await this.getOrOpen(ref, managementContext);
|
|
811
|
-
await session.modelRuntime.
|
|
1257
|
+
await session.modelRuntime.refresh();
|
|
812
1258
|
const candidates = session.scopedModels.length > 0
|
|
813
1259
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
814
1260
|
: await session.modelRuntime.getAvailable();
|
|
@@ -816,18 +1262,18 @@ export class PiSessionService {
|
|
|
816
1262
|
?? session.modelRuntime.getModel(provider, modelId);
|
|
817
1263
|
if (model === undefined)
|
|
818
1264
|
throw new Error(`未找到模型:${provider}/${modelId}`);
|
|
819
|
-
await session.setModel(model);
|
|
820
|
-
this.publishActivity(session,
|
|
1265
|
+
await this.runSessionEntryMutation(session, "change models", () => session.setModel(model));
|
|
1266
|
+
this.publishActivity(session, `模型:${model.id}`, "idle", model.provider);
|
|
821
1267
|
this.publishStatus(session);
|
|
822
1268
|
return this.statusFromSession(session);
|
|
823
1269
|
}
|
|
824
1270
|
async cycleModel(ref, direction, managementContext) {
|
|
825
1271
|
await this.assertWritable(ref);
|
|
826
1272
|
const session = await this.getOrOpen(ref, managementContext);
|
|
827
|
-
const result = await session.cycleModel(direction);
|
|
1273
|
+
const result = await this.runSessionEntryMutation(session, "change models", () => session.cycleModel(direction));
|
|
828
1274
|
if (result === undefined)
|
|
829
1275
|
throw new Error(session.scopedModels.length > 0 ? "作用域内只有一个模型" : "只有一个可用模型");
|
|
830
|
-
this.publishActivity(session,
|
|
1276
|
+
this.publishActivity(session, `模型:${result.model.id}`, "idle", result.model.provider);
|
|
831
1277
|
this.publishStatus(session);
|
|
832
1278
|
return this.statusFromSession(session);
|
|
833
1279
|
}
|
|
@@ -838,24 +1284,28 @@ export class PiSessionService {
|
|
|
838
1284
|
async setThinkingLevel(ref, level, managementContext) {
|
|
839
1285
|
await this.assertWritable(ref);
|
|
840
1286
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1287
|
+
this.assertTreeNavigationInactive(session, "change the thinking level");
|
|
841
1288
|
// pi owns the valid set; validate against the session's live levels rather
|
|
842
1289
|
// than a hardcoded union so this stays correct if pi changes the set.
|
|
843
1290
|
const available = session.getAvailableThinkingLevels();
|
|
844
1291
|
const match = available.find((candidate) => candidate === level);
|
|
845
1292
|
if (match === undefined)
|
|
846
1293
|
throw new Error(`无效的思考级别:${level}`);
|
|
847
|
-
|
|
848
|
-
|
|
1294
|
+
await this.runSessionEntryMutation(session, "change the thinking level", () => {
|
|
1295
|
+
session.setThinkingLevel(match);
|
|
1296
|
+
return Promise.resolve();
|
|
1297
|
+
});
|
|
1298
|
+
this.publishActivity(session, `思考级别:${session.thinkingLevel}`, "idle");
|
|
849
1299
|
this.publishStatus(session);
|
|
850
1300
|
return this.statusFromSession(session);
|
|
851
1301
|
}
|
|
852
1302
|
async cycleThinkingLevel(ref, managementContext) {
|
|
853
1303
|
await this.assertWritable(ref);
|
|
854
1304
|
const session = await this.getOrOpen(ref, managementContext);
|
|
855
|
-
const level = session.cycleThinkingLevel();
|
|
1305
|
+
const level = await this.runSessionEntryMutation(session, "change the thinking level", () => Promise.resolve(session.cycleThinkingLevel()));
|
|
856
1306
|
if (level === undefined)
|
|
857
1307
|
throw new Error("当前模型不支持思考");
|
|
858
|
-
this.publishActivity(session,
|
|
1308
|
+
this.publishActivity(session, `思考级别:${level}`, "idle");
|
|
859
1309
|
this.publishStatus(session);
|
|
860
1310
|
return this.statusFromSession(session);
|
|
861
1311
|
}
|
|
@@ -885,14 +1335,20 @@ export class PiSessionService {
|
|
|
885
1335
|
const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
|
|
886
1336
|
await this.assertWritable(ref);
|
|
887
1337
|
const session = await this.getOrOpen(ref, options?.managementContext);
|
|
1338
|
+
this.assertTreeNavigationInactive(session, "send a prompt");
|
|
888
1339
|
this.maybeGenerateSessionName(session, promptText);
|
|
889
1340
|
const isQueued = session.isStreaming || session.isCompacting;
|
|
890
1341
|
const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
|
|
891
1342
|
if (isQueued && images.length === 0 && this.hasQueuedMessageText(session, promptText)) {
|
|
892
|
-
this.publishActivity(session, "
|
|
1343
|
+
this.publishActivity(session, "已忽略重复的排队消息", "active");
|
|
893
1344
|
this.publishStatus(session);
|
|
894
1345
|
return;
|
|
895
1346
|
}
|
|
1347
|
+
// A chat message answers the session's open ask in the user's own words, so
|
|
1348
|
+
// the form is void: keeping it open would invite answers to questions the
|
|
1349
|
+
// conversation has already moved past. Ignored duplicates skip this on
|
|
1350
|
+
// purpose: they must not void an ask posted after the queued original.
|
|
1351
|
+
await this.voidOpenAskForUserMessage(session);
|
|
896
1352
|
if (session.isCompacting) {
|
|
897
1353
|
this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp", images, echoUserMessage);
|
|
898
1354
|
return;
|
|
@@ -900,14 +1356,14 @@ export class PiSessionService {
|
|
|
900
1356
|
void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
|
|
901
1357
|
}
|
|
902
1358
|
submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
|
|
903
|
-
this.publishActivity(session, behavior === "steer" ? "
|
|
1359
|
+
this.publishActivity(session, behavior === "steer" ? "插队消息已排队" : behavior === "followUp" ? "消息已排队" : "消息已接收", "active");
|
|
904
1360
|
const eventScope = this.eventScopeForSession(session);
|
|
905
1361
|
if (behavior === undefined && echoUserMessage)
|
|
906
1362
|
this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) }, eventScope);
|
|
907
1363
|
const promptOptions = buildPromptOptions(behavior, images);
|
|
908
|
-
const promptPromise = session.prompt(text, promptOptions).catch((error) => {
|
|
1364
|
+
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions)).catch((error) => {
|
|
909
1365
|
const message = error instanceof Error ? error.message : String(error);
|
|
910
|
-
this.publishActivity(session, "
|
|
1366
|
+
this.publishActivity(session, "发生错误", "error", message);
|
|
911
1367
|
this.events.publish(session.sessionId, { type: "session.error", message }, eventScope);
|
|
912
1368
|
});
|
|
913
1369
|
void promptPromise;
|
|
@@ -917,7 +1373,7 @@ export class PiSessionService {
|
|
|
917
1373
|
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
918
1374
|
queue.push({ kind, text, ...(images.length > 0 ? { images } : {}), ...(echoUserMessage ? {} : { echoUserMessage: false }) });
|
|
919
1375
|
this.compactionPromptQueues.set(session.sessionId, queue);
|
|
920
|
-
this.publishActivity(session, "
|
|
1376
|
+
this.publishActivity(session, "压缩期间消息已排队", "active");
|
|
921
1377
|
this.publishStatus(session);
|
|
922
1378
|
}
|
|
923
1379
|
async saveAttachments(ref, attachments, folder, managementContext) {
|
|
@@ -932,6 +1388,7 @@ export class PiSessionService {
|
|
|
932
1388
|
await this.assertWritable(ref);
|
|
933
1389
|
const active = await this.getActive(ref, managementContext);
|
|
934
1390
|
const { session } = active.runtime;
|
|
1391
|
+
this.assertTreeNavigationInactive(session, "run a shell command");
|
|
935
1392
|
const isExcluded = text.startsWith("!!");
|
|
936
1393
|
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
|
937
1394
|
if (!command)
|
|
@@ -939,13 +1396,13 @@ export class PiSessionService {
|
|
|
939
1396
|
if (session.isBashRunning)
|
|
940
1397
|
throw new Error("已有 bash 命令正在运行");
|
|
941
1398
|
const eventScope = active.eventScope;
|
|
942
|
-
this.publishActivity(session, "
|
|
1399
|
+
this.publishActivity(session, "正在运行命令", "active", command);
|
|
943
1400
|
this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded }, eventScope);
|
|
944
|
-
void session.executeBash(command, (chunk) => {
|
|
1401
|
+
void this.runSessionEntryMutation(session, "run a shell command", () => session.executeBash(command, (chunk) => {
|
|
945
1402
|
this.events.publish(session.sessionId, { type: "shell.chunk", chunk }, eventScope);
|
|
946
|
-
this.publishActivity(session, "
|
|
1403
|
+
this.publishActivity(session, "正在运行命令", "active", command);
|
|
947
1404
|
this.publishStatus(session);
|
|
948
|
-
}, { excludeFromContext: isExcluded }).then((result) => {
|
|
1405
|
+
}, { excludeFromContext: isExcluded })).then((result) => {
|
|
949
1406
|
this.events.publish(session.sessionId, {
|
|
950
1407
|
type: "shell.end",
|
|
951
1408
|
output: result.output,
|
|
@@ -954,13 +1411,13 @@ export class PiSessionService {
|
|
|
954
1411
|
truncated: result.truncated,
|
|
955
1412
|
...(result.fullOutputPath === undefined ? {} : { fullOutputPath: result.fullOutputPath }),
|
|
956
1413
|
}, eventScope);
|
|
957
|
-
this.publishActivity(session, "
|
|
1414
|
+
this.publishActivity(session, "命令执行完成", result.exitCode === 0 ? "idle" : "error", command);
|
|
958
1415
|
this.publishStatus(session);
|
|
959
1416
|
}).catch((error) => {
|
|
960
1417
|
const message = error instanceof Error ? error.message : String(error);
|
|
961
1418
|
this.events.publish(session.sessionId, { type: "shell.end", output: message, isError: true }, eventScope);
|
|
962
1419
|
this.events.publish(session.sessionId, { type: "session.error", message }, eventScope);
|
|
963
|
-
this.publishActivity(session, "
|
|
1420
|
+
this.publishActivity(session, "命令执行失败", "error", message);
|
|
964
1421
|
this.publishStatus(session);
|
|
965
1422
|
});
|
|
966
1423
|
}
|
|
@@ -974,31 +1431,67 @@ export class PiSessionService {
|
|
|
974
1431
|
const active = await this.getActive(ref, managementContext);
|
|
975
1432
|
return this.commandService.respond(active.runtime.session.sessionId, requestId, value, active.eventScope);
|
|
976
1433
|
}
|
|
977
|
-
async
|
|
1434
|
+
async navigateTree(ref, request, managementContext) {
|
|
1435
|
+
if (request.targetId.trim() === "")
|
|
1436
|
+
throw new Error("Session tree target is required");
|
|
1437
|
+
if (this.isTreeExclusiveSessionIdentityActive(sessionIdFromLookup(ref))) {
|
|
1438
|
+
throw new Error("Stop current session activity before navigating the session tree");
|
|
1439
|
+
}
|
|
1440
|
+
await this.assertWritable(ref);
|
|
1441
|
+
const options = sessionTreeNavigationOptions(request);
|
|
1442
|
+
const session = await this.getOrOpen(ref, managementContext);
|
|
1443
|
+
if (typeof session.navigateTree !== "function")
|
|
1444
|
+
throw new Error("Session tree navigation is not supported by this Pi runtime");
|
|
978
1445
|
if (this.hasActiveWork(session))
|
|
979
|
-
throw new Error("Stop current session activity before
|
|
980
|
-
|
|
1446
|
+
throw new Error("Stop current session activity before navigating the session tree");
|
|
1447
|
+
// Acquire synchronously after the active-work check. No leaf-producing work
|
|
1448
|
+
// may enter this runtime until Pi's potentially asynchronous summary settles.
|
|
1449
|
+
this.treeNavigations.add(session);
|
|
981
1450
|
try {
|
|
982
|
-
|
|
983
|
-
|
|
1451
|
+
if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
|
|
1452
|
+
throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
|
|
1453
|
+
}
|
|
1454
|
+
this.publishActivity(session, options.summarize ? "正在汇总分支" : "正在切换会话树", "active");
|
|
984
1455
|
this.publishStatus(session);
|
|
1456
|
+
const result = await session.navigateTree(request.targetId, options);
|
|
1457
|
+
if (result.cancelled) {
|
|
1458
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1459
|
+
this.publishActivity(session, result.aborted === true ? "分支汇总已终止" : "会话树切换已取消", "idle");
|
|
1460
|
+
}
|
|
1461
|
+
return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
|
|
1462
|
+
}
|
|
1463
|
+
if (this.isCurrentActiveSession(session))
|
|
1464
|
+
this.publishActivity(session, "已切换会话树", "idle");
|
|
1465
|
+
return { cancelled: false, ...(result.editorText === undefined ? {} : { editorText: result.editorText }) };
|
|
985
1466
|
}
|
|
986
1467
|
catch (error) {
|
|
987
1468
|
const message = error instanceof Error ? error.message : String(error);
|
|
988
|
-
this.publishActivity(session, "
|
|
1469
|
+
this.publishActivity(session, "重新加载失败", "error", message);
|
|
989
1470
|
this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
|
|
990
1471
|
this.publishStatus(session);
|
|
991
1472
|
throw error;
|
|
992
1473
|
}
|
|
1474
|
+
finally {
|
|
1475
|
+
this.treeNavigations.delete(session);
|
|
1476
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1477
|
+
this.flushDeferredTreeNavigationWork(session);
|
|
1478
|
+
this.publishStatus(session);
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
1481
|
+
this.deferredGeneratedSessionNames.delete(session);
|
|
1482
|
+
this.deferredSubsessionNotifications.delete(session);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
993
1485
|
}
|
|
994
1486
|
async archive(ref, managementContext) {
|
|
995
1487
|
const active = await this.getActive(ref, managementContext);
|
|
996
1488
|
const session = active.runtime.session;
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1489
|
+
await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, runtime: active.runtime }], "归档前请先停止当前会话活动", async () => {
|
|
1490
|
+
const archiveInput = await this.archiveInputForSession(session);
|
|
1491
|
+
await this.closeActiveSession(active);
|
|
1492
|
+
await this.archiveStore.archive(archiveInput);
|
|
1493
|
+
await this.forgetUnreadSessions([archiveInput]);
|
|
1494
|
+
});
|
|
1002
1495
|
}
|
|
1003
1496
|
async archiveMany(refs, managementContext) {
|
|
1004
1497
|
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
@@ -1008,11 +1501,14 @@ export class PiSessionService {
|
|
|
1008
1501
|
]);
|
|
1009
1502
|
const failures = [];
|
|
1010
1503
|
const alreadyArchivedSessionIds = [];
|
|
1504
|
+
const unreadArchivedIdentities = [];
|
|
1011
1505
|
const planItems = [];
|
|
1012
1506
|
for (const ref of uniqueRefs) {
|
|
1013
1507
|
const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
1014
1508
|
if (archived !== undefined) {
|
|
1509
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(archived.sessionId, "archive"));
|
|
1015
1510
|
alreadyArchivedSessionIds.push(archived.sessionId);
|
|
1511
|
+
unreadArchivedIdentities.push(archived);
|
|
1016
1512
|
continue;
|
|
1017
1513
|
}
|
|
1018
1514
|
const active = this.activeForLookup(bulkRefToLookup(ref), managementContext);
|
|
@@ -1037,28 +1533,44 @@ export class PiSessionService {
|
|
|
1037
1533
|
failures.push({ sessionId: resolvedSessionId, error: errorMessage(error) });
|
|
1038
1534
|
}
|
|
1039
1535
|
}
|
|
1040
|
-
const
|
|
1536
|
+
const readyPlanItems = [];
|
|
1041
1537
|
for (const item of planItems) {
|
|
1538
|
+
const active = item.active ?? this.activeForLookup(bulkRefToLookup({ id: item.input.sessionId, cwd: item.input.cwd }), managementContext);
|
|
1539
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1540
|
+
failures.push({ sessionId: item.input.sessionId, error: "Stop current session activity before archiving" });
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
readyPlanItems.push(active === undefined ? item : { ...item, active });
|
|
1544
|
+
}
|
|
1545
|
+
const readyInputs = [];
|
|
1546
|
+
const archivedSessionIds = [...alreadyArchivedSessionIds];
|
|
1547
|
+
await this.runTreeExclusiveOperation(readyPlanItems.map(({ input, active }) => ({
|
|
1548
|
+
sessionId: input.sessionId,
|
|
1549
|
+
...(active === undefined ? {} : { session: active.runtime.session, runtime: active.runtime }),
|
|
1550
|
+
})), "Stop current session activity before archiving", async () => {
|
|
1551
|
+
for (const item of readyPlanItems) {
|
|
1552
|
+
try {
|
|
1553
|
+
if (item.active !== undefined)
|
|
1554
|
+
await this.closeActiveSession(item.active);
|
|
1555
|
+
else
|
|
1556
|
+
await this.closeActiveInScope(item.input.sessionId, eventScopeFromManagementContext(managementContext));
|
|
1557
|
+
readyInputs.push(item.input);
|
|
1558
|
+
}
|
|
1559
|
+
catch (error) {
|
|
1560
|
+
failures.push({ sessionId: item.input.sessionId, error: errorMessage(error) });
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1042
1563
|
try {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
await this.closeActiveInScope(item.input.sessionId, eventScopeFromManagementContext(managementContext));
|
|
1047
|
-
readyInputs.push(item.input);
|
|
1564
|
+
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
1565
|
+
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
1566
|
+
unreadArchivedIdentities.push(...archived);
|
|
1048
1567
|
}
|
|
1049
1568
|
catch (error) {
|
|
1050
|
-
|
|
1569
|
+
for (const input of readyInputs)
|
|
1570
|
+
failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
|
|
1051
1571
|
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
try {
|
|
1055
|
-
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
1056
|
-
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
1057
|
-
}
|
|
1058
|
-
catch (error) {
|
|
1059
|
-
for (const input of readyInputs)
|
|
1060
|
-
failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
|
|
1061
|
-
}
|
|
1572
|
+
});
|
|
1573
|
+
await this.forgetUnreadSessions(unreadArchivedIdentities);
|
|
1062
1574
|
return {
|
|
1063
1575
|
archived: true,
|
|
1064
1576
|
archivedSessionIds: uniqueStrings(archivedSessionIds),
|
|
@@ -1075,9 +1587,19 @@ export class PiSessionService {
|
|
|
1075
1587
|
if (busy !== undefined)
|
|
1076
1588
|
throw new Error(`归档 ${sessionDisplayName(busy)} 前请先停止当前会话活动`);
|
|
1077
1589
|
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1590
|
+
await this.runTreeExclusiveOperation(plan.unarchivedTargets.map((target) => ({
|
|
1591
|
+
sessionId: target.id,
|
|
1592
|
+
...(target.activeSession === undefined ? {} : { session: target.activeSession }),
|
|
1593
|
+
})), `归档 ${sessionDisplayName(session)} 前请先停止当前会话活动`, async () => {
|
|
1594
|
+
for (const target of plan.targets) {
|
|
1595
|
+
if (target.archived)
|
|
1596
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(target.id, "archive"));
|
|
1597
|
+
}
|
|
1598
|
+
for (const input of archiveInputs)
|
|
1599
|
+
await this.closeActiveInScope(input.sessionId, eventScopeFromManagementContext(managementContext));
|
|
1600
|
+
await this.archiveStoreArchiveMany(archiveInputs);
|
|
1601
|
+
});
|
|
1602
|
+
await this.forgetUnreadSessions(plan.targets.map((target) => ({ sessionId: target.id, cwd: target.cwd })));
|
|
1081
1603
|
return {
|
|
1082
1604
|
archived: true,
|
|
1083
1605
|
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
@@ -1091,6 +1613,7 @@ export class PiSessionService {
|
|
|
1091
1613
|
throw new Error("未找到会话");
|
|
1092
1614
|
await this.closeActiveInScope(archived.sessionId, eventScopeFromManagementContext(managementContext));
|
|
1093
1615
|
await this.archiveStore.restore(archived.sessionId);
|
|
1616
|
+
await this.forgetUnreadSessions([archived]);
|
|
1094
1617
|
}
|
|
1095
1618
|
async deleteArchived(ref, managementContext) {
|
|
1096
1619
|
const record = await this.getArchived(ref);
|
|
@@ -1102,6 +1625,7 @@ export class PiSessionService {
|
|
|
1102
1625
|
if (record.archivePath === undefined)
|
|
1103
1626
|
await this.ensureArchivedRecordMoved(record);
|
|
1104
1627
|
await this.archiveStore.deleteArchived(record.sessionId);
|
|
1628
|
+
await this.forgetUnreadSessions([record]);
|
|
1105
1629
|
}
|
|
1106
1630
|
async deleteArchivedMany(refs, managementContext) {
|
|
1107
1631
|
if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
@@ -1147,6 +1671,8 @@ export class PiSessionService {
|
|
|
1147
1671
|
for (const sessionId of deleteIds)
|
|
1148
1672
|
failures.push({ sessionId, error: errorMessage(error) });
|
|
1149
1673
|
}
|
|
1674
|
+
const deletedIdSet = new Set(deletedSessionIds);
|
|
1675
|
+
await this.forgetUnreadSessions(readyRecords.filter((record) => deletedIdSet.has(record.sessionId)));
|
|
1150
1676
|
return {
|
|
1151
1677
|
deleted: true,
|
|
1152
1678
|
deletedSessionIds,
|
|
@@ -1160,9 +1686,35 @@ export class PiSessionService {
|
|
|
1160
1686
|
const session = active.runtime.session;
|
|
1161
1687
|
if (this.hasActiveWork(session))
|
|
1162
1688
|
throw new Error("重新加载前请先停止当前会话活动");
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1689
|
+
const sessionFile = session.sessionFile;
|
|
1690
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
1691
|
+
throw new Error("会话尚未持久化");
|
|
1692
|
+
const reopenedSession = await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, runtime: active.runtime }], "重新加载前请先停止当前会话活动", async () => {
|
|
1693
|
+
const priorGeneration = this.notificationGenerationBySession.get(session);
|
|
1694
|
+
const { sessionId, cwd } = notificationIdentityForSession(session);
|
|
1695
|
+
let candidateGeneration;
|
|
1696
|
+
try {
|
|
1697
|
+
await this.closeActiveSession(active, priorGeneration === undefined ? CLEAR_RUNTIME_NOTIFICATIONS : DEFER_RUNTIME_NOTIFICATIONS);
|
|
1698
|
+
candidateGeneration = priorGeneration === undefined
|
|
1699
|
+
? undefined
|
|
1700
|
+
: this.notificationStore.beginReplacement(priorGeneration, { sessionId, cwd });
|
|
1701
|
+
const reopened = await this.openExistingSession(sessionId, cwd, () => this.sessionManager.open(sessionFile), {
|
|
1702
|
+
...(managementContext === undefined ? {} : { managementContext }),
|
|
1703
|
+
...(candidateGeneration === undefined ? {} : { notificationGeneration: candidateGeneration }),
|
|
1704
|
+
});
|
|
1705
|
+
if (candidateGeneration !== undefined) {
|
|
1706
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1707
|
+
}
|
|
1708
|
+
return reopened.runtime.session;
|
|
1709
|
+
}
|
|
1710
|
+
catch (error) {
|
|
1711
|
+
if (candidateGeneration !== undefined) {
|
|
1712
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration));
|
|
1713
|
+
}
|
|
1714
|
+
throw error;
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
this.publishStatus(reopenedSession);
|
|
1166
1718
|
}
|
|
1167
1719
|
async detachParent(ref, managementContext) {
|
|
1168
1720
|
const session = await this.getOrOpen(ref, managementContext);
|
|
@@ -1172,6 +1724,7 @@ export class PiSessionService {
|
|
|
1172
1724
|
await clearParentSession(sessionFile);
|
|
1173
1725
|
clearParentSessionHeader(session.sessionManager);
|
|
1174
1726
|
this.unregisterSubsession(session.sessionId);
|
|
1727
|
+
await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }]);
|
|
1175
1728
|
}
|
|
1176
1729
|
async abort(ref, managementContext) {
|
|
1177
1730
|
const active = this.activeForLookup(ref, managementContext);
|
|
@@ -1180,9 +1733,18 @@ export class PiSessionService {
|
|
|
1180
1733
|
const sessionId = active.runtime.session.sessionId;
|
|
1181
1734
|
this.clearCompactionPromptQueue(sessionId);
|
|
1182
1735
|
clearSessionQueue(active.runtime.session);
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1736
|
+
try {
|
|
1737
|
+
await this.abortSessionOperations(active.runtime.session);
|
|
1738
|
+
this.publishActivity(active.runtime.session, "已停止", "idle");
|
|
1739
|
+
}
|
|
1740
|
+
catch (error) {
|
|
1741
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1742
|
+
this.publishActivity(active.runtime.session, "停止失败", "error", message);
|
|
1743
|
+
throw error;
|
|
1744
|
+
}
|
|
1745
|
+
finally {
|
|
1746
|
+
this.publishStatus(active.runtime.session);
|
|
1747
|
+
}
|
|
1186
1748
|
}
|
|
1187
1749
|
async clearQueue(ref, managementContext) {
|
|
1188
1750
|
await this.assertWritable(ref);
|
|
@@ -1192,13 +1754,15 @@ export class PiSessionService {
|
|
|
1192
1754
|
this.publishStatus(session);
|
|
1193
1755
|
return this.statusFromSession(session);
|
|
1194
1756
|
}
|
|
1195
|
-
stop(ref, managementContext) {
|
|
1757
|
+
async stop(ref, managementContext) {
|
|
1196
1758
|
const active = this.activeForLookup(ref, managementContext);
|
|
1197
|
-
if (active
|
|
1759
|
+
if (active !== undefined) {
|
|
1760
|
+
await this.closeActiveSession(active);
|
|
1198
1761
|
return;
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1762
|
+
}
|
|
1763
|
+
if (isPiSessionRef(ref)) {
|
|
1764
|
+
this.publishNotificationMutations(this.notificationStore.clearSessionIdentity(ref.id, canonicalizeStoredCwd(ref.cwd), "runtime-close"));
|
|
1765
|
+
}
|
|
1202
1766
|
}
|
|
1203
1767
|
async bulkSessionLookupContext(refs) {
|
|
1204
1768
|
const cwdSet = new Set();
|
|
@@ -1386,20 +1950,37 @@ export class PiSessionService {
|
|
|
1386
1950
|
}
|
|
1387
1951
|
return [...names];
|
|
1388
1952
|
}
|
|
1389
|
-
async closeActive(sessionId) {
|
|
1953
|
+
async closeActive(sessionId, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
|
|
1390
1954
|
const pendingOpens = this.pendingSessionOpenPromises(sessionId);
|
|
1391
1955
|
if (pendingOpens.length > 0)
|
|
1392
1956
|
await Promise.allSettled(pendingOpens);
|
|
1393
1957
|
const activeSessions = this.activeSessionsForId(sessionId);
|
|
1394
|
-
|
|
1958
|
+
if (activeSessions.length === 0 && notificationPolicy.kind === "clear") {
|
|
1959
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(sessionId, notificationPolicy.reason));
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
await Promise.all(activeSessions.map((active) => this.closeActiveSession(active, notificationPolicy)));
|
|
1395
1963
|
}
|
|
1396
|
-
async closeActiveInScope(sessionId, eventScope) {
|
|
1964
|
+
async closeActiveInScope(sessionId, eventScope, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
|
|
1397
1965
|
const active = this.activeForSessionIdAndScope(sessionId, eventScope);
|
|
1398
|
-
if (active !== undefined)
|
|
1399
|
-
await this.closeActiveSession(active);
|
|
1966
|
+
if (active !== undefined) {
|
|
1967
|
+
await this.closeActiveSession(active, notificationPolicy);
|
|
1968
|
+
}
|
|
1969
|
+
else if (notificationPolicy.kind === "clear") {
|
|
1970
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(sessionId, notificationPolicy.reason));
|
|
1971
|
+
}
|
|
1400
1972
|
}
|
|
1401
|
-
async closeActiveSession(active) {
|
|
1973
|
+
async closeActiveSession(active, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
|
|
1402
1974
|
const sessionId = active.runtime.session.sessionId;
|
|
1975
|
+
if (notificationPolicy.kind === "clear") {
|
|
1976
|
+
const generation = this.notificationGenerationBySession.get(active.runtime.session);
|
|
1977
|
+
const mutations = generation === undefined
|
|
1978
|
+
? this.notificationStore.clearSession(sessionId, notificationPolicy.reason)
|
|
1979
|
+
: this.notificationStore.clearGeneration(generation, notificationPolicy.reason);
|
|
1980
|
+
this.publishNotificationMutations(mutations);
|
|
1981
|
+
}
|
|
1982
|
+
this.forgetUnreadActivity(active.runtime.session);
|
|
1983
|
+
this.pendingAskStore.forgetSession(sessionId);
|
|
1403
1984
|
this.active.delete(activeSessionKey(sessionId, active.eventScope));
|
|
1404
1985
|
this.activities.delete(activeSessionKey(sessionId, active.eventScope));
|
|
1405
1986
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
|
|
@@ -1412,13 +1993,36 @@ export class PiSessionService {
|
|
|
1412
1993
|
this.subsessionNotifyArmed.delete(sessionId);
|
|
1413
1994
|
clearSessionQueue(active.runtime.session);
|
|
1414
1995
|
active.unsubscribe();
|
|
1996
|
+
active.runtime.setRebindSession(undefined);
|
|
1415
1997
|
try {
|
|
1416
|
-
await active.runtime.session
|
|
1998
|
+
await this.abortSessionOperations(active.runtime.session);
|
|
1417
1999
|
}
|
|
1418
2000
|
finally {
|
|
1419
2001
|
await active.runtime.dispose();
|
|
1420
2002
|
}
|
|
1421
2003
|
}
|
|
2004
|
+
async abortSessionOperations(session) {
|
|
2005
|
+
let branchSummaryAbortFailed = false;
|
|
2006
|
+
let branchSummaryAbortError;
|
|
2007
|
+
try {
|
|
2008
|
+
session.abortBranchSummary?.();
|
|
2009
|
+
}
|
|
2010
|
+
catch (error) {
|
|
2011
|
+
branchSummaryAbortFailed = true;
|
|
2012
|
+
branchSummaryAbortError = error;
|
|
2013
|
+
}
|
|
2014
|
+
try {
|
|
2015
|
+
await session.abort();
|
|
2016
|
+
}
|
|
2017
|
+
catch (abortError) {
|
|
2018
|
+
if (branchSummaryAbortFailed) {
|
|
2019
|
+
throw new AggregateError([branchSummaryAbortError, abortError], "Failed to abort session operations", { cause: abortError });
|
|
2020
|
+
}
|
|
2021
|
+
throw abortError;
|
|
2022
|
+
}
|
|
2023
|
+
if (branchSummaryAbortFailed)
|
|
2024
|
+
throw branchSummaryAbortError;
|
|
2025
|
+
}
|
|
1422
2026
|
async assertWritable(ref) {
|
|
1423
2027
|
if (await this.getArchived(ref) !== undefined)
|
|
1424
2028
|
throw new Error("已归档会话为只读。请恢复会话后继续。");
|
|
@@ -1433,7 +2037,7 @@ export class PiSessionService {
|
|
|
1433
2037
|
const archived = await this.getArchived(ref);
|
|
1434
2038
|
if (archived?.archivePath !== undefined) {
|
|
1435
2039
|
const archivePath = archived.archivePath;
|
|
1436
|
-
return this.openPending(archived.sessionId, archived.cwd, managementContext, () => this.sessionManager.open(archivePath));
|
|
2040
|
+
return this.openPending(archived.sessionId, archived.cwd, managementContext, () => this.sessionManager.open(archivePath), { notifications: "disabled" });
|
|
1437
2041
|
}
|
|
1438
2042
|
const match = isPiSessionRef(ref)
|
|
1439
2043
|
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
@@ -1442,14 +2046,17 @@ export class PiSessionService {
|
|
|
1442
2046
|
throw new Error("未找到会话");
|
|
1443
2047
|
return this.openPending(match.id, match.cwd, managementContext, () => this.sessionManager.open(match.path));
|
|
1444
2048
|
}
|
|
1445
|
-
openPending(sessionId, cwd, managementContext, openSessionManager) {
|
|
2049
|
+
openPending(sessionId, cwd, managementContext, openSessionManager, options = {}) {
|
|
1446
2050
|
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId, managementContextKey(managementContext)]);
|
|
1447
2051
|
const existing = this.pendingSessionOpens.get(key);
|
|
1448
2052
|
if (existing !== undefined)
|
|
1449
2053
|
return existing.promise;
|
|
1450
2054
|
const pending = {
|
|
1451
2055
|
sessionId,
|
|
1452
|
-
promise: this.create(openSessionManager(), cwd,
|
|
2056
|
+
promise: this.create(openSessionManager(), cwd, {
|
|
2057
|
+
...options,
|
|
2058
|
+
...(managementContext === undefined ? {} : { managementContext }),
|
|
2059
|
+
}),
|
|
1453
2060
|
};
|
|
1454
2061
|
pending.promise = pending.promise.finally(() => {
|
|
1455
2062
|
if (this.pendingSessionOpens.get(key) === pending)
|
|
@@ -1521,66 +2128,29 @@ export class PiSessionService {
|
|
|
1521
2128
|
return [...new Set(this.active.values())].filter((active) => active.runtime.session.sessionId === sessionId);
|
|
1522
2129
|
}
|
|
1523
2130
|
async create(sessionManager, cwd, options = {}) {
|
|
1524
|
-
const
|
|
1525
|
-
&& await sessionAllowsDelegationTools(sessionManager, this.sessionManager);
|
|
1526
|
-
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
1527
|
-
cwd,
|
|
1528
|
-
agentDir: this.agentDir,
|
|
1529
|
-
sessionManager,
|
|
1530
|
-
delegationToolsEnabled,
|
|
1531
|
-
...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
|
|
1532
|
-
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
1533
|
-
});
|
|
1534
|
-
const active = { runtime, unsubscribe: noop, managementContextKey: managementContextKey(options.managementContext), eventScope: eventScopeFromManagementContext(options.managementContext) };
|
|
2131
|
+
const startup = this.startupProgress(sessionManager, options.startupIntent ?? "open", options.startupToken, eventScopeFromManagementContext(options.managementContext));
|
|
1535
2132
|
try {
|
|
1536
|
-
await this.
|
|
1537
|
-
this.bindRuntime(active);
|
|
1538
|
-
runtime.setRebindSession(async (session) => {
|
|
1539
|
-
await this.bindSessionExtensions(session);
|
|
1540
|
-
this.bindRuntime(active);
|
|
1541
|
-
await this.recoverSubsessionTrackingForOpenedSession(session);
|
|
1542
|
-
});
|
|
1543
|
-
this.active.set(activeSessionKey(runtime.session.sessionId, active.eventScope), active);
|
|
1544
|
-
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
1545
|
-
this.publishStatus(runtime.session);
|
|
1546
|
-
return active;
|
|
2133
|
+
return await this.createSessionRuntime(sessionManager, cwd, options, startup);
|
|
1547
2134
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
let removedActive = false;
|
|
1551
|
-
for (const [sessionKey, candidate] of this.active.entries()) {
|
|
1552
|
-
if (candidate !== active)
|
|
1553
|
-
continue;
|
|
1554
|
-
this.active.delete(sessionKey);
|
|
1555
|
-
this.activities.delete(sessionKey);
|
|
1556
|
-
removedActive = true;
|
|
1557
|
-
}
|
|
1558
|
-
this.clearAuthLossWarningsForSession(runtime.session.sessionId);
|
|
1559
|
-
this.clearCompactionPromptQueue(runtime.session.sessionId);
|
|
1560
|
-
if (removedActive) {
|
|
1561
|
-
this.workspaceActivity?.removeSession(runtime.session.sessionId, runtime.session.sessionManager.getCwd(), active.eventScope);
|
|
1562
|
-
}
|
|
1563
|
-
try {
|
|
1564
|
-
await runtime.session.abort();
|
|
1565
|
-
}
|
|
1566
|
-
finally {
|
|
1567
|
-
await runtime.dispose();
|
|
1568
|
-
}
|
|
1569
|
-
throw error;
|
|
2135
|
+
finally {
|
|
2136
|
+
startup.end();
|
|
1570
2137
|
}
|
|
1571
2138
|
}
|
|
1572
|
-
async bindSessionExtensions(session) {
|
|
2139
|
+
async bindSessionExtensions(session, generation) {
|
|
2140
|
+
const uiContext = this.sessionUiContext(session, generation);
|
|
1573
2141
|
await session.bindExtensions({
|
|
2142
|
+
uiContext,
|
|
2143
|
+
mode: "rpc",
|
|
1574
2144
|
onError: (error) => {
|
|
1575
2145
|
const message = `${error.extensionPath}: ${error.error}`;
|
|
1576
|
-
this.publishActivity(session, "
|
|
2146
|
+
this.publishActivity(session, "扩展错误", "error", message);
|
|
1577
2147
|
this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
|
|
1578
2148
|
},
|
|
1579
2149
|
});
|
|
1580
2150
|
}
|
|
1581
|
-
bindRuntime(active) {
|
|
2151
|
+
bindRuntime(active, session = active.runtime.session) {
|
|
2152
|
+
this.runtimeBySession.set(session, active.runtime);
|
|
1582
2153
|
active.unsubscribe();
|
|
1583
|
-
const { session } = active.runtime;
|
|
1584
2154
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
1585
2155
|
if (candidate === active) {
|
|
1586
2156
|
this.active.delete(sessionId);
|
|
@@ -1671,7 +2241,7 @@ export class PiSessionService {
|
|
|
1671
2241
|
const model = session.model;
|
|
1672
2242
|
if (model === undefined)
|
|
1673
2243
|
return;
|
|
1674
|
-
void generateShortSessionName(session.agent.
|
|
2244
|
+
void generateShortSessionName(session.agent.streamFunction, model, firstMessage).then((name) => {
|
|
1675
2245
|
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
1676
2246
|
}).catch(() => {
|
|
1677
2247
|
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
|
|
@@ -1680,6 +2250,10 @@ export class PiSessionService {
|
|
|
1680
2250
|
applyGeneratedSessionName(session, name) {
|
|
1681
2251
|
if (name === undefined || session.sessionName !== undefined)
|
|
1682
2252
|
return;
|
|
2253
|
+
if (this.treeNavigations.has(session)) {
|
|
2254
|
+
this.deferredGeneratedSessionNames.set(session, name);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
1683
2257
|
session.setSessionName(name);
|
|
1684
2258
|
this.publishSessionName(session);
|
|
1685
2259
|
}
|
|
@@ -1751,70 +2325,214 @@ export class PiSessionService {
|
|
|
1751
2325
|
}
|
|
1752
2326
|
}
|
|
1753
2327
|
activityLabelFromStatus(session) {
|
|
2328
|
+
if (this.treeNavigations.has(session))
|
|
2329
|
+
return "正在切换会话树";
|
|
2330
|
+
if (this.isSessionEntryMutationActive(session))
|
|
2331
|
+
return "正在更新会话";
|
|
1754
2332
|
if (session.isCompacting)
|
|
1755
|
-
return "
|
|
2333
|
+
return "正在压缩上下文";
|
|
1756
2334
|
if (session.isBashRunning)
|
|
1757
|
-
return "
|
|
2335
|
+
return "正在运行命令";
|
|
1758
2336
|
if (session.isStreaming)
|
|
1759
|
-
return "
|
|
2337
|
+
return "代理正在运行";
|
|
1760
2338
|
if (this.pendingMessageCount(session) > 0)
|
|
1761
|
-
return "
|
|
1762
|
-
return "
|
|
2339
|
+
return "正在排队";
|
|
2340
|
+
return "正在处理";
|
|
1763
2341
|
}
|
|
1764
2342
|
hasActiveWork(session) {
|
|
1765
|
-
return
|
|
2343
|
+
return this.treeNavigations.has(session)
|
|
2344
|
+
|| this.isSessionEntryMutationActive(session)
|
|
2345
|
+
|| this.isTreeExclusiveOperationActive(session)
|
|
2346
|
+
|| sessionHasActiveWork(session, this.compactionQueuedMessages(session.sessionId).length);
|
|
2347
|
+
}
|
|
2348
|
+
async runTreeExclusiveOperation(targets, activeError, operation) {
|
|
2349
|
+
const sessionIds = new Set();
|
|
2350
|
+
const runtimes = new Set();
|
|
2351
|
+
const sessions = new Set();
|
|
2352
|
+
for (const target of targets) {
|
|
2353
|
+
const runtime = target.runtime ?? (target.session === undefined ? undefined : this.activeRuntimeForSession(target.session));
|
|
2354
|
+
const session = target.session ?? runtime?.session;
|
|
2355
|
+
if (session !== undefined && this.hasActiveWork(session))
|
|
2356
|
+
throw new Error(activeError);
|
|
2357
|
+
sessionIds.add(target.sessionId);
|
|
2358
|
+
if (runtime !== undefined)
|
|
2359
|
+
runtimes.add(runtime);
|
|
2360
|
+
if (session !== undefined)
|
|
2361
|
+
sessions.add(session);
|
|
2362
|
+
}
|
|
2363
|
+
for (const sessionId of sessionIds) {
|
|
2364
|
+
this.treeExclusiveSessionOperationCounts.set(sessionId, (this.treeExclusiveSessionOperationCounts.get(sessionId) ?? 0) + 1);
|
|
2365
|
+
}
|
|
2366
|
+
for (const runtime of runtimes) {
|
|
2367
|
+
this.treeExclusiveRuntimeOperationCounts.set(runtime, (this.treeExclusiveRuntimeOperationCounts.get(runtime) ?? 0) + 1);
|
|
2368
|
+
}
|
|
2369
|
+
for (const session of sessions)
|
|
2370
|
+
this.observeUnreadActivityState(session);
|
|
2371
|
+
try {
|
|
2372
|
+
return await operation();
|
|
2373
|
+
}
|
|
2374
|
+
finally {
|
|
2375
|
+
for (const runtime of runtimes)
|
|
2376
|
+
decrementWeakCount(this.treeExclusiveRuntimeOperationCounts, runtime);
|
|
2377
|
+
for (const sessionId of sessionIds)
|
|
2378
|
+
decrementMapCount(this.treeExclusiveSessionOperationCounts, sessionId);
|
|
2379
|
+
for (const session of sessions) {
|
|
2380
|
+
if (this.isCurrentActiveSession(session))
|
|
2381
|
+
this.observeUnreadActivityState(session);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
isTreeExclusiveSessionIdentityActive(sessionId) {
|
|
2386
|
+
return (this.treeExclusiveSessionOperationCounts.get(sessionId) ?? 0) > 0;
|
|
2387
|
+
}
|
|
2388
|
+
isTreeExclusiveOperationActive(session) {
|
|
2389
|
+
if (this.isTreeExclusiveSessionIdentityActive(session.sessionId))
|
|
2390
|
+
return true;
|
|
2391
|
+
const runtime = this.activeRuntimeForSession(session);
|
|
2392
|
+
return runtime !== undefined && (this.treeExclusiveRuntimeOperationCounts.get(runtime) ?? 0) > 0;
|
|
2393
|
+
}
|
|
2394
|
+
activeRuntimeForSession(session) {
|
|
2395
|
+
const tracked = this.runtimeBySession.get(session);
|
|
2396
|
+
if (tracked !== undefined)
|
|
2397
|
+
return tracked;
|
|
2398
|
+
for (const active of new Set(this.active.values())) {
|
|
2399
|
+
if (active.runtime.session === session)
|
|
2400
|
+
return active.runtime;
|
|
2401
|
+
}
|
|
2402
|
+
return undefined;
|
|
2403
|
+
}
|
|
2404
|
+
assertTreeNavigationInactive(session, action) {
|
|
2405
|
+
if (this.treeNavigations.has(session))
|
|
2406
|
+
throw new Error(`Cannot ${action} while session tree navigation is active`);
|
|
2407
|
+
}
|
|
2408
|
+
async runSessionEntryMutation(session, action, operation) {
|
|
2409
|
+
this.beginSessionEntryMutation(session, action);
|
|
2410
|
+
try {
|
|
2411
|
+
return await operation();
|
|
2412
|
+
}
|
|
2413
|
+
finally {
|
|
2414
|
+
this.endSessionEntryMutation(session);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
beginSessionEntryMutation(session, action) {
|
|
2418
|
+
this.assertTreeNavigationInactive(session, action);
|
|
2419
|
+
this.sessionEntryMutationCounts.set(session, (this.sessionEntryMutationCounts.get(session) ?? 0) + 1);
|
|
2420
|
+
this.observeUnreadActivityState(session);
|
|
2421
|
+
}
|
|
2422
|
+
endSessionEntryMutation(session) {
|
|
2423
|
+
const remaining = (this.sessionEntryMutationCounts.get(session) ?? 1) - 1;
|
|
2424
|
+
if (remaining <= 0)
|
|
2425
|
+
this.sessionEntryMutationCounts.delete(session);
|
|
2426
|
+
else
|
|
2427
|
+
this.sessionEntryMutationCounts.set(session, remaining);
|
|
2428
|
+
this.observeUnreadActivityState(session);
|
|
2429
|
+
}
|
|
2430
|
+
isSessionEntryMutationActive(session) {
|
|
2431
|
+
return (this.sessionEntryMutationCounts.get(session) ?? 0) > 0;
|
|
1766
2432
|
}
|
|
1767
2433
|
publishActivityForEvent(session, event) {
|
|
1768
2434
|
const eventType = getString(event, "type");
|
|
1769
2435
|
if (eventType === undefined)
|
|
1770
2436
|
return;
|
|
1771
2437
|
if (eventType === "agent_start") {
|
|
1772
|
-
this.publishActivity(session, "
|
|
2438
|
+
this.publishActivity(session, "代理正在运行", "active");
|
|
1773
2439
|
return;
|
|
1774
2440
|
}
|
|
1775
2441
|
if (eventType === "agent_end") {
|
|
1776
|
-
this.publishActivity(session, "
|
|
1777
|
-
|
|
1778
|
-
|
|
2442
|
+
this.publishActivity(session, "空闲", "idle");
|
|
2443
|
+
const eventScope = this.eventScopeForSession(session);
|
|
2444
|
+
const sessionKey = activeSessionKey(session.sessionId, eventScope);
|
|
2445
|
+
this.activities.scheduleSettledRefresh(sessionKey, () => {
|
|
2446
|
+
if (this.active.get(sessionKey)?.runtime.session !== session)
|
|
2447
|
+
return;
|
|
2448
|
+
this.publishActivity(session, "空闲", "idle");
|
|
1779
2449
|
this.publishStatus(session);
|
|
1780
2450
|
}, 250);
|
|
1781
2451
|
return;
|
|
1782
2452
|
}
|
|
1783
2453
|
if (eventType === "turn_end") {
|
|
1784
|
-
this.publishActivity(session, "
|
|
2454
|
+
this.publishActivity(session, "本轮已完成", "idle");
|
|
1785
2455
|
return;
|
|
1786
2456
|
}
|
|
1787
2457
|
if (eventType === "message_start") {
|
|
1788
|
-
this.publishActivity(session, "
|
|
2458
|
+
this.publishActivity(session, "消息开始处理", "active");
|
|
1789
2459
|
return;
|
|
1790
2460
|
}
|
|
1791
2461
|
if (eventType === "message_end") {
|
|
1792
|
-
this.publishActivity(session, "
|
|
2462
|
+
this.publishActivity(session, "消息处理完成", "idle");
|
|
1793
2463
|
return;
|
|
1794
2464
|
}
|
|
1795
2465
|
if (eventType === "message_update") {
|
|
1796
|
-
this.publishActivity(session, "
|
|
2466
|
+
this.publishActivity(session, "正在接收回复", "active");
|
|
1797
2467
|
return;
|
|
1798
2468
|
}
|
|
1799
2469
|
if (eventType === "tool_execution_start") {
|
|
1800
|
-
this.publishActivity(session, "
|
|
2470
|
+
this.publishActivity(session, "正在运行工具", "active", getString(event, "toolName"));
|
|
1801
2471
|
return;
|
|
1802
2472
|
}
|
|
1803
2473
|
if (eventType === "tool_execution_end") {
|
|
1804
2474
|
const isError = getBoolean(event, "isError") === true;
|
|
1805
|
-
this.publishActivity(session, isError ? "
|
|
2475
|
+
this.publishActivity(session, isError ? "工具执行失败" : "工具执行完成", isError ? "error" : "idle", getString(event, "toolName"));
|
|
1806
2476
|
return;
|
|
1807
2477
|
}
|
|
1808
2478
|
if (eventType === "bash_execution_start") {
|
|
1809
|
-
this.publishActivity(session, "
|
|
2479
|
+
this.publishActivity(session, "正在运行命令", "active");
|
|
1810
2480
|
return;
|
|
1811
2481
|
}
|
|
1812
2482
|
if (eventType === "bash_execution_end") {
|
|
1813
|
-
this.publishActivity(session, "
|
|
2483
|
+
this.publishActivity(session, "命令执行完成", "idle");
|
|
1814
2484
|
return;
|
|
1815
2485
|
}
|
|
1816
2486
|
if (this.hasActiveWork(session))
|
|
1817
|
-
this.publishActivity(session,
|
|
2487
|
+
this.publishActivity(session, "正在处理", "active");
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* Build the reporter for one session construction.
|
|
2491
|
+
*
|
|
2492
|
+
* The session id is known before any await — a `SessionManager` has its id
|
|
2493
|
+
* from construction — so the daemon can name what it is starting even though
|
|
2494
|
+
* the `PiAgentSession` that {@link publishActivity} needs does not exist yet.
|
|
2495
|
+
* Without an id there is nothing to report against, so the reporter stays
|
|
2496
|
+
* silent and the browser keeps its own generic wording.
|
|
2497
|
+
*/
|
|
2498
|
+
startupProgress(sessionManager, intent, startupToken, eventScope) {
|
|
2499
|
+
const sessionId = sessionManager.getSessionId();
|
|
2500
|
+
if (sessionId === "")
|
|
2501
|
+
return { report: noop, end: noop };
|
|
2502
|
+
const label = intent === "create" ? "正在创建会话" : "正在打开会话";
|
|
2503
|
+
return {
|
|
2504
|
+
report: (phase) => { this.publishStartupProgress(sessionId, startupToken, label, "active", this.startupDetail(phase), eventScope); },
|
|
2505
|
+
end: () => {
|
|
2506
|
+
// A real activity published during the window (an extension error, say)
|
|
2507
|
+
// is the truth about this session and must survive the clear.
|
|
2508
|
+
if (this.activities.get(activeSessionKey(sessionId, eventScope)) !== undefined)
|
|
2509
|
+
return;
|
|
2510
|
+
this.publishStartupProgress(sessionId, startupToken, "空闲", "idle", undefined, eventScope);
|
|
2511
|
+
},
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
startupDetail(phase) {
|
|
2515
|
+
return this.catalogRefreshStatus?.isRefreshInFlight() === true
|
|
2516
|
+
? `${phase} · ${STARTUP_CONCURRENT_CATALOG_REFRESH}`
|
|
2517
|
+
: phase;
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Report startup progress on the global channel only, echoing the caller's
|
|
2521
|
+
* correlation token so a waiting browser row recognises its own construction.
|
|
2522
|
+
*
|
|
2523
|
+
* Unlike {@link publishActivity} this deliberately records nothing: no
|
|
2524
|
+
* `activities` entry, no workspace activity, no unread observation. There is
|
|
2525
|
+
* no session to own that state, and a failed creation would leave it stranded.
|
|
2526
|
+
*
|
|
2527
|
+
* Every report is marked `startup`, which is what keeps a session that is
|
|
2528
|
+
* merely opening from counting as one doing work. This is the only publisher
|
|
2529
|
+
* that sets the marker, and because it writes no `activities` entry no later
|
|
2530
|
+
* heartbeat re-publication can carry it.
|
|
2531
|
+
*/
|
|
2532
|
+
publishStartupProgress(sessionId, startupToken, label, phase, detail, eventScope) {
|
|
2533
|
+
const at = new Date().toISOString();
|
|
2534
|
+
const activity = detail === undefined ? { sessionId, phase, label, at, startup: true } : { sessionId, phase, label, detail, at, startup: true };
|
|
2535
|
+
this.events.publishGlobal(startupToken === undefined ? { type: "session.startup", activity } : { type: "session.startup", startupToken, activity }, eventScope);
|
|
1818
2536
|
}
|
|
1819
2537
|
publishActivity(session, label, phase, detail) {
|
|
1820
2538
|
const at = new Date().toISOString();
|
|
@@ -1825,6 +2543,7 @@ export class PiSessionService {
|
|
|
1825
2543
|
this.workspaceActivity?.applySessionActivity(session.sessionManager.getCwd(), activity, eventScope);
|
|
1826
2544
|
this.events.publish(session.sessionId, { type: "activity.update", activity }, eventScope);
|
|
1827
2545
|
this.events.publishGlobal({ type: "activity.update", activity }, eventScope);
|
|
2546
|
+
this.observeUnreadActivityState(session);
|
|
1828
2547
|
}
|
|
1829
2548
|
publishStatus(session) {
|
|
1830
2549
|
const status = this.statusFromSession(session);
|
|
@@ -1833,6 +2552,7 @@ export class PiSessionService {
|
|
|
1833
2552
|
this.workspaceActivity?.applySessionStatus(session.sessionManager.getCwd(), status, eventScope);
|
|
1834
2553
|
this.events.publish(session.sessionId, { type: "status.update", status }, eventScope);
|
|
1835
2554
|
this.events.publishGlobal({ type: "status.update", status }, eventScope);
|
|
2555
|
+
this.observeUnreadActivityState(session);
|
|
1836
2556
|
}
|
|
1837
2557
|
clearStaleActiveActivity(session) {
|
|
1838
2558
|
const eventScope = this.eventScopeForSession(session);
|
|
@@ -1840,7 +2560,7 @@ export class PiSessionService {
|
|
|
1840
2560
|
if (current?.phase !== "active" || this.hasActiveWork(session))
|
|
1841
2561
|
return;
|
|
1842
2562
|
const at = new Date().toISOString();
|
|
1843
|
-
const stored = { phase: "idle", label: "
|
|
2563
|
+
const stored = { phase: "idle", label: "空闲", at };
|
|
1844
2564
|
this.activities.set(activeSessionKey(session.sessionId, eventScope), stored);
|
|
1845
2565
|
const activity = { sessionId: session.sessionId, ...stored };
|
|
1846
2566
|
this.events.publish(session.sessionId, { type: "activity.update", activity }, eventScope);
|
|
@@ -1860,6 +2580,8 @@ export class PiSessionService {
|
|
|
1860
2580
|
const stats = session.getSessionStats();
|
|
1861
2581
|
const model = session.model === undefined ? undefined : modelToClientModel(session.model);
|
|
1862
2582
|
const contextUsage = session.getContextUsage();
|
|
2583
|
+
const warnings = this.warningsForSession(session);
|
|
2584
|
+
const pendingAsk = this.pendingAskStore.pendingAsk(session.sessionId);
|
|
1863
2585
|
return {
|
|
1864
2586
|
sessionId: session.sessionId,
|
|
1865
2587
|
persisted: sessionFileExists(session.sessionFile),
|
|
@@ -1874,8 +2596,24 @@ export class PiSessionService {
|
|
|
1874
2596
|
tokens: stats.tokens,
|
|
1875
2597
|
cost: stats.cost,
|
|
1876
2598
|
...(contextUsage === undefined ? {} : { contextUsage }),
|
|
2599
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
2600
|
+
...(pendingAsk === undefined ? {} : { pendingAsk }),
|
|
1877
2601
|
};
|
|
1878
2602
|
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Compute the live warning set for a session: runtime/resource diagnostics from
|
|
2605
|
+
* the active runtime (if any) plus the Anthropic subscription-auth notice. Read
|
|
2606
|
+
* fresh on each status publish so a rebuilt runtime or an auth/model change is
|
|
2607
|
+
* reflected without caching a stale snapshot.
|
|
2608
|
+
*/
|
|
2609
|
+
warningsForSession(session) {
|
|
2610
|
+
const runtime = [...this.active.values()].find((active) => active.runtime.session === session)?.runtime;
|
|
2611
|
+
const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
|
|
2612
|
+
const anthropic = anthropicSubscriptionWarning(session, join(this.agentDir, "auth.json"));
|
|
2613
|
+
if (anthropic !== undefined)
|
|
2614
|
+
warnings.push(anthropic);
|
|
2615
|
+
return warnings;
|
|
2616
|
+
}
|
|
1879
2617
|
pendingMessageCount(session) {
|
|
1880
2618
|
return session.pendingMessageCount + this.compactionQueuedMessages(session.sessionId).length;
|
|
1881
2619
|
}
|
|
@@ -1885,6 +2623,394 @@ export class PiSessionService {
|
|
|
1885
2623
|
hasQueuedMessageText(session, text) {
|
|
1886
2624
|
return queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)).some((message) => message.text === text);
|
|
1887
2625
|
}
|
|
2626
|
+
notificationCatalog() {
|
|
2627
|
+
return this.notificationStore.catalogSnapshot();
|
|
2628
|
+
}
|
|
2629
|
+
async unreadCatalog() {
|
|
2630
|
+
await this.publishUnreadMutations([]);
|
|
2631
|
+
return this.unreadStore.durableCatalogSnapshot();
|
|
2632
|
+
}
|
|
2633
|
+
async acknowledgeUnread(sessionId, request) {
|
|
2634
|
+
const result = this.unreadStore.acknowledge(sessionId, {
|
|
2635
|
+
...request,
|
|
2636
|
+
cwd: canonicalizeStoredCwd(request.cwd),
|
|
2637
|
+
});
|
|
2638
|
+
await this.publishUnreadMutations(result.mutations);
|
|
2639
|
+
return this.unreadStore.durableCatalogSnapshot();
|
|
2640
|
+
}
|
|
2641
|
+
notificationInbox(ref) {
|
|
2642
|
+
return this.notificationStore.inboxSnapshot(ref.id, canonicalizeStoredCwd(ref.cwd));
|
|
2643
|
+
}
|
|
2644
|
+
dismissNotification(ref, request) {
|
|
2645
|
+
const result = this.notificationStore.dismissNotification(ref.id, canonicalizeStoredCwd(ref.cwd), request.daemonInstanceId, request.notificationId);
|
|
2646
|
+
this.publishNotificationMutations(result.mutations);
|
|
2647
|
+
return result.snapshot;
|
|
2648
|
+
}
|
|
2649
|
+
dismissAllNotifications(ref, request) {
|
|
2650
|
+
const result = this.notificationStore.dismissAll(ref.id, canonicalizeStoredCwd(ref.cwd), request.daemonInstanceId, request.throughOrder, request.throughOverflowWatermark);
|
|
2651
|
+
this.publishNotificationMutations(result.mutations);
|
|
2652
|
+
return result.snapshot;
|
|
2653
|
+
}
|
|
2654
|
+
async deliverSubsessionNotification(session, notification) {
|
|
2655
|
+
await this.runSessionEntryMutation(session, "deliver a subsession notification", () => session.sendCustomMessage({ customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, content: notification.text, display: true, details: { sessionId: notification.childId } }, { triggerTurn: true, deliverAs: "followUp" }));
|
|
2656
|
+
this.publishStatus(session);
|
|
2657
|
+
}
|
|
2658
|
+
logSubsessionNotificationFailure(parentId, childId, error) {
|
|
2659
|
+
this.logger.info({ parentSessionId: parentId, sessionId: childId, error: error instanceof Error ? error.message : String(error) }, "failed to notify parent of subsession completion");
|
|
2660
|
+
}
|
|
2661
|
+
async reloadSessionRuntime(session) {
|
|
2662
|
+
if (this.hasActiveWork(session))
|
|
2663
|
+
throw new Error("Stop current session activity before reloading");
|
|
2664
|
+
await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before reloading", async () => {
|
|
2665
|
+
this.publishActivity(session, "正在重新加载资源", "active");
|
|
2666
|
+
const priorGeneration = this.notificationGenerationBySession.get(session);
|
|
2667
|
+
let candidateGeneration;
|
|
2668
|
+
try {
|
|
2669
|
+
await session.reload(priorGeneration === undefined ? undefined : {
|
|
2670
|
+
beforeSessionStart: () => {
|
|
2671
|
+
candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
2672
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
2673
|
+
this.replaceSessionNotificationContext(session, candidateGeneration);
|
|
2674
|
+
},
|
|
2675
|
+
});
|
|
2676
|
+
if (candidateGeneration !== undefined) {
|
|
2677
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
2678
|
+
}
|
|
2679
|
+
this.publishActivity(session, "资源已重新加载", "idle");
|
|
2680
|
+
this.publishStatus(session);
|
|
2681
|
+
}
|
|
2682
|
+
catch (error) {
|
|
2683
|
+
if (candidateGeneration !== undefined) {
|
|
2684
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
|
|
2685
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
2686
|
+
}
|
|
2687
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2688
|
+
this.publishActivity(session, "重新加载失败", "error", message);
|
|
2689
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
2690
|
+
this.publishStatus(session);
|
|
2691
|
+
throw error;
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
async dismissWarning(ref, dismissId) {
|
|
2696
|
+
const session = await this.getOrOpen(ref);
|
|
2697
|
+
dismissSessionWarning(session, dismissId);
|
|
2698
|
+
this.publishStatus(session);
|
|
2699
|
+
return this.statusFromSession(session);
|
|
2700
|
+
}
|
|
2701
|
+
openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
|
|
2702
|
+
const active = this.activeForLookup({ id: sessionId, cwd }, options.managementContext);
|
|
2703
|
+
if (active !== undefined)
|
|
2704
|
+
return Promise.resolve(active);
|
|
2705
|
+
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
|
|
2706
|
+
const existing = this.pendingSessionOpens.get(key);
|
|
2707
|
+
if (existing !== undefined)
|
|
2708
|
+
return existing.promise;
|
|
2709
|
+
const pending = {
|
|
2710
|
+
sessionId,
|
|
2711
|
+
promise: this.create(openSessionManager(), cwd, options),
|
|
2712
|
+
};
|
|
2713
|
+
pending.promise = pending.promise.finally(() => {
|
|
2714
|
+
if (this.pendingSessionOpens.get(key) === pending)
|
|
2715
|
+
this.pendingSessionOpens.delete(key);
|
|
2716
|
+
});
|
|
2717
|
+
this.pendingSessionOpens.set(key, pending);
|
|
2718
|
+
return pending.promise;
|
|
2719
|
+
}
|
|
2720
|
+
isCurrentActiveSession(session) {
|
|
2721
|
+
return [...this.active.values()].some((active) => active.runtime.session === session);
|
|
2722
|
+
}
|
|
2723
|
+
async createSessionRuntime(sessionManager, cwd, options, startup) {
|
|
2724
|
+
startup.report(STARTUP_PHASE_RUNTIME);
|
|
2725
|
+
const delegationToolsEnabled = options.creationProvenance !== "tracked-subsession"
|
|
2726
|
+
&& await sessionAllowsDelegationTools(sessionManager, this.sessionManager);
|
|
2727
|
+
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
2728
|
+
cwd,
|
|
2729
|
+
agentDir: this.agentDir,
|
|
2730
|
+
sessionManager,
|
|
2731
|
+
delegationToolsEnabled,
|
|
2732
|
+
...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
|
|
2733
|
+
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
2734
|
+
});
|
|
2735
|
+
const active = {
|
|
2736
|
+
runtime,
|
|
2737
|
+
unsubscribe: noop,
|
|
2738
|
+
managementContextKey: managementContextKey(options.managementContext),
|
|
2739
|
+
eventScope: eventScopeFromManagementContext(options.managementContext),
|
|
2740
|
+
};
|
|
2741
|
+
let boundSession = runtime.session;
|
|
2742
|
+
let notificationGeneration = options.notificationGeneration;
|
|
2743
|
+
let notificationOwnership = options.notifications === "disabled"
|
|
2744
|
+
? "disabled"
|
|
2745
|
+
: notificationGeneration === undefined
|
|
2746
|
+
? "registered"
|
|
2747
|
+
: "external";
|
|
2748
|
+
if (notificationOwnership === "registered") {
|
|
2749
|
+
const notificationIdentity = notificationIdentityForSession(runtime.session);
|
|
2750
|
+
const existingCandidate = this.notificationStore.beginReplacementForSession(notificationIdentity.sessionId, notificationIdentity.cwd);
|
|
2751
|
+
if (existingCandidate !== undefined) {
|
|
2752
|
+
notificationGeneration = existingCandidate;
|
|
2753
|
+
notificationOwnership = "replacement";
|
|
2754
|
+
}
|
|
2755
|
+
else {
|
|
2756
|
+
const registration = this.notificationStore.registerSession(notificationIdentity.sessionId, notificationIdentity.cwd);
|
|
2757
|
+
notificationGeneration = registration.generation;
|
|
2758
|
+
this.publishNotificationMutations(registration.mutations);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
if (notificationGeneration !== undefined)
|
|
2762
|
+
this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
|
|
2763
|
+
try {
|
|
2764
|
+
if (options.creationProvenance === "tracked-subsession") {
|
|
2765
|
+
await this.publishUnreadMutations(this.unreadStore.excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
|
|
2766
|
+
}
|
|
2767
|
+
else {
|
|
2768
|
+
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
2769
|
+
}
|
|
2770
|
+
startup.report(STARTUP_PHASE_EXTENSIONS);
|
|
2771
|
+
await this.bindSessionExtensions(runtime.session, notificationGeneration);
|
|
2772
|
+
this.bindRuntime(active);
|
|
2773
|
+
runtime.setRebindSession(async (session) => {
|
|
2774
|
+
const priorGeneration = notificationGeneration;
|
|
2775
|
+
let candidateGeneration;
|
|
2776
|
+
try {
|
|
2777
|
+
await this.prepareUnreadRuntimeRebind(boundSession, session);
|
|
2778
|
+
await this.recoverSubsessionTrackingForOpenedSession(session);
|
|
2779
|
+
if (priorGeneration !== undefined) {
|
|
2780
|
+
candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
2781
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
2782
|
+
}
|
|
2783
|
+
this.bindRuntime(active, session);
|
|
2784
|
+
boundSession = session;
|
|
2785
|
+
await this.bindSessionExtensions(session, candidateGeneration);
|
|
2786
|
+
if (candidateGeneration !== undefined) {
|
|
2787
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
2788
|
+
notificationGeneration = candidateGeneration;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
catch (error) {
|
|
2792
|
+
if (candidateGeneration !== undefined) {
|
|
2793
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
|
|
2794
|
+
notificationGeneration = candidateGeneration;
|
|
2795
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
2796
|
+
}
|
|
2797
|
+
throw error;
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
this.active.set(activeSessionKey(runtime.session.sessionId, active.eventScope), active);
|
|
2801
|
+
if (notificationOwnership === "replacement" && notificationGeneration !== undefined) {
|
|
2802
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
|
|
2803
|
+
notificationOwnership = "external";
|
|
2804
|
+
}
|
|
2805
|
+
this.publishStatus(runtime.session);
|
|
2806
|
+
return active;
|
|
2807
|
+
}
|
|
2808
|
+
catch (error) {
|
|
2809
|
+
if (notificationGeneration !== undefined) {
|
|
2810
|
+
if (notificationOwnership === "registered") {
|
|
2811
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(runtime.session.sessionId, "initialization-failed"));
|
|
2812
|
+
}
|
|
2813
|
+
else if (notificationOwnership === "replacement") {
|
|
2814
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(notificationGeneration));
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
active.unsubscribe();
|
|
2818
|
+
this.forgetUnreadActivity(boundSession);
|
|
2819
|
+
let removedActive = false;
|
|
2820
|
+
for (const [sessionKey, candidate] of this.active.entries()) {
|
|
2821
|
+
if (candidate !== active)
|
|
2822
|
+
continue;
|
|
2823
|
+
this.active.delete(sessionKey);
|
|
2824
|
+
this.activities.delete(sessionKey);
|
|
2825
|
+
this.clearAuthLossWarningsForSession(runtime.session.sessionId);
|
|
2826
|
+
this.clearCompactionPromptQueue(runtime.session.sessionId);
|
|
2827
|
+
removedActive = true;
|
|
2828
|
+
}
|
|
2829
|
+
if (removedActive) {
|
|
2830
|
+
this.workspaceActivity?.removeSession(runtime.session.sessionId, runtime.session.sessionManager.getCwd(), active.eventScope);
|
|
2831
|
+
}
|
|
2832
|
+
try {
|
|
2833
|
+
await runtime.session.abort();
|
|
2834
|
+
}
|
|
2835
|
+
finally {
|
|
2836
|
+
await runtime.dispose();
|
|
2837
|
+
}
|
|
2838
|
+
throw error;
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
replaceSessionNotificationContext(session, generation) {
|
|
2842
|
+
session.extensionRunner.setUIContext(this.sessionUiContext(session, generation), "rpc");
|
|
2843
|
+
}
|
|
2844
|
+
sessionUiContext(session, generation) {
|
|
2845
|
+
const baseUiContext = session.extensionRunner.getUIContext();
|
|
2846
|
+
const notify = (message, type) => {
|
|
2847
|
+
if (generation === undefined) {
|
|
2848
|
+
this.events.publish(session.sessionId, {
|
|
2849
|
+
type: "command.output",
|
|
2850
|
+
level: type === "error" ? "error" : "info",
|
|
2851
|
+
message,
|
|
2852
|
+
});
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
const added = this.notificationStore.addNotification(generation, message, type);
|
|
2856
|
+
this.publishNotificationMutations(added.mutations);
|
|
2857
|
+
if (added.notification === undefined)
|
|
2858
|
+
return;
|
|
2859
|
+
this.events.publish(session.sessionId, {
|
|
2860
|
+
type: "command.output",
|
|
2861
|
+
level: type === "error" ? "error" : "info",
|
|
2862
|
+
message,
|
|
2863
|
+
notificationId: added.notification.id,
|
|
2864
|
+
});
|
|
2865
|
+
};
|
|
2866
|
+
// PI WEB owns the browser-facing notification and text-formatting
|
|
2867
|
+
// boundaries. Delegate every other UI method to Pi's headless defaults so
|
|
2868
|
+
// unsupported dialogs cancel safely instead of hanging.
|
|
2869
|
+
return new Proxy(baseUiContext, {
|
|
2870
|
+
get(target, property, receiver) {
|
|
2871
|
+
if (property === "notify")
|
|
2872
|
+
return notify;
|
|
2873
|
+
if (property === "theme")
|
|
2874
|
+
return plainTextTheme;
|
|
2875
|
+
const value = Reflect.get(target, property, receiver);
|
|
2876
|
+
return value;
|
|
2877
|
+
},
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
publishNotificationMutations(mutations) {
|
|
2881
|
+
for (const mutation of mutations) {
|
|
2882
|
+
const scopes = new Set(this.activeSessionsForId(mutation.sessionId).map((active) => active.eventScope));
|
|
2883
|
+
if (scopes.size === 0)
|
|
2884
|
+
scopes.add(NORMAL_SESSION_EVENT_SCOPE);
|
|
2885
|
+
for (const scope of scopes) {
|
|
2886
|
+
this.events.publish(mutation.sessionId, mutation.inboxEvent, scope);
|
|
2887
|
+
this.events.publishGlobal(mutation.summaryEvent, scope);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
async prepareUnreadRuntimeRebind(previous, next) {
|
|
2892
|
+
const previousCwd = canonicalizeStoredCwd(previous.sessionManager.getCwd());
|
|
2893
|
+
this.unreadStore.forgetActivity(previous.sessionId, previousCwd);
|
|
2894
|
+
const nextCwd = canonicalizeStoredCwd(next.sessionManager.getCwd());
|
|
2895
|
+
if (previous.sessionId === next.sessionId && cwdPathsEqual(previousCwd, nextCwd))
|
|
2896
|
+
return;
|
|
2897
|
+
await this.publishUnreadMutations(this.unreadStore.forgetSession(previous.sessionId, previousCwd));
|
|
2898
|
+
}
|
|
2899
|
+
forgetUnreadActivity(session) {
|
|
2900
|
+
this.unreadStore.forgetActivity(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()));
|
|
2901
|
+
}
|
|
2902
|
+
async forgetUnreadSessions(identities) {
|
|
2903
|
+
const mutations = [];
|
|
2904
|
+
for (const identity of identities) {
|
|
2905
|
+
mutations.push(...this.unreadStore.forgetSession(identity.sessionId, canonicalizeStoredCwd(identity.cwd)));
|
|
2906
|
+
}
|
|
2907
|
+
await this.publishUnreadMutations(mutations);
|
|
2908
|
+
}
|
|
2909
|
+
observeUnreadActivityState(session) {
|
|
2910
|
+
const mutations = this.unreadStore.observeActivityState(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()), this.hasActiveWork(session));
|
|
2911
|
+
if (mutations.length === 0)
|
|
2912
|
+
return;
|
|
2913
|
+
void this.publishUnreadMutations(mutations).catch(() => undefined);
|
|
2914
|
+
}
|
|
2915
|
+
publishUnreadMutations(mutations) {
|
|
2916
|
+
this.enqueueUnreadMutations(mutations);
|
|
2917
|
+
this.unreadPublicationFlushRequested = true;
|
|
2918
|
+
if (this.unreadPublication === undefined && this.unreadPublicationRetryTimer !== undefined) {
|
|
2919
|
+
const failure = this.unreadPublicationFailure;
|
|
2920
|
+
return Promise.reject(failure instanceof Error
|
|
2921
|
+
? failure
|
|
2922
|
+
: new Error("Session unread publication is awaiting retry", { cause: failure }));
|
|
2923
|
+
}
|
|
2924
|
+
return this.ensureUnreadPublication();
|
|
2925
|
+
}
|
|
2926
|
+
ensureUnreadPublication() {
|
|
2927
|
+
const existing = this.unreadPublication;
|
|
2928
|
+
if (existing !== undefined)
|
|
2929
|
+
return existing;
|
|
2930
|
+
const publication = this.drainUnreadPublication();
|
|
2931
|
+
this.unreadPublication = publication;
|
|
2932
|
+
void publication.then(() => {
|
|
2933
|
+
if (this.unreadPublication === publication)
|
|
2934
|
+
this.unreadPublication = undefined;
|
|
2935
|
+
}, (error) => {
|
|
2936
|
+
if (this.unreadPublication === publication)
|
|
2937
|
+
this.unreadPublication = undefined;
|
|
2938
|
+
this.unreadPublicationFailure = error;
|
|
2939
|
+
this.logger.info({ error: error instanceof Error ? error.message : String(error) }, "failed to publish durable session unread mutations");
|
|
2940
|
+
this.scheduleUnreadPublicationRetry();
|
|
2941
|
+
});
|
|
2942
|
+
return publication;
|
|
2943
|
+
}
|
|
2944
|
+
async drainUnreadPublication() {
|
|
2945
|
+
while (this.unreadPublicationFlushRequested || this.pendingUnreadMutations.length > 0) {
|
|
2946
|
+
this.unreadPublicationFlushRequested = false;
|
|
2947
|
+
const batch = this.pendingUnreadMutations.splice(0);
|
|
2948
|
+
let publishedCount = 0;
|
|
2949
|
+
try {
|
|
2950
|
+
await this.unreadStore.flush();
|
|
2951
|
+
for (const mutation of batch) {
|
|
2952
|
+
this.events.publishGlobal(mutation.event);
|
|
2953
|
+
publishedCount += 1;
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
catch (error) {
|
|
2957
|
+
this.prependUnreadMutations(batch.slice(publishedCount));
|
|
2958
|
+
this.unreadPublicationFlushRequested = true;
|
|
2959
|
+
throw error;
|
|
2960
|
+
}
|
|
2961
|
+
this.unreadPublicationFailure = undefined;
|
|
2962
|
+
this.clearUnreadPublicationRetry();
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
enqueueUnreadMutations(mutations) {
|
|
2966
|
+
this.pendingUnreadMutations.push(...mutations);
|
|
2967
|
+
this.trimPendingUnreadMutations();
|
|
2968
|
+
}
|
|
2969
|
+
prependUnreadMutations(mutations) {
|
|
2970
|
+
this.pendingUnreadMutations.unshift(...mutations);
|
|
2971
|
+
this.trimPendingUnreadMutations();
|
|
2972
|
+
}
|
|
2973
|
+
trimPendingUnreadMutations() {
|
|
2974
|
+
const excess = this.pendingUnreadMutations.length - MAX_PENDING_UNREAD_MUTATIONS;
|
|
2975
|
+
if (excess > 0)
|
|
2976
|
+
this.pendingUnreadMutations.splice(0, excess);
|
|
2977
|
+
}
|
|
2978
|
+
scheduleUnreadPublicationRetry() {
|
|
2979
|
+
if (this.unreadPublicationStopped || this.unreadPublicationRetryTimer !== undefined)
|
|
2980
|
+
return;
|
|
2981
|
+
const delay = this.unreadPublicationRetryDelayMs;
|
|
2982
|
+
this.unreadPublicationRetryDelayMs = Math.min(Math.max(delay * 2, this.unreadPublicationRetryInitialMs), Math.max(MAX_UNREAD_PUBLICATION_RETRY_MS, this.unreadPublicationRetryInitialMs));
|
|
2983
|
+
this.unreadPublicationRetryTimer = setTimeout(() => {
|
|
2984
|
+
this.unreadPublicationRetryTimer = undefined;
|
|
2985
|
+
void this.ensureUnreadPublication().catch(() => undefined);
|
|
2986
|
+
}, delay);
|
|
2987
|
+
this.unreadPublicationRetryTimer.unref();
|
|
2988
|
+
}
|
|
2989
|
+
clearUnreadPublicationRetry() {
|
|
2990
|
+
if (this.unreadPublicationRetryTimer !== undefined)
|
|
2991
|
+
clearTimeout(this.unreadPublicationRetryTimer);
|
|
2992
|
+
this.unreadPublicationRetryTimer = undefined;
|
|
2993
|
+
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
2994
|
+
}
|
|
2995
|
+
flushDeferredTreeNavigationWork(session) {
|
|
2996
|
+
const generatedName = this.deferredGeneratedSessionNames.get(session);
|
|
2997
|
+
this.deferredGeneratedSessionNames.delete(session);
|
|
2998
|
+
if (generatedName !== undefined) {
|
|
2999
|
+
try {
|
|
3000
|
+
this.applyGeneratedSessionName(session, generatedName);
|
|
3001
|
+
}
|
|
3002
|
+
catch (error) {
|
|
3003
|
+
this.logger.info({ sessionId: session.sessionId, error: error instanceof Error ? error.message : String(error) }, "failed to apply deferred session name");
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
const notifications = this.deferredSubsessionNotifications.get(session) ?? [];
|
|
3007
|
+
this.deferredSubsessionNotifications.delete(session);
|
|
3008
|
+
for (const notification of notifications) {
|
|
3009
|
+
void this.deliverSubsessionNotification(session, notification).catch((error) => {
|
|
3010
|
+
this.logSubsessionNotificationFailure(notification.parentId, notification.childId, error);
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
1888
3014
|
}
|
|
1889
3015
|
function previewResponseFromPlan(plan) {
|
|
1890
3016
|
return {
|
|
@@ -1940,6 +3066,12 @@ function modelToClientModel(model) {
|
|
|
1940
3066
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
1941
3067
|
};
|
|
1942
3068
|
}
|
|
3069
|
+
function notificationIdentityForSession(session) {
|
|
3070
|
+
return {
|
|
3071
|
+
sessionId: session.sessionId,
|
|
3072
|
+
cwd: canonicalizeStoredCwd(session.sessionManager.getCwd()),
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
1943
3075
|
function clientSessionFromListEntry(session) {
|
|
1944
3076
|
return {
|
|
1945
3077
|
id: session.id,
|
|
@@ -2136,28 +3268,6 @@ function activeSessionFileMatches(active, expectedSessionFile) {
|
|
|
2136
3268
|
function trackedLinkParentFileMatches(link, parentSessionFile) {
|
|
2137
3269
|
return link.parentSessionFile !== undefined && sessionPathsEqual(link.parentSessionFile, parentSessionFile);
|
|
2138
3270
|
}
|
|
2139
|
-
async function readSessionHeaderSummary(sessionFile) {
|
|
2140
|
-
let file;
|
|
2141
|
-
try {
|
|
2142
|
-
file = await open(sessionFile, "r");
|
|
2143
|
-
const buffer = Buffer.alloc(4096);
|
|
2144
|
-
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
2145
|
-
const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n", 1)[0];
|
|
2146
|
-
if (firstLine === undefined || firstLine === "")
|
|
2147
|
-
return undefined;
|
|
2148
|
-
const header = JSON.parse(firstLine);
|
|
2149
|
-
if (!isRecord(header) || header["type"] !== "session" || typeof header["id"] !== "string")
|
|
2150
|
-
return undefined;
|
|
2151
|
-
const parentSession = getString(header, "parentSession");
|
|
2152
|
-
return { id: header["id"], ...(parentSession === undefined ? {} : { parentSession }) };
|
|
2153
|
-
}
|
|
2154
|
-
catch {
|
|
2155
|
-
return undefined;
|
|
2156
|
-
}
|
|
2157
|
-
finally {
|
|
2158
|
-
await file?.close().catch(() => undefined);
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
3271
|
async function sessionFileHeaderMatches(sessionFile, expected) {
|
|
2162
3272
|
const header = await readSessionHeaderSummary(sessionFile);
|
|
2163
3273
|
if (header?.id !== expected.sessionId)
|
|
@@ -2223,8 +3333,11 @@ function stringValue(value) {
|
|
|
2223
3333
|
return typeof value === "string" ? value : "";
|
|
2224
3334
|
}
|
|
2225
3335
|
function historyMessages(session) {
|
|
3336
|
+
return historyMessagesFromManager(session.sessionManager);
|
|
3337
|
+
}
|
|
3338
|
+
function historyMessagesFromManager(sessionManager) {
|
|
2226
3339
|
const messages = [];
|
|
2227
|
-
for (const entry of
|
|
3340
|
+
for (const entry of sessionManager.getBranch()) {
|
|
2228
3341
|
if (!isRecord(entry))
|
|
2229
3342
|
continue;
|
|
2230
3343
|
if (entry["type"] === "message")
|
|
@@ -2244,11 +3357,13 @@ const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
|
|
|
2244
3357
|
const SUBSESSION_CHILD_LINK_CUSTOM_TYPE = "pi-web.subsession.spawned";
|
|
2245
3358
|
/** customType marking a parent-facing subsession-completion notice. */
|
|
2246
3359
|
const SUBSESSION_NOTIFICATION_CUSTOM_TYPE = "subsession.completion";
|
|
2247
|
-
const
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
3360
|
+
const SUBSESSION_NOTIFICATION_MAX_OUTPUT_CHARS = 2000;
|
|
3361
|
+
/** Avoid duplicating a partial result in context when deliberate inspection can return the full output. */
|
|
3362
|
+
function formatSubsessionNotificationOutput(childSessionId, text) {
|
|
3363
|
+
if (text.length > SUBSESSION_NOTIFICATION_MAX_OUTPUT_CHARS) {
|
|
3364
|
+
return `Output from subsession ${childSessionId} was too long for this completion notice and was omitted. Call check_subsession with sessionId "${childSessionId}" to retrieve the final output.`;
|
|
3365
|
+
}
|
|
3366
|
+
return `--- SUBSESSION OUTPUT: ${childSessionId} ---\n${text === "" ? "(no output)" : text}`;
|
|
2252
3367
|
}
|
|
2253
3368
|
/** Most recent assistant text from a history message list, or "" if none. */
|
|
2254
3369
|
function finalAssistantText(messages) {
|