@jmfederico/pi-web 1.202607.0 → 1.202607.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 +12 -9
- package/dist/cli.js +305 -322
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-tXF3UTsY.js → CodeViewer-B4M13447.js} +1 -1
- package/dist/client/assets/{TerminalPanel-D_ihiWAt.js → TerminalPanel-Bmvj-Ecn.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-BziirXBJ.js → UnifiedDiffViewer-C0hygwRo.js} +1 -1
- package/dist/client/assets/index-TvPsRyS1.js +3545 -0
- package/dist/client/index.html +6 -6
- package/dist/client/manifest.webmanifest +4 -4
- package/dist/config.js +157 -2
- package/dist/config.js.map +1 -1
- package/dist/nativeServices/serviceDoctor.js +480 -0
- package/dist/nativeServices/serviceDoctor.js.map +1 -0
- package/dist/nativeServices/serviceInstall.js +35 -0
- package/dist/nativeServices/serviceInstall.js.map +1 -0
- package/dist/nativeServices/servicePlan.js +358 -0
- package/dist/nativeServices/servicePlan.js.map +1 -0
- package/dist/nativeServices/serviceProbe.js +447 -0
- package/dist/nativeServices/serviceProbe.js.map +1 -0
- package/dist/nativeServices/serviceRendering.js +122 -0
- package/dist/nativeServices/serviceRendering.js.map +1 -0
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +12 -0
- package/dist/plugin-api.d.ts +2 -0
- package/dist/server/activeAgentProfileProvider.js +25 -0
- package/dist/server/activeAgentProfileProvider.js.map +1 -0
- package/dist/server/app.js +57 -16
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +43 -9
- 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/machines/machinePluginProxyRoutes.js +7 -4
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +16 -4
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineRoutes.js +1 -1
- package/dist/server/machines/machineRoutes.js.map +1 -1
- package/dist/server/machines/machineService.js +2 -2
- package/dist/server/machines/machineService.js.map +1 -1
- package/dist/server/piPackageRoutes.js +7 -3
- package/dist/server/piPackageRoutes.js.map +1 -1
- package/dist/server/piPackageService.js +34 -2
- package/dist/server/piPackageService.js.map +1 -1
- package/dist/server/piWebPluginService.js +48 -12
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebReleaseLookupCache.js +37 -0
- package/dist/server/piWebReleaseLookupCache.js.map +1 -0
- package/dist/server/piWebStatus.js +38 -39
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/piWebStatusCache.js +19 -6
- package/dist/server/piWebStatusCache.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +37 -7
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessiond/sessionDaemonStartup.js +49 -0
- package/dist/server/sessiond/sessionDaemonStartup.js.map +1 -0
- package/dist/server/sessiond.js +128 -62
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/authProviderOptions.js +30 -32
- package/dist/server/sessions/authProviderOptions.js.map +1 -1
- package/dist/server/sessions/authRoutes.js +14 -4
- package/dist/server/sessions/authRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +140 -29
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/globalProviderPolicy.js +98 -0
- package/dist/server/sessions/globalProviderPolicy.js.map +1 -0
- package/dist/server/sessions/modelCatalogRefresher.js +184 -0
- package/dist/server/sessions/modelCatalogRefresher.js.map +1 -0
- package/dist/server/sessions/oauthLoginFlowService.js +176 -53
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +22 -21
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +936 -111
- 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/sessionArchiveMigration.js +590 -0
- package/dist/server/sessions/sessionArchiveMigration.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +7 -4
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +68 -9
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionNotificationStore.js +484 -0
- package/dist/server/sessions/sessionNotificationStore.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +202 -2
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionService.js +2 -0
- package/dist/server/sessions/sessionService.js.map +1 -0
- 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/spawnSessionTool.js +2 -2
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +69 -31
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/terminals/terminalService.js +10 -4
- package/dist/server/terminals/terminalService.js.map +1 -1
- package/dist/sessiond/activeAgentProfile.js +31 -0
- package/dist/sessiond/activeAgentProfile.js.map +1 -0
- package/dist/sessiond/sessionDaemonClient.js +38 -0
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/activeAgentProfile.js +76 -0
- package/dist/shared/activeAgentProfile.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +256 -4
- package/dist/shared/apiTypes.js +12 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +11 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +12 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/piWebStatusParsing.js +6 -0
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/docs/config.md +85 -12
- package/docs/plugins.md +29 -11
- package/install.sh +1 -1
- package/package.json +9 -8
- package/dist/client/assets/index-Brfk1lwl.js +0 -2950
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { open, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
4
6
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
5
7
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
6
8
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
9
|
+
import { projectSessionTree } from "./sessionTreeProjection.js";
|
|
7
10
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
8
11
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
9
12
|
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
10
13
|
import { computeEditPreview } from "./editPreview.js";
|
|
11
|
-
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
|
12
14
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
13
15
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
16
|
+
import { SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
14
17
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
15
18
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
16
19
|
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
17
20
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
18
21
|
import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
22
|
+
import { SessionNotificationStore, } from "./sessionNotificationStore.js";
|
|
23
|
+
import { plainTextTheme } from "./plainTextTheme.js";
|
|
24
|
+
import { SessionUnreadStore } from "./sessionUnreadStore.js";
|
|
19
25
|
const noopLogger = { info() { } };
|
|
26
|
+
const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
|
|
27
|
+
const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
|
|
28
|
+
const MAX_PENDING_UNREAD_MUTATIONS = SESSION_UNREAD_LIMIT + 1;
|
|
20
29
|
function noop() {
|
|
21
30
|
// Intentionally empty default unsubscribe callback.
|
|
22
31
|
}
|
|
@@ -37,6 +46,37 @@ function isPiSessionRef(ref) {
|
|
|
37
46
|
function lookupMatchesActiveSession(ref, active) {
|
|
38
47
|
return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
|
|
39
48
|
}
|
|
49
|
+
function sessionTreeNavigationOptions(request) {
|
|
50
|
+
switch (request.summary.mode) {
|
|
51
|
+
case "none":
|
|
52
|
+
return { summarize: false };
|
|
53
|
+
case "default":
|
|
54
|
+
return { summarize: true };
|
|
55
|
+
case "custom": {
|
|
56
|
+
const customInstructions = request.summary.instructions.trim();
|
|
57
|
+
if (customInstructions === "")
|
|
58
|
+
throw new Error("Custom branch-summary instructions are required");
|
|
59
|
+
if (customInstructions.length > SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH) {
|
|
60
|
+
throw new Error(`Custom branch-summary instructions must be at most ${String(SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH)} characters`);
|
|
61
|
+
}
|
|
62
|
+
return { summarize: true, customInstructions };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function decrementWeakCount(counts, key) {
|
|
67
|
+
const remaining = (counts.get(key) ?? 1) - 1;
|
|
68
|
+
if (remaining <= 0)
|
|
69
|
+
counts.delete(key);
|
|
70
|
+
else
|
|
71
|
+
counts.set(key, remaining);
|
|
72
|
+
}
|
|
73
|
+
function decrementMapCount(counts, key) {
|
|
74
|
+
const remaining = (counts.get(key) ?? 1) - 1;
|
|
75
|
+
if (remaining <= 0)
|
|
76
|
+
counts.delete(key);
|
|
77
|
+
else
|
|
78
|
+
counts.set(key, remaining);
|
|
79
|
+
}
|
|
40
80
|
function requirePromptText(value) {
|
|
41
81
|
if (typeof value !== "string")
|
|
42
82
|
throw new Error("Prompt text is required");
|
|
@@ -49,6 +89,109 @@ function parsePromptStreamingBehavior(value) {
|
|
|
49
89
|
return value;
|
|
50
90
|
throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
|
|
51
91
|
}
|
|
92
|
+
const CLEAR_RUNTIME_NOTIFICATIONS = { kind: "clear", reason: "runtime-close" };
|
|
93
|
+
const DEFER_RUNTIME_NOTIFICATIONS = { kind: "defer" };
|
|
94
|
+
function resourceDiagnosticToWarning(diagnostic, source) {
|
|
95
|
+
return {
|
|
96
|
+
severity: diagnostic.type === "error" ? "error" : "warning",
|
|
97
|
+
message: diagnostic.message,
|
|
98
|
+
source,
|
|
99
|
+
...(diagnostic.path === undefined ? {} : { path: diagnostic.path }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function runtimeDiagnosticToWarning(diagnostic) {
|
|
103
|
+
return { severity: diagnostic.type, message: diagnostic.message, source: "runtime" };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Compute the live warnings for a runtime by re-reading its current resource
|
|
107
|
+
* loader diagnostics, extension load errors, and runtime setup diagnostics.
|
|
108
|
+
*
|
|
109
|
+
* This mimics the TUI recomputing warnings on every (re)bind: it reads the
|
|
110
|
+
* runtime's current state rather than a cached snapshot, so a rebuilt runtime
|
|
111
|
+
* yields fresh warnings. Runtimes without SDK services (e.g. test fakes)
|
|
112
|
+
* contribute no warnings.
|
|
113
|
+
*/
|
|
114
|
+
export function collectRuntimeWarnings(runtime) {
|
|
115
|
+
const warnings = [];
|
|
116
|
+
for (const diagnostic of runtime.diagnostics ?? [])
|
|
117
|
+
warnings.push(runtimeDiagnosticToWarning(diagnostic));
|
|
118
|
+
const resourceLoader = runtime.services?.resourceLoader;
|
|
119
|
+
if (resourceLoader !== undefined) {
|
|
120
|
+
for (const diagnostic of resourceLoader.getSkills().diagnostics)
|
|
121
|
+
warnings.push(resourceDiagnosticToWarning(diagnostic, "skill"));
|
|
122
|
+
for (const diagnostic of resourceLoader.getPrompts().diagnostics)
|
|
123
|
+
warnings.push(resourceDiagnosticToWarning(diagnostic, "prompt"));
|
|
124
|
+
for (const diagnostic of resourceLoader.getThemes().diagnostics)
|
|
125
|
+
warnings.push(resourceDiagnosticToWarning(diagnostic, "theme"));
|
|
126
|
+
for (const error of resourceLoader.getExtensions().errors) {
|
|
127
|
+
warnings.push({ severity: "error", message: `${error.path}: ${error.error}`, source: "extension", path: error.path });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return warnings;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Verbatim TUI wording for the Anthropic subscription-auth billing notice. Kept
|
|
134
|
+
* character-for-character in sync with `ANTHROPIC_SUBSCRIPTION_AUTH_WARNING` in
|
|
135
|
+
* the SDK's interactive mode so the browser shows the same message the TUI does.
|
|
136
|
+
*/
|
|
137
|
+
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.";
|
|
138
|
+
/** Mirror of the SDK TUI `isAnthropicSubscriptionAuthKey` (subscription API keys start with `sk-ant-oat`). */
|
|
139
|
+
function isAnthropicSubscriptionAuthKey(apiKey) {
|
|
140
|
+
return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Dismiss id for the Anthropic subscription-auth billing notice. This is `pi`'s
|
|
144
|
+
* own `WarningSettings` key verbatim (`anthropicExtraUsage`): we carry the
|
|
145
|
+
* coupling `pi` already defines rather than inventing a parallel vocabulary, and
|
|
146
|
+
* {@link dismissSessionWarning} maps it back to `setWarnings`.
|
|
147
|
+
*/
|
|
148
|
+
const ANTHROPIC_EXTRA_USAGE_DISMISS_ID = "anthropicExtraUsage";
|
|
149
|
+
/**
|
|
150
|
+
* Port of the TUI `maybeWarnAboutAnthropicSubscriptionAuth` gate/trigger, computed
|
|
151
|
+
* live from the session's current model, stored Anthropic credential, and warning
|
|
152
|
+
* settings. Returns the billing warning when the active provider is `anthropic`
|
|
153
|
+
* and auth is a subscription credential (stored `oauth`, or an `sk-ant-oat` API
|
|
154
|
+
* key), unless suppressed via `getWarnings().anthropicExtraUsage === false`.
|
|
155
|
+
*
|
|
156
|
+
* The stored credential is read synchronously (matching the TUI's `oauth` branch
|
|
157
|
+
* and the documented `sk-ant-oat` key trigger) so warnings stay part of the
|
|
158
|
+
* synchronous live status computation.
|
|
159
|
+
*/
|
|
160
|
+
export function anthropicSubscriptionWarning(session, authPath) {
|
|
161
|
+
if (session.settingsManager.getWarnings().anthropicExtraUsage === false)
|
|
162
|
+
return undefined;
|
|
163
|
+
if (session.model?.provider !== "anthropic")
|
|
164
|
+
return undefined;
|
|
165
|
+
const credential = readStoredCredential("anthropic", authPath);
|
|
166
|
+
if (credential === undefined)
|
|
167
|
+
return undefined;
|
|
168
|
+
const isSubscriptionAuth = credential.type === "oauth"
|
|
169
|
+
? true
|
|
170
|
+
: isAnthropicSubscriptionAuthKey(credential.key);
|
|
171
|
+
if (!isSubscriptionAuth)
|
|
172
|
+
return undefined;
|
|
173
|
+
return {
|
|
174
|
+
severity: "warning",
|
|
175
|
+
message: ANTHROPIC_SUBSCRIPTION_AUTH_WARNING,
|
|
176
|
+
source: "anthropic",
|
|
177
|
+
dismiss: { id: ANTHROPIC_EXTRA_USAGE_DISMISS_ID },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Durably suppress a dismissable session warning by mapping its opaque dismiss
|
|
182
|
+
* id back to the concrete `pi` suppression it represents. Only known ids are
|
|
183
|
+
* honored; unknown ids throw so a stale/forged client cannot silently no-op.
|
|
184
|
+
*
|
|
185
|
+
* This is the single place provider-specific suppression lives: the wire type,
|
|
186
|
+
* parser, and UI stay agnostic. Adding a future dismissable warning is a
|
|
187
|
+
* server-only change here plus a `dismiss` id on its producer.
|
|
188
|
+
*/
|
|
189
|
+
export function dismissSessionWarning(session, dismissId) {
|
|
190
|
+
if (dismissId !== ANTHROPIC_EXTRA_USAGE_DISMISS_ID) {
|
|
191
|
+
throw new Error(`Unknown session warning dismiss id: ${dismissId}`);
|
|
192
|
+
}
|
|
193
|
+
session.settingsManager.setWarnings({ ...session.settingsManager.getWarnings(), anthropicExtraUsage: false });
|
|
194
|
+
}
|
|
52
195
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
53
196
|
if (!(options.sessionManager instanceof SessionManager))
|
|
54
197
|
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
@@ -83,9 +226,9 @@ export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn,
|
|
|
83
226
|
...(delegationEnabled && subsessions !== undefined ? createSubsessionToolDefinitions(cwd, subsessions) : []),
|
|
84
227
|
];
|
|
85
228
|
}
|
|
86
|
-
function createDefaultRuntimeFactory(
|
|
229
|
+
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions) {
|
|
87
230
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => {
|
|
88
|
-
const services = await createAgentSessionServices({ cwd, agentDir,
|
|
231
|
+
const services = await createAgentSessionServices({ cwd, agentDir, modelRuntime });
|
|
89
232
|
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
90
233
|
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
91
234
|
const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions);
|
|
@@ -121,11 +264,20 @@ function createPiWebEditToolDefinition(cwd) {
|
|
|
121
264
|
});
|
|
122
265
|
}
|
|
123
266
|
export class PiSessionService {
|
|
124
|
-
constructor(events, deps
|
|
267
|
+
constructor(events, deps) {
|
|
125
268
|
this.events = events;
|
|
126
269
|
this.active = new Map();
|
|
127
270
|
this.pendingSessionOpens = new Map();
|
|
128
271
|
this.activities = new Map();
|
|
272
|
+
/** Runtime-identity gate held while Pi may await abandoned-branch summarization. */
|
|
273
|
+
this.treeNavigations = new WeakSet();
|
|
274
|
+
/** Counts async operations that may append an entry before they settle. */
|
|
275
|
+
this.sessionEntryMutationCounts = new WeakMap();
|
|
276
|
+
/** Runtime/session-identity reservations for operations that must not overlap tree navigation. */
|
|
277
|
+
this.treeExclusiveRuntimeOperationCounts = new WeakMap();
|
|
278
|
+
this.treeExclusiveSessionOperationCounts = new Map();
|
|
279
|
+
this.deferredSubsessionNotifications = new WeakMap();
|
|
280
|
+
this.deferredGeneratedSessionNames = new WeakMap();
|
|
129
281
|
this.compactionPromptQueues = new Map();
|
|
130
282
|
this.compactionDrainTimers = new Map();
|
|
131
283
|
this.authLossWarnings = new Set();
|
|
@@ -143,17 +295,25 @@ export class PiSessionService {
|
|
|
143
295
|
* child that works again (and stops again) notifies the parent each time.
|
|
144
296
|
*/
|
|
145
297
|
this.subsessionNotifyArmed = new Map();
|
|
298
|
+
this.notificationGenerationBySession = new WeakMap();
|
|
299
|
+
this.pendingUnreadMutations = [];
|
|
300
|
+
this.unreadPublicationFlushRequested = false;
|
|
301
|
+
this.unreadPublicationStopped = false;
|
|
146
302
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
147
|
-
this.agentDir = deps.agentDir
|
|
148
|
-
this.sessionManager = deps.sessionManager
|
|
149
|
-
this.
|
|
303
|
+
this.agentDir = deps.agentDir;
|
|
304
|
+
this.sessionManager = deps.sessionManager;
|
|
305
|
+
this.modelRuntime = deps.modelRuntime;
|
|
150
306
|
this.spawnTargets = deps.spawnTargets;
|
|
151
307
|
this.logger = deps.logger ?? noopLogger;
|
|
152
308
|
this.now = deps.now ?? (() => new Date());
|
|
309
|
+
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
310
|
+
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
311
|
+
this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
|
|
312
|
+
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
153
313
|
// Subsessions are a beta capability gated behind their own flag, and they
|
|
154
314
|
// also require the spawn capability (they share its project-scope resolver).
|
|
155
315
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
156
|
-
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.
|
|
316
|
+
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
157
317
|
spawn: (input) => this.spawnSubsession(input),
|
|
158
318
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
159
319
|
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
@@ -164,19 +324,57 @@ export class PiSessionService {
|
|
|
164
324
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
165
325
|
this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, { echoUserMessage: false }), events, {
|
|
166
326
|
onCompactionStart: (session) => {
|
|
327
|
+
this.beginSessionEntryMutation(session, "compact the session");
|
|
167
328
|
this.publishActivity(session, "compacting", "active");
|
|
168
329
|
this.publishStatus(session);
|
|
169
330
|
},
|
|
170
331
|
onCompactionEnd: (session, result, detail) => {
|
|
332
|
+
this.endSessionEntryMutation(session);
|
|
171
333
|
this.publishActivity(session, result === "success" ? "compaction complete" : "compaction failed", result === "success" ? "idle" : "error", detail);
|
|
172
334
|
this.publishStatus(session);
|
|
173
335
|
},
|
|
174
336
|
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
337
|
+
getSessionTree: (session) => {
|
|
338
|
+
if (typeof session.sessionManager.getTree !== "function" || typeof session.navigateTree !== "function")
|
|
339
|
+
return undefined;
|
|
340
|
+
return projectSessionTree(session.sessionManager.getTree(), session.sessionManager.getLeafId());
|
|
341
|
+
},
|
|
342
|
+
hasActiveWork: (session) => this.hasActiveWork(session),
|
|
343
|
+
isTreeNavigationActive: (session) => this.treeNavigations.has(session),
|
|
344
|
+
runSessionReplacement: (session, operation) => this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before replacing the session", operation),
|
|
175
345
|
}, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
|
|
176
346
|
}
|
|
177
347
|
activeCount() {
|
|
178
348
|
return this.active.size;
|
|
179
349
|
}
|
|
350
|
+
notificationCatalog() {
|
|
351
|
+
return this.notificationStore.catalogSnapshot();
|
|
352
|
+
}
|
|
353
|
+
async unreadCatalog() {
|
|
354
|
+
await this.publishUnreadMutations([]);
|
|
355
|
+
return this.unreadStore.durableCatalogSnapshot();
|
|
356
|
+
}
|
|
357
|
+
async acknowledgeUnread(sessionId, request) {
|
|
358
|
+
const result = this.unreadStore.acknowledge(sessionId, {
|
|
359
|
+
...request,
|
|
360
|
+
cwd: canonicalizeStoredCwd(request.cwd),
|
|
361
|
+
});
|
|
362
|
+
await this.publishUnreadMutations(result.mutations);
|
|
363
|
+
return this.unreadStore.durableCatalogSnapshot();
|
|
364
|
+
}
|
|
365
|
+
notificationInbox(ref) {
|
|
366
|
+
return this.notificationStore.inboxSnapshot(ref.id, canonicalizeStoredCwd(ref.cwd));
|
|
367
|
+
}
|
|
368
|
+
dismissNotification(ref, request) {
|
|
369
|
+
const result = this.notificationStore.dismissNotification(ref.id, canonicalizeStoredCwd(ref.cwd), request.daemonInstanceId, request.notificationId);
|
|
370
|
+
this.publishNotificationMutations(result.mutations);
|
|
371
|
+
return result.snapshot;
|
|
372
|
+
}
|
|
373
|
+
dismissAllNotifications(ref, request) {
|
|
374
|
+
const result = this.notificationStore.dismissAll(ref.id, canonicalizeStoredCwd(ref.cwd), request.daemonInstanceId, request.throughOrder, request.throughOverflowWatermark);
|
|
375
|
+
this.publishNotificationMutations(result.mutations);
|
|
376
|
+
return result.snapshot;
|
|
377
|
+
}
|
|
180
378
|
async cleanupPreview(request) {
|
|
181
379
|
return previewResponseFromPlan(await this.cleanupPlan(request));
|
|
182
380
|
}
|
|
@@ -194,22 +392,24 @@ export class PiSessionService {
|
|
|
194
392
|
skippedBusySessionIds.add(input.sessionId);
|
|
195
393
|
continue;
|
|
196
394
|
}
|
|
197
|
-
await this.closeActive(input.sessionId);
|
|
395
|
+
await this.closeActive(input.sessionId, { kind: "clear", reason: "archive" });
|
|
198
396
|
readyArchiveInputs.push(input);
|
|
199
397
|
}
|
|
200
398
|
await this.archiveStoreArchiveMany(readyArchiveInputs);
|
|
201
399
|
archiveInputs.push(...readyArchiveInputs);
|
|
400
|
+
await this.forgetUnreadSessions(readyArchiveInputs);
|
|
202
401
|
for (const record of plan.deleteRecords) {
|
|
203
402
|
if (this.activeSessionHasWork(record.sessionId)) {
|
|
204
403
|
skippedBusySessionIds.add(record.sessionId);
|
|
205
404
|
continue;
|
|
206
405
|
}
|
|
207
|
-
await this.closeActive(record.sessionId);
|
|
406
|
+
await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
|
|
208
407
|
readyDeleteRecords.push(record);
|
|
209
408
|
}
|
|
210
409
|
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
211
410
|
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
212
411
|
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
412
|
+
await this.forgetUnreadSessions(deleteRecords);
|
|
213
413
|
return summarizeSessionCleanupExecution({
|
|
214
414
|
archiveInputs,
|
|
215
415
|
deleteRecords,
|
|
@@ -219,12 +419,16 @@ export class PiSessionService {
|
|
|
219
419
|
});
|
|
220
420
|
}
|
|
221
421
|
async dispose() {
|
|
422
|
+
this.unreadPublicationStopped = true;
|
|
423
|
+
this.clearUnreadPublicationRetry();
|
|
222
424
|
clearInterval(this.heartbeat);
|
|
223
425
|
this.clearCompactionDrainTimers();
|
|
224
426
|
const pendingOpens = this.pendingSessionOpenPromises();
|
|
225
427
|
if (pendingOpens.length > 0)
|
|
226
428
|
await Promise.allSettled(pendingOpens);
|
|
227
429
|
const activeSessions = Array.from(new Set(this.active.values()));
|
|
430
|
+
for (const active of activeSessions)
|
|
431
|
+
this.forgetUnreadActivity(active.runtime.session);
|
|
228
432
|
this.active.clear();
|
|
229
433
|
this.pendingSessionOpens.clear();
|
|
230
434
|
this.activities.clear();
|
|
@@ -235,16 +439,19 @@ export class PiSessionService {
|
|
|
235
439
|
this.subsessionLinks.clear();
|
|
236
440
|
this.subsessionHydratedParents.clear();
|
|
237
441
|
this.subsessionNotifyArmed.clear();
|
|
442
|
+
this.notificationStore.clearAll("service-dispose");
|
|
238
443
|
await Promise.all(activeSessions.map(async (active) => {
|
|
239
444
|
active.unsubscribe();
|
|
445
|
+
active.runtime.setRebindSession(undefined);
|
|
240
446
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
|
|
241
447
|
try {
|
|
242
|
-
await active.runtime.session
|
|
448
|
+
await this.abortSessionOperations(active.runtime.session);
|
|
243
449
|
}
|
|
244
450
|
finally {
|
|
245
451
|
await active.runtime.dispose();
|
|
246
452
|
}
|
|
247
453
|
}));
|
|
454
|
+
await this.publishUnreadMutations([]);
|
|
248
455
|
}
|
|
249
456
|
async list(cwd) {
|
|
250
457
|
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
|
|
@@ -253,8 +460,13 @@ export class PiSessionService {
|
|
|
253
460
|
.filter((record) => record.cwd === cwd)
|
|
254
461
|
.map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
|
|
255
462
|
const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
|
|
463
|
+
for (const record of archivedForCwd) {
|
|
464
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(record.sessionId, "archive-reconcile"));
|
|
465
|
+
}
|
|
256
466
|
const unarchivedSessions = sessions.filter((session) => !archivedById.has(session.id)).map(clientSessionFromListEntry);
|
|
257
|
-
|
|
467
|
+
const reconcilableSessionIds = this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById);
|
|
468
|
+
this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds);
|
|
469
|
+
await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
|
|
258
470
|
const archivedSessions = archivedForCwd
|
|
259
471
|
.sort(compareArchivedRecords)
|
|
260
472
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
@@ -329,7 +541,7 @@ export class PiSessionService {
|
|
|
329
541
|
...(parentSessionFile === undefined ? {} : { parentSessionFile }),
|
|
330
542
|
cwd: decision.cwd,
|
|
331
543
|
};
|
|
332
|
-
this.registerVerifiedSubsession(link);
|
|
544
|
+
await this.registerVerifiedSubsession(link);
|
|
333
545
|
this.persistSubsessionLink(link);
|
|
334
546
|
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
335
547
|
await this.prompt(created.id, input.prompt);
|
|
@@ -404,7 +616,7 @@ export class PiSessionService {
|
|
|
404
616
|
return undefined;
|
|
405
617
|
return sessionFileMatches(session, link.childSessionFile) ? link : undefined;
|
|
406
618
|
}
|
|
407
|
-
registerVerifiedSubsession(link) {
|
|
619
|
+
async registerVerifiedSubsession(link) {
|
|
408
620
|
const { childSessionId, parentSessionId } = link;
|
|
409
621
|
const previousParentId = this.subsessionParents.get(childSessionId);
|
|
410
622
|
if (previousParentId !== undefined && previousParentId !== parentSessionId) {
|
|
@@ -420,6 +632,24 @@ export class PiSessionService {
|
|
|
420
632
|
this.subsessionLinks.set(childSessionId, link);
|
|
421
633
|
if (!this.subsessionNotifyArmed.has(childSessionId))
|
|
422
634
|
this.subsessionNotifyArmed.set(childSessionId, false);
|
|
635
|
+
const cwd = this.cwdForVerifiedSubsession(link);
|
|
636
|
+
await this.publishUnreadMutations(this.unreadStore.excludeSession(childSessionId, cwd));
|
|
637
|
+
}
|
|
638
|
+
cwdForVerifiedSubsession(link) {
|
|
639
|
+
const activeCwd = this.activeChildForSubsessionLink(link)?.runtime.session.sessionManager.getCwd();
|
|
640
|
+
const linkedCwd = nonEmptyString(activeCwd) ?? nonEmptyString(link.cwd);
|
|
641
|
+
if (linkedCwd !== undefined)
|
|
642
|
+
return canonicalizeStoredCwd(linkedCwd);
|
|
643
|
+
const childSessionFile = link.childSessionFile;
|
|
644
|
+
if (childSessionFile !== undefined) {
|
|
645
|
+
try {
|
|
646
|
+
return canonicalizeStoredCwd(this.sessionManager.open(childSessionFile).getCwd());
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
throw new Error("Could not resolve cwd for verified tracked sub-session", { cause: error });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
throw new Error("Could not resolve cwd for verified tracked sub-session");
|
|
423
653
|
}
|
|
424
654
|
unregisterSubsession(childSessionId) {
|
|
425
655
|
const parentSessionId = this.subsessionParents.get(childSessionId);
|
|
@@ -466,40 +696,44 @@ export class PiSessionService {
|
|
|
466
696
|
const activeParent = this.active.get(parentSessionId);
|
|
467
697
|
if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
|
|
468
698
|
const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
|
|
469
|
-
await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
470
|
-
|
|
699
|
+
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
700
|
+
if (complete)
|
|
701
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
471
702
|
return;
|
|
472
703
|
}
|
|
473
704
|
if (parentSessionFile === undefined)
|
|
474
705
|
return;
|
|
475
|
-
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId)
|
|
476
|
-
this.subsessionHydratedParents.add(hydrationKey);
|
|
706
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId)
|
|
477
707
|
return;
|
|
478
|
-
}
|
|
479
708
|
let parentManager;
|
|
480
709
|
try {
|
|
481
710
|
parentManager = this.sessionManager.open(parentSessionFile);
|
|
482
711
|
}
|
|
483
712
|
catch {
|
|
484
|
-
this.subsessionHydratedParents.add(hydrationKey);
|
|
485
713
|
return;
|
|
486
714
|
}
|
|
487
|
-
await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
|
|
488
|
-
|
|
715
|
+
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
|
|
716
|
+
if (complete)
|
|
717
|
+
this.subsessionHydratedParents.add(hydrationKey);
|
|
489
718
|
}
|
|
490
719
|
async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile) {
|
|
491
720
|
// Parent custom links are the authoritative recovery record: verify the
|
|
492
|
-
// exact live child file/header before tracking.
|
|
721
|
+
// exact live child file/header before tracking. Do not negatively cache a
|
|
722
|
+
// scan while a candidate child is temporarily unavailable.
|
|
493
723
|
const entries = parentManager.getEntries?.() ?? parentManager.getBranch();
|
|
724
|
+
let complete = true;
|
|
494
725
|
for (const entry of entries) {
|
|
495
726
|
const link = parsePersistedParentSubsessionLink(entry);
|
|
496
|
-
if (link
|
|
727
|
+
if (link?.spawnedBySessionId !== parentSessionId)
|
|
497
728
|
continue;
|
|
498
729
|
const verified = await this.verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link);
|
|
499
|
-
if (verified === undefined)
|
|
730
|
+
if (verified === undefined) {
|
|
731
|
+
complete = false;
|
|
500
732
|
continue;
|
|
501
|
-
|
|
733
|
+
}
|
|
734
|
+
await this.registerVerifiedSubsession(verified);
|
|
502
735
|
}
|
|
736
|
+
return complete;
|
|
503
737
|
}
|
|
504
738
|
async verifiedSubsessionLinkFromParentLink(parentSessionId, parentSessionFile, link) {
|
|
505
739
|
if (parentSessionFile === undefined)
|
|
@@ -518,7 +752,7 @@ export class PiSessionService {
|
|
|
518
752
|
const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
|
|
519
753
|
if (link === undefined)
|
|
520
754
|
return;
|
|
521
|
-
this.registerVerifiedSubsession(link);
|
|
755
|
+
await this.registerVerifiedSubsession(link);
|
|
522
756
|
}
|
|
523
757
|
verifiedSubsessionLinkFromOpenedChild(session) {
|
|
524
758
|
return verifiedTrackedSubsessionLink(this.sessionManager, {
|
|
@@ -563,6 +797,16 @@ export class PiSessionService {
|
|
|
563
797
|
return "error";
|
|
564
798
|
return "idle";
|
|
565
799
|
}
|
|
800
|
+
workingSubsessionIds(parentSessionId) {
|
|
801
|
+
const childIds = this.subsessionChildren.get(parentSessionId);
|
|
802
|
+
if (childIds === undefined)
|
|
803
|
+
return [];
|
|
804
|
+
return [...childIds].filter((childId) => {
|
|
805
|
+
const link = this.subsessionLinks.get(childId);
|
|
806
|
+
const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
|
|
807
|
+
return active !== undefined && this.hasActiveWork(active.runtime.session);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
566
810
|
/**
|
|
567
811
|
* Drive parent notifications from a tracked child's status. Arms a pending
|
|
568
812
|
* notification while the child is working, and when it stops fires a single
|
|
@@ -583,8 +827,12 @@ export class PiSessionService {
|
|
|
583
827
|
this.subsessionNotifyArmed.set(childId, false);
|
|
584
828
|
const status = this.activities.get(childId)?.phase === "error" ? "error" : "idle";
|
|
585
829
|
const finalText = finalAssistantText(historyMessages(session));
|
|
586
|
-
const
|
|
587
|
-
const
|
|
830
|
+
const outputSection = formatSubsessionNotificationOutput(childId, finalText);
|
|
831
|
+
const workingIds = this.workingSubsessionIds(link.parentSessionId);
|
|
832
|
+
const next = workingIds.length === 0
|
|
833
|
+
? "No other tracked subsessions are working."
|
|
834
|
+
: `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.`;
|
|
835
|
+
const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n${outputSection}`;
|
|
588
836
|
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
|
589
837
|
}
|
|
590
838
|
async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
|
|
@@ -613,13 +861,25 @@ export class PiSessionService {
|
|
|
613
861
|
async notifyParentOfSubsession(parentId, childId, text) {
|
|
614
862
|
try {
|
|
615
863
|
const session = await this.getOrOpenParentForSubsession(parentId, childId);
|
|
616
|
-
|
|
617
|
-
|
|
864
|
+
if (this.treeNavigations.has(session)) {
|
|
865
|
+
const pending = this.deferredSubsessionNotifications.get(session) ?? [];
|
|
866
|
+
pending.push({ parentId, childId, text });
|
|
867
|
+
this.deferredSubsessionNotifications.set(session, pending);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
await this.deliverSubsessionNotification(session, { parentId, childId, text });
|
|
618
871
|
}
|
|
619
872
|
catch (error) {
|
|
620
|
-
this.
|
|
873
|
+
this.logSubsessionNotificationFailure(parentId, childId, error);
|
|
621
874
|
}
|
|
622
875
|
}
|
|
876
|
+
async deliverSubsessionNotification(session, notification) {
|
|
877
|
+
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" }));
|
|
878
|
+
this.publishStatus(session);
|
|
879
|
+
}
|
|
880
|
+
logSubsessionNotificationFailure(parentId, childId, error) {
|
|
881
|
+
this.logger.info({ parentSessionId: parentId, sessionId: childId, error: error instanceof Error ? error.message : String(error) }, "failed to notify parent of subsession completion");
|
|
882
|
+
}
|
|
623
883
|
async messages(ref, page) {
|
|
624
884
|
const session = await this.getOrOpen(ref);
|
|
625
885
|
return pageMessagesAtSafeBoundary(historyMessages(session), page);
|
|
@@ -627,26 +887,47 @@ export class PiSessionService {
|
|
|
627
887
|
async status(ref) {
|
|
628
888
|
return this.statusFromSession(await this.getOrOpen(ref));
|
|
629
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Join-time snapshot of the in-flight assistant stream. The `seq` watermark and
|
|
892
|
+
* the partial are read together in one synchronous tick (no await between the
|
|
893
|
+
* `currentSeq` read and the `state.streamingMessage` read) so a joining client
|
|
894
|
+
* can seed the partial and then apply only buffered live events with
|
|
895
|
+
* `seq > snapshot.seq`. The partial is browser-projected to strip thinking
|
|
896
|
+
* signatures; it is `null` when no assistant message is mid-stream.
|
|
897
|
+
*/
|
|
898
|
+
async streamSnapshot(ref) {
|
|
899
|
+
const session = await this.getOrOpen(ref);
|
|
900
|
+
// Single consistent tick: capture the watermark and the partial together so
|
|
901
|
+
// the seq matches the partial the client seeds against.
|
|
902
|
+
const seq = this.events.currentSeq(session.sessionId);
|
|
903
|
+
const streamingMessage = session.state.streamingMessage;
|
|
904
|
+
const partial = streamingMessage === undefined || streamingMessage === null
|
|
905
|
+
? null
|
|
906
|
+
: projectBrowserMessage(streamingMessage);
|
|
907
|
+
return { seq, partial };
|
|
908
|
+
}
|
|
630
909
|
async availableModels(ref) {
|
|
631
910
|
const session = await this.getOrOpen(ref);
|
|
632
|
-
session.
|
|
911
|
+
await session.modelRuntime.refresh();
|
|
633
912
|
const models = session.scopedModels.length > 0
|
|
634
913
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
635
|
-
: session.
|
|
914
|
+
: session.modelRuntime.getAvailableSnapshot();
|
|
636
915
|
return models.map(modelToClientModel);
|
|
637
916
|
}
|
|
638
917
|
async setModel(ref, provider, modelId) {
|
|
639
918
|
await this.assertWritable(ref);
|
|
640
919
|
const session = await this.getOrOpen(ref);
|
|
641
|
-
|
|
920
|
+
this.assertTreeNavigationInactive(session, "change models");
|
|
921
|
+
await session.modelRuntime.refresh();
|
|
922
|
+
this.assertTreeNavigationInactive(session, "change models");
|
|
642
923
|
const candidates = session.scopedModels.length > 0
|
|
643
924
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
644
|
-
: session.
|
|
925
|
+
: session.modelRuntime.getAvailableSnapshot();
|
|
645
926
|
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
|
|
646
|
-
?? session.
|
|
927
|
+
?? session.modelRuntime.getModel(provider, modelId);
|
|
647
928
|
if (model === undefined)
|
|
648
929
|
throw new Error(`Model not found: ${provider}/${modelId}`);
|
|
649
|
-
await session.setModel(model);
|
|
930
|
+
await this.runSessionEntryMutation(session, "change models", () => session.setModel(model));
|
|
650
931
|
this.publishActivity(session, `model: ${model.id}`, "idle", model.provider);
|
|
651
932
|
this.publishStatus(session);
|
|
652
933
|
return this.statusFromSession(session);
|
|
@@ -654,7 +935,7 @@ export class PiSessionService {
|
|
|
654
935
|
async cycleModel(ref, direction) {
|
|
655
936
|
await this.assertWritable(ref);
|
|
656
937
|
const session = await this.getOrOpen(ref);
|
|
657
|
-
const result = await session.cycleModel(direction);
|
|
938
|
+
const result = await this.runSessionEntryMutation(session, "change models", () => session.cycleModel(direction));
|
|
658
939
|
if (result === undefined)
|
|
659
940
|
throw new Error(session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available");
|
|
660
941
|
this.publishActivity(session, `model: ${result.model.id}`, "idle", result.model.provider);
|
|
@@ -668,6 +949,7 @@ export class PiSessionService {
|
|
|
668
949
|
async setThinkingLevel(ref, level) {
|
|
669
950
|
await this.assertWritable(ref);
|
|
670
951
|
const session = await this.getOrOpen(ref);
|
|
952
|
+
this.assertTreeNavigationInactive(session, "change the thinking level");
|
|
671
953
|
// pi owns the valid set; validate against the session's live levels rather
|
|
672
954
|
// than a hardcoded union so this stays correct if pi changes the set.
|
|
673
955
|
const available = session.getAvailableThinkingLevels();
|
|
@@ -682,6 +964,7 @@ export class PiSessionService {
|
|
|
682
964
|
async cycleThinkingLevel(ref) {
|
|
683
965
|
await this.assertWritable(ref);
|
|
684
966
|
const session = await this.getOrOpen(ref);
|
|
967
|
+
this.assertTreeNavigationInactive(session, "change the thinking level");
|
|
685
968
|
const level = session.cycleThinkingLevel();
|
|
686
969
|
if (level === undefined)
|
|
687
970
|
throw new Error("Current model does not support thinking");
|
|
@@ -715,6 +998,7 @@ export class PiSessionService {
|
|
|
715
998
|
const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
|
|
716
999
|
await this.assertWritable(ref);
|
|
717
1000
|
const session = await this.getOrOpen(ref);
|
|
1001
|
+
this.assertTreeNavigationInactive(session, "send a prompt");
|
|
718
1002
|
this.maybeGenerateSessionName(session, promptText);
|
|
719
1003
|
const isQueued = session.isStreaming || session.isCompacting;
|
|
720
1004
|
const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
|
|
@@ -734,7 +1018,7 @@ export class PiSessionService {
|
|
|
734
1018
|
if (behavior === undefined && echoUserMessage)
|
|
735
1019
|
this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) });
|
|
736
1020
|
const promptOptions = buildPromptOptions(behavior, images);
|
|
737
|
-
const promptPromise = session.prompt(text, promptOptions).catch((error) => {
|
|
1021
|
+
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions)).catch((error) => {
|
|
738
1022
|
const message = error instanceof Error ? error.message : String(error);
|
|
739
1023
|
this.publishActivity(session, "error", "error", message);
|
|
740
1024
|
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
@@ -761,6 +1045,7 @@ export class PiSessionService {
|
|
|
761
1045
|
await this.assertWritable(ref);
|
|
762
1046
|
const active = await this.getActive(ref);
|
|
763
1047
|
const { session } = active.runtime;
|
|
1048
|
+
this.assertTreeNavigationInactive(session, "run a shell command");
|
|
764
1049
|
const isExcluded = text.startsWith("!!");
|
|
765
1050
|
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
|
766
1051
|
if (!command)
|
|
@@ -769,11 +1054,11 @@ export class PiSessionService {
|
|
|
769
1054
|
throw new Error("A bash command is already running");
|
|
770
1055
|
this.publishActivity(session, "running bash", "active", command);
|
|
771
1056
|
this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded });
|
|
772
|
-
void session.executeBash(command, (chunk) => {
|
|
1057
|
+
void this.runSessionEntryMutation(session, "run a shell command", () => session.executeBash(command, (chunk) => {
|
|
773
1058
|
this.events.publish(session.sessionId, { type: "shell.chunk", chunk });
|
|
774
1059
|
this.publishActivity(session, "running bash", "active", command);
|
|
775
1060
|
this.publishStatus(session);
|
|
776
|
-
}, { excludeFromContext: isExcluded }).then((result) => {
|
|
1061
|
+
}, { excludeFromContext: isExcluded })).then((result) => {
|
|
777
1062
|
this.events.publish(session.sessionId, {
|
|
778
1063
|
type: "shell.end",
|
|
779
1064
|
output: result.output,
|
|
@@ -802,30 +1087,103 @@ export class PiSessionService {
|
|
|
802
1087
|
const active = await this.getActive(ref);
|
|
803
1088
|
return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
|
|
804
1089
|
}
|
|
805
|
-
async
|
|
1090
|
+
async navigateTree(ref, request) {
|
|
1091
|
+
if (request.targetId.trim() === "")
|
|
1092
|
+
throw new Error("Session tree target is required");
|
|
1093
|
+
if (this.isTreeExclusiveSessionIdentityActive(sessionIdFromLookup(ref))) {
|
|
1094
|
+
throw new Error("Stop current session activity before navigating the session tree");
|
|
1095
|
+
}
|
|
1096
|
+
await this.assertWritable(ref);
|
|
1097
|
+
const options = sessionTreeNavigationOptions(request);
|
|
1098
|
+
const session = await this.getOrOpen(ref);
|
|
1099
|
+
if (typeof session.navigateTree !== "function")
|
|
1100
|
+
throw new Error("Session tree navigation is not supported by this Pi runtime");
|
|
806
1101
|
if (this.hasActiveWork(session))
|
|
807
|
-
throw new Error("Stop current session activity before
|
|
808
|
-
|
|
1102
|
+
throw new Error("Stop current session activity before navigating the session tree");
|
|
1103
|
+
// Acquire synchronously after the active-work check. No leaf-producing work
|
|
1104
|
+
// may enter this runtime until Pi's potentially asynchronous summary settles.
|
|
1105
|
+
this.treeNavigations.add(session);
|
|
809
1106
|
try {
|
|
810
|
-
|
|
811
|
-
|
|
1107
|
+
if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
|
|
1108
|
+
throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
|
|
1109
|
+
}
|
|
1110
|
+
this.publishActivity(session, options.summarize ? "summarizing branch" : "navigating session tree", "active");
|
|
812
1111
|
this.publishStatus(session);
|
|
1112
|
+
const result = await session.navigateTree(request.targetId, options);
|
|
1113
|
+
if (result.cancelled) {
|
|
1114
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1115
|
+
this.publishActivity(session, result.aborted === true ? "branch summary aborted" : "tree navigation cancelled", "idle");
|
|
1116
|
+
}
|
|
1117
|
+
return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
|
|
1118
|
+
}
|
|
1119
|
+
if (this.isCurrentActiveSession(session))
|
|
1120
|
+
this.publishActivity(session, "session tree navigated", "idle");
|
|
1121
|
+
return { cancelled: false, ...(result.editorText === undefined ? {} : { editorText: result.editorText }) };
|
|
813
1122
|
}
|
|
814
1123
|
catch (error) {
|
|
815
1124
|
const message = error instanceof Error ? error.message : String(error);
|
|
816
|
-
this.
|
|
817
|
-
|
|
818
|
-
|
|
1125
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1126
|
+
this.publishActivity(session, "tree navigation failed", "error", message);
|
|
1127
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1128
|
+
}
|
|
819
1129
|
throw error;
|
|
820
1130
|
}
|
|
1131
|
+
finally {
|
|
1132
|
+
this.treeNavigations.delete(session);
|
|
1133
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1134
|
+
this.flushDeferredTreeNavigationWork(session);
|
|
1135
|
+
this.publishStatus(session);
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
this.deferredGeneratedSessionNames.delete(session);
|
|
1139
|
+
this.deferredSubsessionNotifications.delete(session);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async reloadSessionRuntime(session) {
|
|
1144
|
+
if (this.hasActiveWork(session))
|
|
1145
|
+
throw new Error("Stop current session activity before reloading");
|
|
1146
|
+
await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before reloading", async () => {
|
|
1147
|
+
this.publishActivity(session, "reloading resources", "active");
|
|
1148
|
+
const priorGeneration = this.notificationGenerationBySession.get(session);
|
|
1149
|
+
let candidateGeneration;
|
|
1150
|
+
try {
|
|
1151
|
+
await session.reload(priorGeneration === undefined ? undefined : {
|
|
1152
|
+
beforeSessionStart: () => {
|
|
1153
|
+
candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
1154
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1155
|
+
this.replaceSessionNotificationContext(session, candidateGeneration);
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
if (candidateGeneration !== undefined) {
|
|
1159
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1160
|
+
}
|
|
1161
|
+
this.publishActivity(session, "resources reloaded", "idle");
|
|
1162
|
+
this.publishStatus(session);
|
|
1163
|
+
}
|
|
1164
|
+
catch (error) {
|
|
1165
|
+
if (candidateGeneration !== undefined) {
|
|
1166
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
|
|
1167
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1168
|
+
}
|
|
1169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1170
|
+
this.publishActivity(session, "reload failed", "error", message);
|
|
1171
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1172
|
+
this.publishStatus(session);
|
|
1173
|
+
throw error;
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
821
1176
|
}
|
|
822
1177
|
async archive(ref) {
|
|
823
1178
|
const session = await this.getOrOpen(ref);
|
|
824
1179
|
if (this.hasActiveWork(session))
|
|
825
1180
|
throw new Error("Stop current session activity before archiving");
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1181
|
+
await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before archiving", async () => {
|
|
1182
|
+
const archiveInput = await this.archiveInputForSession(session);
|
|
1183
|
+
await this.closeActive(session.sessionId, { kind: "clear", reason: "archive" });
|
|
1184
|
+
await this.archiveStore.archive(archiveInput);
|
|
1185
|
+
await this.forgetUnreadSessions([archiveInput]);
|
|
1186
|
+
});
|
|
829
1187
|
}
|
|
830
1188
|
async archiveMany(refs) {
|
|
831
1189
|
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
@@ -835,11 +1193,14 @@ export class PiSessionService {
|
|
|
835
1193
|
]);
|
|
836
1194
|
const failures = [];
|
|
837
1195
|
const alreadyArchivedSessionIds = [];
|
|
1196
|
+
const unreadArchivedIdentities = [];
|
|
838
1197
|
const planItems = [];
|
|
839
1198
|
for (const ref of uniqueRefs) {
|
|
840
1199
|
const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
841
1200
|
if (archived !== undefined) {
|
|
1201
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(archived.sessionId, "archive"));
|
|
842
1202
|
alreadyArchivedSessionIds.push(archived.sessionId);
|
|
1203
|
+
unreadArchivedIdentities.push(archived);
|
|
843
1204
|
continue;
|
|
844
1205
|
}
|
|
845
1206
|
const active = this.activeForLookup(bulkRefToLookup(ref));
|
|
@@ -864,25 +1225,41 @@ export class PiSessionService {
|
|
|
864
1225
|
failures.push({ sessionId: resolvedSessionId, error: errorMessage(error) });
|
|
865
1226
|
}
|
|
866
1227
|
}
|
|
867
|
-
const
|
|
1228
|
+
const readyPlanItems = [];
|
|
868
1229
|
for (const item of planItems) {
|
|
1230
|
+
const active = this.activeForLookup({ id: item.input.sessionId, cwd: item.input.cwd });
|
|
1231
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1232
|
+
failures.push({ sessionId: item.input.sessionId, error: "Stop current session activity before archiving" });
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
readyPlanItems.push(active === undefined ? item : { ...item, active });
|
|
1236
|
+
}
|
|
1237
|
+
const readyInputs = [];
|
|
1238
|
+
const archivedSessionIds = [...alreadyArchivedSessionIds];
|
|
1239
|
+
await this.runTreeExclusiveOperation(readyPlanItems.map(({ input, active }) => ({
|
|
1240
|
+
sessionId: input.sessionId,
|
|
1241
|
+
...(active === undefined ? {} : { session: active.runtime.session, runtime: active.runtime }),
|
|
1242
|
+
})), "Stop current session activity before archiving", async () => {
|
|
1243
|
+
for (const item of readyPlanItems) {
|
|
1244
|
+
try {
|
|
1245
|
+
await this.closeActive(item.input.sessionId, { kind: "clear", reason: "archive" });
|
|
1246
|
+
readyInputs.push(item.input);
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
failures.push({ sessionId: item.input.sessionId, error: errorMessage(error) });
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
869
1252
|
try {
|
|
870
|
-
await this.
|
|
871
|
-
|
|
1253
|
+
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
1254
|
+
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
1255
|
+
unreadArchivedIdentities.push(...archived);
|
|
872
1256
|
}
|
|
873
1257
|
catch (error) {
|
|
874
|
-
|
|
1258
|
+
for (const input of readyInputs)
|
|
1259
|
+
failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
|
|
875
1260
|
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
try {
|
|
879
|
-
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
880
|
-
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
881
|
-
}
|
|
882
|
-
catch (error) {
|
|
883
|
-
for (const input of readyInputs)
|
|
884
|
-
failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
|
|
885
|
-
}
|
|
1261
|
+
});
|
|
1262
|
+
await this.forgetUnreadSessions(unreadArchivedIdentities);
|
|
886
1263
|
return {
|
|
887
1264
|
archived: true,
|
|
888
1265
|
archivedSessionIds: uniqueStrings(archivedSessionIds),
|
|
@@ -899,9 +1276,19 @@ export class PiSessionService {
|
|
|
899
1276
|
if (busy !== undefined)
|
|
900
1277
|
throw new Error(`Stop current session activity before archiving ${sessionDisplayName(busy)}`);
|
|
901
1278
|
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1279
|
+
await this.runTreeExclusiveOperation(plan.unarchivedTargets.map((target) => ({
|
|
1280
|
+
sessionId: target.id,
|
|
1281
|
+
...(target.activeSession === undefined ? {} : { session: target.activeSession }),
|
|
1282
|
+
})), `Stop current session activity before archiving ${sessionDisplayName(session)}`, async () => {
|
|
1283
|
+
for (const target of plan.targets) {
|
|
1284
|
+
if (target.archived)
|
|
1285
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(target.id, "archive"));
|
|
1286
|
+
}
|
|
1287
|
+
for (const input of archiveInputs)
|
|
1288
|
+
await this.closeActive(input.sessionId, { kind: "clear", reason: "archive" });
|
|
1289
|
+
await this.archiveStoreArchiveMany(archiveInputs);
|
|
1290
|
+
});
|
|
1291
|
+
await this.forgetUnreadSessions(plan.targets.map((target) => ({ sessionId: target.id, cwd: target.cwd })));
|
|
905
1292
|
return {
|
|
906
1293
|
archived: true,
|
|
907
1294
|
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
@@ -913,8 +1300,9 @@ export class PiSessionService {
|
|
|
913
1300
|
const archived = await this.getArchived(ref);
|
|
914
1301
|
if (archived === undefined)
|
|
915
1302
|
throw new Error("Session not found");
|
|
916
|
-
await this.closeActive(archived.sessionId);
|
|
1303
|
+
await this.closeActive(archived.sessionId, { kind: "clear", reason: "restore" });
|
|
917
1304
|
await this.archiveStore.restore(archived.sessionId);
|
|
1305
|
+
await this.forgetUnreadSessions([archived]);
|
|
918
1306
|
}
|
|
919
1307
|
async deleteArchived(ref) {
|
|
920
1308
|
const record = await this.getArchived(ref);
|
|
@@ -922,10 +1310,11 @@ export class PiSessionService {
|
|
|
922
1310
|
throw new Error("Archived session not found");
|
|
923
1311
|
if (this.archiveStore.deleteArchived === undefined)
|
|
924
1312
|
throw new Error("Archive store does not support deletion");
|
|
925
|
-
await this.closeActive(record.sessionId);
|
|
1313
|
+
await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
|
|
926
1314
|
if (record.archivePath === undefined)
|
|
927
1315
|
await this.ensureArchivedRecordMoved(record);
|
|
928
1316
|
await this.archiveStore.deleteArchived(record.sessionId);
|
|
1317
|
+
await this.forgetUnreadSessions([record]);
|
|
929
1318
|
}
|
|
930
1319
|
async deleteArchivedMany(refs) {
|
|
931
1320
|
if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
@@ -950,7 +1339,7 @@ export class PiSessionService {
|
|
|
950
1339
|
const readyRecords = [];
|
|
951
1340
|
for (const item of planItems) {
|
|
952
1341
|
try {
|
|
953
|
-
await this.closeActive(item.record.sessionId);
|
|
1342
|
+
await this.closeActive(item.record.sessionId, { kind: "clear", reason: "delete" });
|
|
954
1343
|
readyRecords.push(item.record);
|
|
955
1344
|
}
|
|
956
1345
|
catch (error) {
|
|
@@ -971,6 +1360,8 @@ export class PiSessionService {
|
|
|
971
1360
|
for (const sessionId of deleteIds)
|
|
972
1361
|
failures.push({ sessionId, error: errorMessage(error) });
|
|
973
1362
|
}
|
|
1363
|
+
const deletedIdSet = new Set(deletedSessionIds);
|
|
1364
|
+
await this.forgetUnreadSessions(readyRecords.filter((record) => deletedIdSet.has(record.sessionId)));
|
|
974
1365
|
return {
|
|
975
1366
|
deleted: true,
|
|
976
1367
|
deletedSessionIds,
|
|
@@ -983,9 +1374,29 @@ export class PiSessionService {
|
|
|
983
1374
|
const session = await this.getOrOpen(ref);
|
|
984
1375
|
if (this.hasActiveWork(session))
|
|
985
1376
|
throw new Error("Stop current session activity before reloading");
|
|
986
|
-
await this.
|
|
987
|
-
|
|
988
|
-
|
|
1377
|
+
const reopenedSession = await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before reloading", async () => {
|
|
1378
|
+
const priorGeneration = this.notificationGenerationBySession.get(session);
|
|
1379
|
+
const { sessionId, cwd } = notificationIdentityForSession(session);
|
|
1380
|
+
let candidateGeneration;
|
|
1381
|
+
try {
|
|
1382
|
+
await this.closeActive(sessionId, priorGeneration === undefined ? CLEAR_RUNTIME_NOTIFICATIONS : DEFER_RUNTIME_NOTIFICATIONS);
|
|
1383
|
+
candidateGeneration = priorGeneration === undefined
|
|
1384
|
+
? undefined
|
|
1385
|
+
: this.notificationStore.beginReplacement(priorGeneration, { sessionId, cwd });
|
|
1386
|
+
const reopened = await this.getActive(ref, candidateGeneration === undefined ? {} : { notificationGeneration: candidateGeneration });
|
|
1387
|
+
if (candidateGeneration !== undefined) {
|
|
1388
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1389
|
+
}
|
|
1390
|
+
return reopened.runtime.session;
|
|
1391
|
+
}
|
|
1392
|
+
catch (error) {
|
|
1393
|
+
if (candidateGeneration !== undefined) {
|
|
1394
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration));
|
|
1395
|
+
}
|
|
1396
|
+
throw error;
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
this.publishStatus(reopenedSession);
|
|
989
1400
|
}
|
|
990
1401
|
async detachParent(ref) {
|
|
991
1402
|
const session = await this.getOrOpen(ref);
|
|
@@ -995,6 +1406,21 @@ export class PiSessionService {
|
|
|
995
1406
|
await clearParentSession(sessionFile);
|
|
996
1407
|
clearParentSessionHeader(session.sessionManager);
|
|
997
1408
|
this.unregisterSubsession(session.sessionId);
|
|
1409
|
+
await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }]);
|
|
1410
|
+
}
|
|
1411
|
+
async clearQueue(ref) {
|
|
1412
|
+
await this.assertWritable(ref);
|
|
1413
|
+
const session = await this.getOrOpen(ref);
|
|
1414
|
+
this.clearCompactionPromptQueue(session.sessionId);
|
|
1415
|
+
clearSessionQueue(session);
|
|
1416
|
+
this.publishStatus(session);
|
|
1417
|
+
return this.statusFromSession(session);
|
|
1418
|
+
}
|
|
1419
|
+
async dismissWarning(ref, dismissId) {
|
|
1420
|
+
const session = await this.getOrOpen(ref);
|
|
1421
|
+
dismissSessionWarning(session, dismissId);
|
|
1422
|
+
this.publishStatus(session);
|
|
1423
|
+
return this.statusFromSession(session);
|
|
998
1424
|
}
|
|
999
1425
|
async abort(ref) {
|
|
1000
1426
|
const active = this.activeForLookup(ref);
|
|
@@ -1003,17 +1429,30 @@ export class PiSessionService {
|
|
|
1003
1429
|
const sessionId = active.runtime.session.sessionId;
|
|
1004
1430
|
this.clearCompactionPromptQueue(sessionId);
|
|
1005
1431
|
clearSessionQueue(active.runtime.session);
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1432
|
+
try {
|
|
1433
|
+
await this.abortSessionOperations(active.runtime.session);
|
|
1434
|
+
this.publishActivity(active.runtime.session, "stopped", "idle");
|
|
1435
|
+
}
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1438
|
+
this.publishActivity(active.runtime.session, "stop failed", "error", message);
|
|
1439
|
+
throw error;
|
|
1440
|
+
}
|
|
1441
|
+
finally {
|
|
1442
|
+
this.publishStatus(active.runtime.session);
|
|
1443
|
+
}
|
|
1009
1444
|
}
|
|
1010
|
-
stop(ref) {
|
|
1445
|
+
async stop(ref) {
|
|
1011
1446
|
const active = this.activeForLookup(ref);
|
|
1012
|
-
if (active
|
|
1447
|
+
if (active !== undefined) {
|
|
1448
|
+
await this.closeActive(active.runtime.session.sessionId);
|
|
1013
1449
|
return;
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1450
|
+
}
|
|
1451
|
+
if (isPiSessionRef(ref)) {
|
|
1452
|
+
this.publishNotificationMutations(this.notificationStore.clearSessionIdentity(ref.id, canonicalizeStoredCwd(ref.cwd), "runtime-close"));
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
await this.closeActive(ref);
|
|
1017
1456
|
}
|
|
1018
1457
|
async bulkSessionLookupContext(refs) {
|
|
1019
1458
|
const cwdSet = new Set();
|
|
@@ -1202,13 +1641,21 @@ export class PiSessionService {
|
|
|
1202
1641
|
}
|
|
1203
1642
|
return [...names];
|
|
1204
1643
|
}
|
|
1205
|
-
async closeActive(sessionId) {
|
|
1644
|
+
async closeActive(sessionId, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
|
|
1206
1645
|
const pendingOpens = this.pendingSessionOpenPromises(sessionId);
|
|
1207
1646
|
if (pendingOpens.length > 0)
|
|
1208
1647
|
await Promise.allSettled(pendingOpens);
|
|
1209
1648
|
const active = this.active.get(sessionId);
|
|
1649
|
+
if (notificationPolicy.kind === "clear") {
|
|
1650
|
+
const generation = active === undefined ? undefined : this.notificationGenerationBySession.get(active.runtime.session);
|
|
1651
|
+
const mutations = generation === undefined
|
|
1652
|
+
? this.notificationStore.clearSession(sessionId, notificationPolicy.reason)
|
|
1653
|
+
: this.notificationStore.clearGeneration(generation, notificationPolicy.reason);
|
|
1654
|
+
this.publishNotificationMutations(mutations);
|
|
1655
|
+
}
|
|
1210
1656
|
if (!active)
|
|
1211
1657
|
return;
|
|
1658
|
+
this.forgetUnreadActivity(active.runtime.session);
|
|
1212
1659
|
this.active.delete(sessionId);
|
|
1213
1660
|
this.activities.delete(sessionId);
|
|
1214
1661
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
|
|
@@ -1221,13 +1668,36 @@ export class PiSessionService {
|
|
|
1221
1668
|
this.subsessionNotifyArmed.delete(sessionId);
|
|
1222
1669
|
clearSessionQueue(active.runtime.session);
|
|
1223
1670
|
active.unsubscribe();
|
|
1671
|
+
active.runtime.setRebindSession(undefined);
|
|
1224
1672
|
try {
|
|
1225
|
-
await active.runtime.session
|
|
1673
|
+
await this.abortSessionOperations(active.runtime.session);
|
|
1226
1674
|
}
|
|
1227
1675
|
finally {
|
|
1228
1676
|
await active.runtime.dispose();
|
|
1229
1677
|
}
|
|
1230
1678
|
}
|
|
1679
|
+
async abortSessionOperations(session) {
|
|
1680
|
+
let branchSummaryAbortFailed = false;
|
|
1681
|
+
let branchSummaryAbortError;
|
|
1682
|
+
try {
|
|
1683
|
+
session.abortBranchSummary?.();
|
|
1684
|
+
}
|
|
1685
|
+
catch (error) {
|
|
1686
|
+
branchSummaryAbortFailed = true;
|
|
1687
|
+
branchSummaryAbortError = error;
|
|
1688
|
+
}
|
|
1689
|
+
try {
|
|
1690
|
+
await session.abort();
|
|
1691
|
+
}
|
|
1692
|
+
catch (abortError) {
|
|
1693
|
+
if (branchSummaryAbortFailed) {
|
|
1694
|
+
throw new AggregateError([branchSummaryAbortError, abortError], "Failed to abort session operations", { cause: abortError });
|
|
1695
|
+
}
|
|
1696
|
+
throw abortError;
|
|
1697
|
+
}
|
|
1698
|
+
if (branchSummaryAbortFailed)
|
|
1699
|
+
throw branchSummaryAbortError;
|
|
1700
|
+
}
|
|
1231
1701
|
async assertWritable(ref) {
|
|
1232
1702
|
if (await this.getArchived(ref) !== undefined)
|
|
1233
1703
|
throw new Error("Archived sessions are read-only. Restore the session to continue.");
|
|
@@ -1235,23 +1705,23 @@ export class PiSessionService {
|
|
|
1235
1705
|
async getOrOpen(ref) {
|
|
1236
1706
|
return (await this.getActive(ref)).runtime.session;
|
|
1237
1707
|
}
|
|
1238
|
-
async getActive(ref) {
|
|
1708
|
+
async getActive(ref, options = {}) {
|
|
1239
1709
|
const active = this.activeForLookup(ref);
|
|
1240
1710
|
if (active !== undefined)
|
|
1241
1711
|
return active;
|
|
1242
1712
|
const archived = await this.getArchived(ref);
|
|
1243
1713
|
if (archived?.archivePath !== undefined) {
|
|
1244
1714
|
const { archivePath } = archived;
|
|
1245
|
-
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath));
|
|
1715
|
+
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath), { notifications: "disabled" });
|
|
1246
1716
|
}
|
|
1247
1717
|
const match = isPiSessionRef(ref)
|
|
1248
1718
|
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
1249
1719
|
: (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
1250
1720
|
if (!match)
|
|
1251
1721
|
throw new Error("Session not found");
|
|
1252
|
-
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path));
|
|
1722
|
+
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
1253
1723
|
}
|
|
1254
|
-
openExistingSession(sessionId, cwd, openSessionManager) {
|
|
1724
|
+
openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
|
|
1255
1725
|
const active = this.activeForLookup({ id: sessionId, cwd });
|
|
1256
1726
|
if (active !== undefined)
|
|
1257
1727
|
return Promise.resolve(active);
|
|
@@ -1261,7 +1731,7 @@ export class PiSessionService {
|
|
|
1261
1731
|
return existing.promise;
|
|
1262
1732
|
const pending = {
|
|
1263
1733
|
sessionId,
|
|
1264
|
-
promise: this.create(openSessionManager(), cwd),
|
|
1734
|
+
promise: this.create(openSessionManager(), cwd, options),
|
|
1265
1735
|
};
|
|
1266
1736
|
pending.promise = pending.promise.finally(() => {
|
|
1267
1737
|
if (this.pendingSessionOpens.get(key) === pending)
|
|
@@ -1283,6 +1753,9 @@ export class PiSessionService {
|
|
|
1283
1753
|
return undefined;
|
|
1284
1754
|
return archived;
|
|
1285
1755
|
}
|
|
1756
|
+
isCurrentActiveSession(session) {
|
|
1757
|
+
return this.active.get(session.sessionId)?.runtime.session === session;
|
|
1758
|
+
}
|
|
1286
1759
|
activeForLookup(ref) {
|
|
1287
1760
|
const sessionId = sessionIdFromLookup(ref);
|
|
1288
1761
|
const exact = this.active.get(sessionId);
|
|
@@ -1305,21 +1778,83 @@ export class PiSessionService {
|
|
|
1305
1778
|
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
1306
1779
|
});
|
|
1307
1780
|
const active = { runtime, unsubscribe: noop };
|
|
1781
|
+
let boundSession = runtime.session;
|
|
1782
|
+
let notificationGeneration = options.notificationGeneration;
|
|
1783
|
+
let notificationOwnership = options.notifications === "disabled"
|
|
1784
|
+
? "disabled"
|
|
1785
|
+
: notificationGeneration === undefined
|
|
1786
|
+
? "registered"
|
|
1787
|
+
: "external";
|
|
1788
|
+
if (notificationOwnership === "registered") {
|
|
1789
|
+
const notificationIdentity = notificationIdentityForSession(runtime.session);
|
|
1790
|
+
const existingCandidate = this.notificationStore.beginReplacementForSession(notificationIdentity.sessionId, notificationIdentity.cwd);
|
|
1791
|
+
if (existingCandidate !== undefined) {
|
|
1792
|
+
notificationGeneration = existingCandidate;
|
|
1793
|
+
notificationOwnership = "replacement";
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
const registration = this.notificationStore.registerSession(notificationIdentity.sessionId, notificationIdentity.cwd);
|
|
1797
|
+
notificationGeneration = registration.generation;
|
|
1798
|
+
this.publishNotificationMutations(registration.mutations);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (notificationGeneration !== undefined)
|
|
1802
|
+
this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
|
|
1308
1803
|
try {
|
|
1309
|
-
|
|
1804
|
+
if (options.creationProvenance === "tracked-subsession") {
|
|
1805
|
+
await this.publishUnreadMutations(this.unreadStore.excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
|
|
1806
|
+
}
|
|
1807
|
+
else {
|
|
1808
|
+
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
1809
|
+
}
|
|
1810
|
+
await this.bindSessionExtensions(runtime.session, notificationGeneration);
|
|
1310
1811
|
this.bindRuntime(active);
|
|
1311
1812
|
runtime.setRebindSession(async (session) => {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1813
|
+
const priorGeneration = notificationGeneration;
|
|
1814
|
+
let candidateGeneration;
|
|
1815
|
+
try {
|
|
1816
|
+
await this.prepareUnreadRuntimeRebind(boundSession, session);
|
|
1817
|
+
await this.recoverSubsessionTrackingForOpenedSession(session);
|
|
1818
|
+
if (priorGeneration !== undefined) {
|
|
1819
|
+
candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
1820
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1821
|
+
}
|
|
1822
|
+
this.bindRuntime(active, session);
|
|
1823
|
+
boundSession = session;
|
|
1824
|
+
await this.bindSessionExtensions(session, candidateGeneration);
|
|
1825
|
+
if (candidateGeneration !== undefined) {
|
|
1826
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1827
|
+
notificationGeneration = candidateGeneration;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
catch (error) {
|
|
1831
|
+
if (candidateGeneration !== undefined) {
|
|
1832
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
|
|
1833
|
+
notificationGeneration = candidateGeneration;
|
|
1834
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1835
|
+
}
|
|
1836
|
+
throw error;
|
|
1837
|
+
}
|
|
1315
1838
|
});
|
|
1316
1839
|
this.active.set(runtime.session.sessionId, active);
|
|
1317
|
-
|
|
1840
|
+
if (notificationOwnership === "replacement" && notificationGeneration !== undefined) {
|
|
1841
|
+
this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
|
|
1842
|
+
notificationOwnership = "external";
|
|
1843
|
+
}
|
|
1318
1844
|
this.publishStatus(runtime.session);
|
|
1319
1845
|
return active;
|
|
1320
1846
|
}
|
|
1321
1847
|
catch (error) {
|
|
1848
|
+
if (notificationGeneration !== undefined) {
|
|
1849
|
+
if (notificationOwnership === "registered") {
|
|
1850
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(runtime.session.sessionId, "initialization-failed"));
|
|
1851
|
+
}
|
|
1852
|
+
else if (notificationOwnership === "replacement") {
|
|
1853
|
+
this.publishNotificationMutations(this.notificationStore.abortReplacement(notificationGeneration));
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1322
1856
|
active.unsubscribe();
|
|
1857
|
+
this.forgetUnreadActivity(boundSession);
|
|
1323
1858
|
let removedActive = false;
|
|
1324
1859
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
1325
1860
|
if (candidate !== active)
|
|
@@ -1342,8 +1877,11 @@ export class PiSessionService {
|
|
|
1342
1877
|
throw error;
|
|
1343
1878
|
}
|
|
1344
1879
|
}
|
|
1345
|
-
async bindSessionExtensions(session) {
|
|
1880
|
+
async bindSessionExtensions(session, generation) {
|
|
1881
|
+
const uiContext = this.sessionUiContext(session, generation);
|
|
1346
1882
|
await session.bindExtensions({
|
|
1883
|
+
uiContext,
|
|
1884
|
+
mode: "rpc",
|
|
1347
1885
|
onError: (error) => {
|
|
1348
1886
|
const message = `${error.extensionPath}: ${error.error}`;
|
|
1349
1887
|
this.publishActivity(session, "extension error", "error", message);
|
|
@@ -1351,9 +1889,157 @@ export class PiSessionService {
|
|
|
1351
1889
|
},
|
|
1352
1890
|
});
|
|
1353
1891
|
}
|
|
1354
|
-
|
|
1892
|
+
replaceSessionNotificationContext(session, generation) {
|
|
1893
|
+
session.extensionRunner.setUIContext(this.sessionUiContext(session, generation), "rpc");
|
|
1894
|
+
}
|
|
1895
|
+
sessionUiContext(session, generation) {
|
|
1896
|
+
const baseUiContext = session.extensionRunner.getUIContext();
|
|
1897
|
+
const notify = (message, type) => {
|
|
1898
|
+
if (generation === undefined) {
|
|
1899
|
+
this.events.publish(session.sessionId, {
|
|
1900
|
+
type: "command.output",
|
|
1901
|
+
level: type === "error" ? "error" : "info",
|
|
1902
|
+
message,
|
|
1903
|
+
});
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const added = this.notificationStore.addNotification(generation, message, type);
|
|
1907
|
+
this.publishNotificationMutations(added.mutations);
|
|
1908
|
+
if (added.notification === undefined)
|
|
1909
|
+
return;
|
|
1910
|
+
this.events.publish(session.sessionId, {
|
|
1911
|
+
type: "command.output",
|
|
1912
|
+
level: type === "error" ? "error" : "info",
|
|
1913
|
+
message,
|
|
1914
|
+
notificationId: added.notification.id,
|
|
1915
|
+
});
|
|
1916
|
+
};
|
|
1917
|
+
// PI WEB owns the browser-facing notification and text-formatting
|
|
1918
|
+
// boundaries. Delegate every other UI method to Pi's headless defaults so
|
|
1919
|
+
// unsupported dialogs cancel safely instead of hanging.
|
|
1920
|
+
return new Proxy(baseUiContext, {
|
|
1921
|
+
get(target, property, receiver) {
|
|
1922
|
+
if (property === "notify")
|
|
1923
|
+
return notify;
|
|
1924
|
+
if (property === "theme")
|
|
1925
|
+
return plainTextTheme;
|
|
1926
|
+
const value = Reflect.get(target, property, receiver);
|
|
1927
|
+
return value;
|
|
1928
|
+
},
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
publishNotificationMutations(mutations) {
|
|
1932
|
+
for (const mutation of mutations) {
|
|
1933
|
+
this.events.publish(mutation.sessionId, mutation.inboxEvent);
|
|
1934
|
+
this.events.publishNotificationSummary(mutation.summaryEvent);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
async prepareUnreadRuntimeRebind(previous, next) {
|
|
1938
|
+
const previousCwd = canonicalizeStoredCwd(previous.sessionManager.getCwd());
|
|
1939
|
+
this.unreadStore.forgetActivity(previous.sessionId, previousCwd);
|
|
1940
|
+
const nextCwd = canonicalizeStoredCwd(next.sessionManager.getCwd());
|
|
1941
|
+
if (previous.sessionId === next.sessionId && cwdPathsEqual(previousCwd, nextCwd))
|
|
1942
|
+
return;
|
|
1943
|
+
await this.publishUnreadMutations(this.unreadStore.forgetSession(previous.sessionId, previousCwd));
|
|
1944
|
+
}
|
|
1945
|
+
forgetUnreadActivity(session) {
|
|
1946
|
+
this.unreadStore.forgetActivity(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()));
|
|
1947
|
+
}
|
|
1948
|
+
async forgetUnreadSessions(identities) {
|
|
1949
|
+
const mutations = [];
|
|
1950
|
+
for (const identity of identities) {
|
|
1951
|
+
mutations.push(...this.unreadStore.forgetSession(identity.sessionId, canonicalizeStoredCwd(identity.cwd)));
|
|
1952
|
+
}
|
|
1953
|
+
await this.publishUnreadMutations(mutations);
|
|
1954
|
+
}
|
|
1955
|
+
observeUnreadActivityState(session) {
|
|
1956
|
+
const mutations = this.unreadStore.observeActivityState(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()), this.hasActiveWork(session));
|
|
1957
|
+
if (mutations.length === 0)
|
|
1958
|
+
return;
|
|
1959
|
+
void this.publishUnreadMutations(mutations).catch(() => undefined);
|
|
1960
|
+
}
|
|
1961
|
+
publishUnreadMutations(mutations) {
|
|
1962
|
+
this.enqueueUnreadMutations(mutations);
|
|
1963
|
+
this.unreadPublicationFlushRequested = true;
|
|
1964
|
+
if (this.unreadPublication === undefined && this.unreadPublicationRetryTimer !== undefined) {
|
|
1965
|
+
const failure = this.unreadPublicationFailure;
|
|
1966
|
+
return Promise.reject(failure instanceof Error
|
|
1967
|
+
? failure
|
|
1968
|
+
: new Error("Session unread publication is awaiting retry", { cause: failure }));
|
|
1969
|
+
}
|
|
1970
|
+
return this.ensureUnreadPublication();
|
|
1971
|
+
}
|
|
1972
|
+
ensureUnreadPublication() {
|
|
1973
|
+
const existing = this.unreadPublication;
|
|
1974
|
+
if (existing !== undefined)
|
|
1975
|
+
return existing;
|
|
1976
|
+
const publication = this.drainUnreadPublication();
|
|
1977
|
+
this.unreadPublication = publication;
|
|
1978
|
+
void publication.then(() => {
|
|
1979
|
+
if (this.unreadPublication === publication)
|
|
1980
|
+
this.unreadPublication = undefined;
|
|
1981
|
+
}, (error) => {
|
|
1982
|
+
if (this.unreadPublication === publication)
|
|
1983
|
+
this.unreadPublication = undefined;
|
|
1984
|
+
this.unreadPublicationFailure = error;
|
|
1985
|
+
this.logger.info({ error: error instanceof Error ? error.message : String(error) }, "failed to publish durable session unread mutations");
|
|
1986
|
+
this.scheduleUnreadPublicationRetry();
|
|
1987
|
+
});
|
|
1988
|
+
return publication;
|
|
1989
|
+
}
|
|
1990
|
+
async drainUnreadPublication() {
|
|
1991
|
+
while (this.unreadPublicationFlushRequested || this.pendingUnreadMutations.length > 0) {
|
|
1992
|
+
this.unreadPublicationFlushRequested = false;
|
|
1993
|
+
const batch = this.pendingUnreadMutations.splice(0);
|
|
1994
|
+
let publishedCount = 0;
|
|
1995
|
+
try {
|
|
1996
|
+
await this.unreadStore.flush();
|
|
1997
|
+
for (const mutation of batch) {
|
|
1998
|
+
this.events.publishGlobal(mutation.event);
|
|
1999
|
+
publishedCount += 1;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
catch (error) {
|
|
2003
|
+
this.prependUnreadMutations(batch.slice(publishedCount));
|
|
2004
|
+
this.unreadPublicationFlushRequested = true;
|
|
2005
|
+
throw error;
|
|
2006
|
+
}
|
|
2007
|
+
this.unreadPublicationFailure = undefined;
|
|
2008
|
+
this.clearUnreadPublicationRetry();
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
enqueueUnreadMutations(mutations) {
|
|
2012
|
+
this.pendingUnreadMutations.push(...mutations);
|
|
2013
|
+
this.trimPendingUnreadMutations();
|
|
2014
|
+
}
|
|
2015
|
+
prependUnreadMutations(mutations) {
|
|
2016
|
+
this.pendingUnreadMutations.unshift(...mutations);
|
|
2017
|
+
this.trimPendingUnreadMutations();
|
|
2018
|
+
}
|
|
2019
|
+
trimPendingUnreadMutations() {
|
|
2020
|
+
const excess = this.pendingUnreadMutations.length - MAX_PENDING_UNREAD_MUTATIONS;
|
|
2021
|
+
if (excess > 0)
|
|
2022
|
+
this.pendingUnreadMutations.splice(0, excess);
|
|
2023
|
+
}
|
|
2024
|
+
scheduleUnreadPublicationRetry() {
|
|
2025
|
+
if (this.unreadPublicationStopped || this.unreadPublicationRetryTimer !== undefined)
|
|
2026
|
+
return;
|
|
2027
|
+
const delay = this.unreadPublicationRetryDelayMs;
|
|
2028
|
+
this.unreadPublicationRetryDelayMs = Math.min(Math.max(delay * 2, this.unreadPublicationRetryInitialMs), Math.max(MAX_UNREAD_PUBLICATION_RETRY_MS, this.unreadPublicationRetryInitialMs));
|
|
2029
|
+
this.unreadPublicationRetryTimer = setTimeout(() => {
|
|
2030
|
+
this.unreadPublicationRetryTimer = undefined;
|
|
2031
|
+
void this.ensureUnreadPublication().catch(() => undefined);
|
|
2032
|
+
}, delay);
|
|
2033
|
+
this.unreadPublicationRetryTimer.unref();
|
|
2034
|
+
}
|
|
2035
|
+
clearUnreadPublicationRetry() {
|
|
2036
|
+
if (this.unreadPublicationRetryTimer !== undefined)
|
|
2037
|
+
clearTimeout(this.unreadPublicationRetryTimer);
|
|
2038
|
+
this.unreadPublicationRetryTimer = undefined;
|
|
2039
|
+
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
2040
|
+
}
|
|
2041
|
+
bindRuntime(active, session = active.runtime.session) {
|
|
1355
2042
|
active.unsubscribe();
|
|
1356
|
-
const { session } = active.runtime;
|
|
1357
2043
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
1358
2044
|
if (candidate === active) {
|
|
1359
2045
|
this.active.delete(sessionId);
|
|
@@ -1444,7 +2130,7 @@ export class PiSessionService {
|
|
|
1444
2130
|
const model = session.model;
|
|
1445
2131
|
if (model === undefined)
|
|
1446
2132
|
return;
|
|
1447
|
-
void generateShortSessionName(session.agent.
|
|
2133
|
+
void generateShortSessionName(session.agent.streamFunction, model, firstMessage).then((name) => {
|
|
1448
2134
|
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
1449
2135
|
}).catch(() => {
|
|
1450
2136
|
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
|
|
@@ -1453,14 +2139,38 @@ export class PiSessionService {
|
|
|
1453
2139
|
applyGeneratedSessionName(session, name) {
|
|
1454
2140
|
if (name === undefined || session.sessionName !== undefined)
|
|
1455
2141
|
return;
|
|
2142
|
+
if (this.treeNavigations.has(session)) {
|
|
2143
|
+
this.deferredGeneratedSessionNames.set(session, name);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
1456
2146
|
session.setSessionName(name);
|
|
1457
2147
|
this.publishSessionName(session);
|
|
1458
2148
|
}
|
|
2149
|
+
flushDeferredTreeNavigationWork(session) {
|
|
2150
|
+
const generatedName = this.deferredGeneratedSessionNames.get(session);
|
|
2151
|
+
this.deferredGeneratedSessionNames.delete(session);
|
|
2152
|
+
if (generatedName !== undefined) {
|
|
2153
|
+
try {
|
|
2154
|
+
this.applyGeneratedSessionName(session, generatedName);
|
|
2155
|
+
}
|
|
2156
|
+
catch (error) {
|
|
2157
|
+
this.logger.info({ sessionId: session.sessionId, error: error instanceof Error ? error.message : String(error) }, "failed to apply deferred session name");
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const notifications = this.deferredSubsessionNotifications.get(session) ?? [];
|
|
2161
|
+
this.deferredSubsessionNotifications.delete(session);
|
|
2162
|
+
for (const notification of notifications) {
|
|
2163
|
+
void this.deliverSubsessionNotification(session, notification).catch((error) => {
|
|
2164
|
+
this.logSubsessionNotificationFailure(notification.parentId, notification.childId, error);
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
1459
2168
|
applyAuthChange(change = {}) {
|
|
1460
|
-
|
|
2169
|
+
// ModelRuntime.login()/logout() refresh the shared runtime before AuthService
|
|
2170
|
+
// emits the change, so no refresh is needed here. Keeping this synchronous
|
|
2171
|
+
// also lets every active session observe the same committed auth snapshot.
|
|
1461
2172
|
for (const active of this.active.values()) {
|
|
1462
2173
|
const { session } = active.runtime;
|
|
1463
|
-
session.modelRegistry.refresh();
|
|
1464
2174
|
this.syncCurrentModelAuthWarning(session, change.removedProviderId);
|
|
1465
2175
|
this.publishStatus(session);
|
|
1466
2176
|
}
|
|
@@ -1472,10 +2182,10 @@ export class PiSessionService {
|
|
|
1472
2182
|
if (model.provider === "unknown" && model.id === "unknown")
|
|
1473
2183
|
return;
|
|
1474
2184
|
const warningKey = authLossWarningKey(session.sessionId, model.provider, model.id);
|
|
1475
|
-
const registered = session.
|
|
2185
|
+
const registered = session.modelRuntime.getModel(model.provider, model.id);
|
|
1476
2186
|
if (registered === undefined)
|
|
1477
2187
|
return;
|
|
1478
|
-
if (session.
|
|
2188
|
+
if (session.modelRuntime.hasConfiguredAuth(model.provider)) {
|
|
1479
2189
|
this.authLossWarnings.delete(warningKey);
|
|
1480
2190
|
return;
|
|
1481
2191
|
}
|
|
@@ -1523,6 +2233,10 @@ export class PiSessionService {
|
|
|
1523
2233
|
}
|
|
1524
2234
|
}
|
|
1525
2235
|
activityLabelFromStatus(session) {
|
|
2236
|
+
if (this.treeNavigations.has(session))
|
|
2237
|
+
return "navigating session tree";
|
|
2238
|
+
if (this.isSessionEntryMutationActive(session))
|
|
2239
|
+
return "updating session";
|
|
1526
2240
|
if (session.isCompacting)
|
|
1527
2241
|
return "compacting";
|
|
1528
2242
|
if (session.isBashRunning)
|
|
@@ -1534,7 +2248,92 @@ export class PiSessionService {
|
|
|
1534
2248
|
return "active";
|
|
1535
2249
|
}
|
|
1536
2250
|
hasActiveWork(session) {
|
|
1537
|
-
return
|
|
2251
|
+
return this.treeNavigations.has(session)
|
|
2252
|
+
|| this.isSessionEntryMutationActive(session)
|
|
2253
|
+
|| this.isTreeExclusiveOperationActive(session)
|
|
2254
|
+
|| sessionHasActiveWork(session, this.compactionQueuedMessages(session.sessionId).length);
|
|
2255
|
+
}
|
|
2256
|
+
async runTreeExclusiveOperation(targets, activeError, operation) {
|
|
2257
|
+
const sessionIds = new Set();
|
|
2258
|
+
const runtimes = new Set();
|
|
2259
|
+
const sessions = new Set();
|
|
2260
|
+
for (const target of targets) {
|
|
2261
|
+
const runtime = target.runtime ?? (target.session === undefined ? undefined : this.activeRuntimeForSession(target.session));
|
|
2262
|
+
const session = target.session ?? runtime?.session;
|
|
2263
|
+
if (session !== undefined && this.hasActiveWork(session))
|
|
2264
|
+
throw new Error(activeError);
|
|
2265
|
+
sessionIds.add(target.sessionId);
|
|
2266
|
+
if (runtime !== undefined)
|
|
2267
|
+
runtimes.add(runtime);
|
|
2268
|
+
if (session !== undefined)
|
|
2269
|
+
sessions.add(session);
|
|
2270
|
+
}
|
|
2271
|
+
for (const sessionId of sessionIds) {
|
|
2272
|
+
this.treeExclusiveSessionOperationCounts.set(sessionId, (this.treeExclusiveSessionOperationCounts.get(sessionId) ?? 0) + 1);
|
|
2273
|
+
}
|
|
2274
|
+
for (const runtime of runtimes) {
|
|
2275
|
+
this.treeExclusiveRuntimeOperationCounts.set(runtime, (this.treeExclusiveRuntimeOperationCounts.get(runtime) ?? 0) + 1);
|
|
2276
|
+
}
|
|
2277
|
+
for (const session of sessions)
|
|
2278
|
+
this.observeUnreadActivityState(session);
|
|
2279
|
+
try {
|
|
2280
|
+
return await operation();
|
|
2281
|
+
}
|
|
2282
|
+
finally {
|
|
2283
|
+
for (const runtime of runtimes)
|
|
2284
|
+
decrementWeakCount(this.treeExclusiveRuntimeOperationCounts, runtime);
|
|
2285
|
+
for (const sessionId of sessionIds)
|
|
2286
|
+
decrementMapCount(this.treeExclusiveSessionOperationCounts, sessionId);
|
|
2287
|
+
for (const session of sessions) {
|
|
2288
|
+
if (this.isCurrentActiveSession(session))
|
|
2289
|
+
this.observeUnreadActivityState(session);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
isTreeExclusiveSessionIdentityActive(sessionId) {
|
|
2294
|
+
return (this.treeExclusiveSessionOperationCounts.get(sessionId) ?? 0) > 0;
|
|
2295
|
+
}
|
|
2296
|
+
isTreeExclusiveOperationActive(session) {
|
|
2297
|
+
if (this.isTreeExclusiveSessionIdentityActive(session.sessionId))
|
|
2298
|
+
return true;
|
|
2299
|
+
const runtime = this.activeRuntimeForSession(session);
|
|
2300
|
+
return runtime !== undefined && (this.treeExclusiveRuntimeOperationCounts.get(runtime) ?? 0) > 0;
|
|
2301
|
+
}
|
|
2302
|
+
activeRuntimeForSession(session) {
|
|
2303
|
+
for (const active of new Set(this.active.values())) {
|
|
2304
|
+
if (active.runtime.session === session)
|
|
2305
|
+
return active.runtime;
|
|
2306
|
+
}
|
|
2307
|
+
return undefined;
|
|
2308
|
+
}
|
|
2309
|
+
assertTreeNavigationInactive(session, action) {
|
|
2310
|
+
if (this.treeNavigations.has(session))
|
|
2311
|
+
throw new Error(`Cannot ${action} while session tree navigation is active`);
|
|
2312
|
+
}
|
|
2313
|
+
async runSessionEntryMutation(session, action, operation) {
|
|
2314
|
+
this.beginSessionEntryMutation(session, action);
|
|
2315
|
+
try {
|
|
2316
|
+
return await operation();
|
|
2317
|
+
}
|
|
2318
|
+
finally {
|
|
2319
|
+
this.endSessionEntryMutation(session);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
beginSessionEntryMutation(session, action) {
|
|
2323
|
+
this.assertTreeNavigationInactive(session, action);
|
|
2324
|
+
this.sessionEntryMutationCounts.set(session, (this.sessionEntryMutationCounts.get(session) ?? 0) + 1);
|
|
2325
|
+
this.observeUnreadActivityState(session);
|
|
2326
|
+
}
|
|
2327
|
+
endSessionEntryMutation(session) {
|
|
2328
|
+
const remaining = (this.sessionEntryMutationCounts.get(session) ?? 1) - 1;
|
|
2329
|
+
if (remaining <= 0)
|
|
2330
|
+
this.sessionEntryMutationCounts.delete(session);
|
|
2331
|
+
else
|
|
2332
|
+
this.sessionEntryMutationCounts.set(session, remaining);
|
|
2333
|
+
this.observeUnreadActivityState(session);
|
|
2334
|
+
}
|
|
2335
|
+
isSessionEntryMutationActive(session) {
|
|
2336
|
+
return (this.sessionEntryMutationCounts.get(session) ?? 0) > 0;
|
|
1538
2337
|
}
|
|
1539
2338
|
publishActivityForEvent(session, event) {
|
|
1540
2339
|
const eventType = getString(event, "type");
|
|
@@ -1596,6 +2395,7 @@ export class PiSessionService {
|
|
|
1596
2395
|
this.workspaceActivity?.applySessionActivity(session.sessionManager.getCwd(), activity);
|
|
1597
2396
|
this.events.publish(session.sessionId, { type: "activity.update", activity });
|
|
1598
2397
|
this.events.publishGlobal({ type: "activity.update", activity });
|
|
2398
|
+
this.observeUnreadActivityState(session);
|
|
1599
2399
|
}
|
|
1600
2400
|
publishStatus(session) {
|
|
1601
2401
|
const status = this.statusFromSession(session);
|
|
@@ -1603,6 +2403,7 @@ export class PiSessionService {
|
|
|
1603
2403
|
this.workspaceActivity?.applySessionStatus(session.sessionManager.getCwd(), status);
|
|
1604
2404
|
this.events.publish(session.sessionId, { type: "status.update", status });
|
|
1605
2405
|
this.events.publishGlobal({ type: "status.update", status });
|
|
2406
|
+
this.observeUnreadActivityState(session);
|
|
1606
2407
|
}
|
|
1607
2408
|
clearStaleActiveActivity(session) {
|
|
1608
2409
|
const current = this.activities.get(session.sessionId);
|
|
@@ -1619,6 +2420,7 @@ export class PiSessionService {
|
|
|
1619
2420
|
const stats = session.getSessionStats();
|
|
1620
2421
|
const model = session.model === undefined ? undefined : modelToClientModel(session.model);
|
|
1621
2422
|
const contextUsage = session.getContextUsage();
|
|
2423
|
+
const warnings = this.warningsForSession(session);
|
|
1622
2424
|
return {
|
|
1623
2425
|
sessionId: session.sessionId,
|
|
1624
2426
|
persisted: sessionFileExists(session.sessionFile),
|
|
@@ -1633,8 +2435,23 @@ export class PiSessionService {
|
|
|
1633
2435
|
tokens: stats.tokens,
|
|
1634
2436
|
cost: stats.cost,
|
|
1635
2437
|
...(contextUsage === undefined ? {} : { contextUsage }),
|
|
2438
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
1636
2439
|
};
|
|
1637
2440
|
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Compute the live warning set for a session: runtime/resource diagnostics from
|
|
2443
|
+
* the active runtime (if any) plus the Anthropic subscription-auth notice. Read
|
|
2444
|
+
* fresh on each status publish so a rebuilt runtime or an auth/model change is
|
|
2445
|
+
* reflected without caching a stale snapshot.
|
|
2446
|
+
*/
|
|
2447
|
+
warningsForSession(session) {
|
|
2448
|
+
const runtime = this.active.get(session.sessionId)?.runtime;
|
|
2449
|
+
const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
|
|
2450
|
+
const anthropic = anthropicSubscriptionWarning(session, join(this.agentDir, "auth.json"));
|
|
2451
|
+
if (anthropic !== undefined)
|
|
2452
|
+
warnings.push(anthropic);
|
|
2453
|
+
return warnings;
|
|
2454
|
+
}
|
|
1638
2455
|
pendingMessageCount(session) {
|
|
1639
2456
|
return session.pendingMessageCount + this.compactionQueuedMessages(session.sessionId).length;
|
|
1640
2457
|
}
|
|
@@ -1699,6 +2516,12 @@ function modelToClientModel(model) {
|
|
|
1699
2516
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
1700
2517
|
};
|
|
1701
2518
|
}
|
|
2519
|
+
function notificationIdentityForSession(session) {
|
|
2520
|
+
return {
|
|
2521
|
+
sessionId: session.sessionId,
|
|
2522
|
+
cwd: canonicalizeStoredCwd(session.sessionManager.getCwd()),
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
1702
2525
|
function clientSessionFromListEntry(session) {
|
|
1703
2526
|
return {
|
|
1704
2527
|
id: session.id,
|
|
@@ -2096,11 +2919,13 @@ const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
|
|
|
2096
2919
|
const SUBSESSION_CHILD_LINK_CUSTOM_TYPE = "pi-web.subsession.spawned";
|
|
2097
2920
|
/** customType marking a parent-facing subsession-completion notice. */
|
|
2098
2921
|
const SUBSESSION_NOTIFICATION_CUSTOM_TYPE = "subsession.completion";
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2922
|
+
const SUBSESSION_NOTIFICATION_MAX_OUTPUT_CHARS = 2000;
|
|
2923
|
+
/** Avoid duplicating a partial result in context when deliberate inspection can return the full output. */
|
|
2924
|
+
function formatSubsessionNotificationOutput(childSessionId, text) {
|
|
2925
|
+
if (text.length > SUBSESSION_NOTIFICATION_MAX_OUTPUT_CHARS) {
|
|
2926
|
+
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.`;
|
|
2927
|
+
}
|
|
2928
|
+
return `--- SUBSESSION OUTPUT: ${childSessionId} ---\n${text === "" ? "(no output)" : text}`;
|
|
2104
2929
|
}
|
|
2105
2930
|
/** Most recent assistant text from a history message list, or "" if none. */
|
|
2106
2931
|
function finalAssistantText(messages) {
|