@hyperdreamer/pi-webui 1.10.7 → 1.11.0-beta.10
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 +17 -2
- package/dist/cli.js +265 -32
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-BIb2Wx8W.js → CodeViewer-CAf_j27u.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-DpWiGlQo.js → UnifiedDiffViewer-wBjEJWa1.js} +1 -1
- package/dist/client/assets/{index-DX583DEv.js → index-5EtDb9nj.js} +1197 -476
- package/dist/client/index.html +1 -1
- package/dist/config.js +52 -11
- package/dist/config.js.map +1 -1
- package/dist/pi-webui-plugins/workspace-memory/pi-webui-plugin.js +3 -2
- package/dist/pi-webui-plugins/workspace-tasks/pi-webui-plugin.js +1 -0
- package/dist/plugin-api.d.ts +30 -0
- package/dist/server/app.js +6 -1
- package/dist/server/app.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +40 -5
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/realtime/sessionStatusCoalescer.js +137 -0
- package/dist/server/realtime/sessionStatusCoalescer.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +2 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +57 -5
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/modelPolicyCapability.js +88 -0
- package/dist/server/sessions/modelPolicyCapability.js.map +1 -0
- package/dist/server/sessions/modelPolicyTool.js +31 -0
- package/dist/server/sessions/modelPolicyTool.js.map +1 -0
- package/dist/server/sessions/modelTierRegistry.js +14 -0
- package/dist/server/sessions/modelTierRegistry.js.map +1 -1
- package/dist/server/sessions/modelTierSettingsRoutes.js +36 -0
- package/dist/server/sessions/modelTierSettingsRoutes.js.map +1 -0
- package/dist/server/sessions/modelTierSettingsService.js +100 -0
- package/dist/server/sessions/modelTierSettingsService.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +1643 -433
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionDefaultsRoutes.js +39 -0
- package/dist/server/sessions/sessionDefaultsRoutes.js.map +1 -1
- package/dist/server/sessions/sessionDefaultsService.js +36 -6
- package/dist/server/sessions/sessionDefaultsService.js.map +1 -1
- package/dist/server/sessions/sessionMetadataStore.js +107 -13
- package/dist/server/sessions/sessionMetadataStore.js.map +1 -1
- package/dist/server/sessions/sessionModelPolicy.js +118 -0
- package/dist/server/sessions/sessionModelPolicy.js.map +1 -0
- package/dist/server/sessions/sessionReorder.js +117 -0
- package/dist/server/sessions/sessionReorder.js.map +1 -0
- package/dist/server/sessions/sessionRouteFastifyOptions.js +8 -0
- package/dist/server/sessions/sessionRouteFastifyOptions.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +208 -3
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/starterModelPolicyPreferenceStore.js +164 -0
- package/dist/server/sessions/starterModelPolicyPreferenceStore.js.map +1 -0
- package/dist/server/sessions/utilityModelExtension.js +199 -0
- package/dist/server/sessions/utilityModelExtension.js.map +1 -0
- package/dist/server/sessions/utilityModelResolver.js +77 -0
- package/dist/server/sessions/utilityModelResolver.js.map +1 -0
- package/dist/server/sessions/utilityModelSettingsRoutes.js +62 -0
- package/dist/server/sessions/utilityModelSettingsRoutes.js.map +1 -0
- package/dist/server/sessions/utilityModelSettingsService.js +101 -0
- package/dist/server/sessions/utilityModelSettingsService.js.map +1 -0
- package/dist/server/skills/optionalSkillInstall.js +69 -0
- package/dist/server/skills/optionalSkillInstall.js.map +1 -0
- package/dist/server/skills/optionalSkillInstaller.js +148 -0
- package/dist/server/skills/optionalSkillInstaller.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +129 -3
- package/dist/shared/apiTypes.js +10 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +15 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +7 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/docs/assets/pi-webui-desktop.png +0 -0
- package/docs/config.md +61 -3
- package/docs/plugins.md +93 -16
- package/optional-skills/deterministic-subagent-driven-development/SKILL.md +224 -0
- package/optional-skills/deterministic-subagent-driven-development/pi-webui-skill.json +28 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/final-reviewer.md +132 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/implementer.md +101 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/re-reviewer.md +60 -0
- package/optional-skills/deterministic-subagent-driven-development/prompts/task-reviewer.md +80 -0
- package/optional-skills/deterministic-subagent-driven-development/references/capability-contract.md +174 -0
- package/optional-skills/deterministic-subagent-driven-development/references/plan-contract.md +268 -0
- package/optional-skills/deterministic-subagent-driven-development/references/state-machine.md +177 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/manifest.mjs +258 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/plan-policy.mjs +350 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/prompt-renderer.mjs +290 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-machine.mjs +1264 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-store.mjs +532 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state +3 -0
- package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state.mjs +349 -0
- package/optional-skills/deterministic-writing-plans/SKILL.md +232 -0
- package/optional-skills/deterministic-writing-plans/references/grammar.md +84 -0
- package/optional-skills/deterministic-writing-plans/templates/plan-skeleton.md +143 -0
- package/package.json +13 -9
|
@@ -7,27 +7,37 @@ import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
|
7
7
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
8
8
|
import { exportSessionHistoryHtml } from "./sessionHistoryExport.js";
|
|
9
9
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
10
|
-
import { SessionCommandService, clientSessionFromRuntime } from "./sessionCommandService.js";
|
|
11
|
-
import { projectSessionTree } from "./sessionTreeProjection.js";
|
|
12
|
-
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
13
|
-
import { SessionMetadataStore } from "./sessionMetadataStore.js";
|
|
14
|
-
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
15
|
-
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
10
|
+
import { SessionCommandService, clientSessionFromRuntime, } from "./sessionCommandService.js";
|
|
11
|
+
import { projectSessionTree, } from "./sessionTreeProjection.js";
|
|
12
|
+
import { SessionArchiveStore, } from "./sessionArchiveStore.js";
|
|
13
|
+
import { SessionMetadataStore, SessionMetadataOrderConflictError } from "./sessionMetadataStore.js";
|
|
14
|
+
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree, } from "./sessionArchiveTree.js";
|
|
15
|
+
import { deterministicSessionName, fallbackSessionName, generateShortSessionName, } from "./sessionNameGenerator.js";
|
|
16
16
|
import { computeEditPreview } from "./editPreview.js";
|
|
17
|
-
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
17
|
+
import { attachmentsToInlineImages, saveAttachmentsToWorkspace, } from "./attachmentService.js";
|
|
18
18
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
19
19
|
import { isKnownThinkingLevel } from "../../shared/thinkingLevels.js";
|
|
20
|
-
import { SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
20
|
+
import { SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT, } from "../../shared/apiTypes.js";
|
|
21
|
+
import { assertSubmittedSessionsCurrent, SessionReorderDomainError, validateSessionReorder, } from "./sessionReorder.js";
|
|
21
22
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
22
|
-
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
23
|
-
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
23
|
+
import { createSpawnSessionToolDefinition, } from "./spawnSessionTool.js";
|
|
24
|
+
import { createSubsessionToolDefinitions, } from "./spawnSubsessionTool.js";
|
|
25
|
+
import { createModelPolicyToolDefinition, } from "./modelPolicyTool.js";
|
|
26
|
+
import { buildModelPolicyCapability, } from "./modelPolicyCapability.js";
|
|
24
27
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
25
|
-
import { planForceCleanup, planSessionCleanup, summarizeForceCleanupExecution, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
28
|
+
import { planForceCleanup, planSessionCleanup, summarizeForceCleanupExecution, summarizeSessionCleanupExecution, } from "./sessionCleanup.js";
|
|
26
29
|
import { SessionNotificationStore, } from "./sessionNotificationStore.js";
|
|
27
30
|
import { plainTextTheme } from "./plainTextTheme.js";
|
|
28
|
-
import { SessionUnreadStore } from "./sessionUnreadStore.js";
|
|
29
|
-
import { createModelTierRegistry, isModelTier, runtimeThinkingLevels } from "./modelTierRegistry.js";
|
|
30
|
-
|
|
31
|
+
import { SessionUnreadStore, } from "./sessionUnreadStore.js";
|
|
32
|
+
import { createModelTierRegistry, isModelTier, runtimeThinkingLevels, } from "./modelTierRegistry.js";
|
|
33
|
+
import { createUtilityModelExtension, } from "./utilityModelExtension.js";
|
|
34
|
+
import { createUtilityModelResolver, runWithUtilityModelFallback, } from "./utilityModelResolver.js";
|
|
35
|
+
import { inspectSessionModelPolicy, planSessionModelPolicyUpdate, serializeSessionModelPolicy, SESSION_MODEL_POLICY_CUSTOM_TYPE, } from "./sessionModelPolicy.js";
|
|
36
|
+
const noopLogger = {
|
|
37
|
+
info() {
|
|
38
|
+
/* no-op */
|
|
39
|
+
},
|
|
40
|
+
};
|
|
31
41
|
const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
|
|
32
42
|
const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
|
|
33
43
|
const MAX_PENDING_UNREAD_MUTATIONS = SESSION_UNREAD_LIMIT + 1;
|
|
@@ -89,13 +99,13 @@ function requirePromptText(value) {
|
|
|
89
99
|
throw new Error("Prompt text is required");
|
|
90
100
|
return value;
|
|
91
101
|
}
|
|
92
|
-
function
|
|
93
|
-
const match = /^(?:\uFEFF)
|
|
102
|
+
function leadingTierEcho(prompt) {
|
|
103
|
+
const match = /^(?:\uFEFF)?Model tier: ([^\s\r\n]+)(?:[ \t]*(?:\r?\n|$))/u.exec(prompt);
|
|
94
104
|
if (match === null)
|
|
95
105
|
return undefined;
|
|
96
106
|
const value = match[1];
|
|
97
107
|
if (value === undefined || !isModelTier(value))
|
|
98
|
-
throw new Error(`Unknown leading tier
|
|
108
|
+
throw new Error(`Unknown leading model tier label: ${value ?? ""}`);
|
|
99
109
|
return value;
|
|
100
110
|
}
|
|
101
111
|
function parsePromptStreamingBehavior(value) {
|
|
@@ -105,7 +115,10 @@ function parsePromptStreamingBehavior(value) {
|
|
|
105
115
|
return value;
|
|
106
116
|
throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
|
|
107
117
|
}
|
|
108
|
-
const CLEAR_RUNTIME_NOTIFICATIONS = {
|
|
118
|
+
const CLEAR_RUNTIME_NOTIFICATIONS = {
|
|
119
|
+
kind: "clear",
|
|
120
|
+
reason: "runtime-close",
|
|
121
|
+
};
|
|
109
122
|
const DEFER_RUNTIME_NOTIFICATIONS = { kind: "defer" };
|
|
110
123
|
function resourceDiagnosticToWarning(diagnostic, source) {
|
|
111
124
|
return {
|
|
@@ -116,7 +129,11 @@ function resourceDiagnosticToWarning(diagnostic, source) {
|
|
|
116
129
|
};
|
|
117
130
|
}
|
|
118
131
|
function runtimeDiagnosticToWarning(diagnostic) {
|
|
119
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
severity: diagnostic.type,
|
|
134
|
+
message: diagnostic.message,
|
|
135
|
+
source: "runtime",
|
|
136
|
+
};
|
|
120
137
|
}
|
|
121
138
|
/**
|
|
122
139
|
* Compute the live warnings for a runtime by re-reading its current resource
|
|
@@ -140,7 +157,12 @@ export function collectRuntimeWarnings(runtime) {
|
|
|
140
157
|
for (const diagnostic of resourceLoader.getThemes().diagnostics)
|
|
141
158
|
warnings.push(resourceDiagnosticToWarning(diagnostic, "theme"));
|
|
142
159
|
for (const error of resourceLoader.getExtensions().errors) {
|
|
143
|
-
warnings.push({
|
|
160
|
+
warnings.push({
|
|
161
|
+
severity: "error",
|
|
162
|
+
message: `${error.path}: ${error.error}`,
|
|
163
|
+
source: "extension",
|
|
164
|
+
path: error.path,
|
|
165
|
+
});
|
|
144
166
|
}
|
|
145
167
|
}
|
|
146
168
|
return warnings;
|
|
@@ -206,7 +228,10 @@ export function dismissSessionWarning(session, dismissId) {
|
|
|
206
228
|
if (dismissId !== ANTHROPIC_EXTRA_USAGE_DISMISS_ID) {
|
|
207
229
|
throw new Error(`Unknown session warning dismiss id: ${dismissId}`);
|
|
208
230
|
}
|
|
209
|
-
session.settingsManager.setWarnings({
|
|
231
|
+
session.settingsManager.setWarnings({
|
|
232
|
+
...session.settingsManager.getWarnings(),
|
|
233
|
+
anthropicExtraUsage: false,
|
|
234
|
+
});
|
|
210
235
|
}
|
|
211
236
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
212
237
|
if (!(options.sessionManager instanceof SessionManager))
|
|
@@ -234,32 +259,72 @@ function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, ini
|
|
|
234
259
|
return createRuntime({
|
|
235
260
|
...options,
|
|
236
261
|
...(model === undefined ? {} : { initialModel: model }),
|
|
237
|
-
...(thinkingLevel === undefined
|
|
238
|
-
|
|
262
|
+
...(thinkingLevel === undefined
|
|
263
|
+
? {}
|
|
264
|
+
: { initialThinkingLevel: thinkingLevel }),
|
|
265
|
+
...(toolsEnabled === undefined
|
|
266
|
+
? {}
|
|
267
|
+
: { delegationToolsEnabled: toolsEnabled }),
|
|
239
268
|
});
|
|
240
269
|
};
|
|
241
270
|
}
|
|
242
|
-
export function createPiWebUiCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions) {
|
|
271
|
+
export function createPiWebUiCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions, modelPolicy) {
|
|
243
272
|
return [
|
|
244
273
|
createPiWebUiEditToolDefinition(cwd),
|
|
245
|
-
...(delegationEnabled && spawn !== undefined
|
|
246
|
-
|
|
274
|
+
...(delegationEnabled && spawn !== undefined
|
|
275
|
+
? [createSpawnSessionToolDefinition(cwd, { spawn })]
|
|
276
|
+
: []),
|
|
277
|
+
...(delegationEnabled && subsessions !== undefined
|
|
278
|
+
? createSubsessionToolDefinitions(cwd, subsessions)
|
|
279
|
+
: []),
|
|
280
|
+
// Paired with the subsession tools: the policy view exists so a dispatching
|
|
281
|
+
// agent can verify tier dispatch before using it, so it is pointless without
|
|
282
|
+
// the dispatch capability and misleading if advertised separately.
|
|
283
|
+
...(delegationEnabled &&
|
|
284
|
+
subsessions !== undefined &&
|
|
285
|
+
modelPolicy !== undefined
|
|
286
|
+
? [createModelPolicyToolDefinition(modelPolicy)]
|
|
287
|
+
: []),
|
|
247
288
|
];
|
|
248
289
|
}
|
|
249
|
-
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
const
|
|
290
|
+
export function createDefaultRuntimeFactory(modelRuntime, sessionManagers, utilityModelResolver, logger, spawn, subsessions, modelPolicy, sdk = {
|
|
291
|
+
createServices: createAgentSessionServices,
|
|
292
|
+
createFromServices: createAgentSessionFromServices,
|
|
293
|
+
}) {
|
|
294
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled, }) => {
|
|
295
|
+
const runtimeRefs = {};
|
|
296
|
+
const services = await sdk.createServices({
|
|
297
|
+
cwd,
|
|
298
|
+
agentDir,
|
|
299
|
+
modelRuntime,
|
|
300
|
+
resourceLoaderOptions: {
|
|
301
|
+
extensionFactories: [
|
|
302
|
+
createUtilityModelExtension({
|
|
303
|
+
// Pi's ExtensionContext exposes Model<any>; production candidates still come from ModelRuntime's Model<Api> snapshot.
|
|
304
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
305
|
+
resolver: utilityModelResolver,
|
|
306
|
+
modelRuntime,
|
|
307
|
+
refs: runtimeRefs,
|
|
308
|
+
logger,
|
|
309
|
+
}),
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
runtimeRefs.settingsManager = services.settingsManager;
|
|
314
|
+
const resolvedDelegationToolsEnabled = delegationToolsEnabled ??
|
|
315
|
+
(await sessionAllowsDelegationTools(sessionManager, sessionManagers));
|
|
316
|
+
const customTools = createPiWebUiCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions, modelPolicy);
|
|
317
|
+
const result = await sdk.createFromServices({
|
|
256
318
|
services,
|
|
257
319
|
sessionManager,
|
|
258
320
|
customTools,
|
|
259
321
|
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
260
322
|
...(initialModel === undefined ? {} : { model: initialModel }),
|
|
261
|
-
...(initialThinkingLevel === undefined
|
|
323
|
+
...(initialThinkingLevel === undefined
|
|
324
|
+
? {}
|
|
325
|
+
: { thinkingLevel: initialThinkingLevel }),
|
|
262
326
|
});
|
|
327
|
+
runtimeRefs.streamFunction = result.session.agent.streamFunction;
|
|
263
328
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
264
329
|
};
|
|
265
330
|
}
|
|
@@ -269,16 +334,29 @@ function createPiWebUiEditToolDefinition(cwd) {
|
|
|
269
334
|
name: editTool.name,
|
|
270
335
|
label: editTool.label,
|
|
271
336
|
description: editTool.description,
|
|
272
|
-
...(editTool.promptSnippet === undefined
|
|
273
|
-
|
|
337
|
+
...(editTool.promptSnippet === undefined
|
|
338
|
+
? {}
|
|
339
|
+
: { promptSnippet: editTool.promptSnippet }),
|
|
340
|
+
...(editTool.promptGuidelines === undefined
|
|
341
|
+
? {}
|
|
342
|
+
: { promptGuidelines: editTool.promptGuidelines }),
|
|
274
343
|
parameters: editTool.parameters,
|
|
275
|
-
...(editTool.renderShell === undefined
|
|
276
|
-
|
|
277
|
-
|
|
344
|
+
...(editTool.renderShell === undefined
|
|
345
|
+
? {}
|
|
346
|
+
: { renderShell: editTool.renderShell }),
|
|
347
|
+
...(editTool.prepareArguments === undefined
|
|
348
|
+
? {}
|
|
349
|
+
: { prepareArguments: editTool.prepareArguments }),
|
|
350
|
+
...(editTool.executionMode === undefined
|
|
351
|
+
? {}
|
|
352
|
+
: { executionMode: editTool.executionMode }),
|
|
278
353
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
279
354
|
const preview = await computeEditPreview(params.path, params.edits, cwd);
|
|
280
355
|
if (signal?.aborted !== true) {
|
|
281
|
-
onUpdate?.({
|
|
356
|
+
onUpdate?.({
|
|
357
|
+
content: [{ type: "text", text: "Edit preview computed." }],
|
|
358
|
+
details: { preview },
|
|
359
|
+
});
|
|
282
360
|
}
|
|
283
361
|
return editTool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
284
362
|
},
|
|
@@ -291,6 +369,43 @@ export class PiSessionService {
|
|
|
291
369
|
this.pendingSessionOpens = new Map();
|
|
292
370
|
this.activities = new Map();
|
|
293
371
|
this.generationMetrics = new WeakMap();
|
|
372
|
+
this.modelPolicyInspections = new WeakMap();
|
|
373
|
+
this.modelPolicyLadderValidations = new WeakMap();
|
|
374
|
+
/**
|
|
375
|
+
* Reason the newest persisted policy entry is unusable. Recomputed on every
|
|
376
|
+
* inspection refresh, so repairing the entry clears it.
|
|
377
|
+
*/
|
|
378
|
+
this.modelPolicyEntryReasons = new WeakMap();
|
|
379
|
+
/**
|
|
380
|
+
* `MODEL_POLICY_BLOCKED` runtime state from an application whose restoration
|
|
381
|
+
* could not be proven. Deliberately *not* cleared by inspection refresh: only
|
|
382
|
+
* an explicit successful policy application clears it, because the runtime
|
|
383
|
+
* tuple — not the persisted entry — is what became ambiguous.
|
|
384
|
+
*
|
|
385
|
+
* Keyed by session *id* rather than by the runtime session object so the block
|
|
386
|
+
* is daemon-owned: it survives a runtime rebind, a reload, and a close/reopen
|
|
387
|
+
* of the same session, none of which prove anything about the ambiguous tuple.
|
|
388
|
+
*/
|
|
389
|
+
this.modelPolicyRuntimeBlocks = new Map();
|
|
390
|
+
/**
|
|
391
|
+
* Sessions inside the apply-and-persist window of a policy transition. During
|
|
392
|
+
* that window the runtime tuple is transient (model set, thinking not yet, or
|
|
393
|
+
* persistence not yet confirmed), so no prompt may reach Pi.
|
|
394
|
+
*/
|
|
395
|
+
this.modelPolicyMutationCounts = new Map();
|
|
396
|
+
/**
|
|
397
|
+
* Tuple last *confirmed* by the policy runtime adapter, captured when the
|
|
398
|
+
* outermost policy mutation opens and dropped when it closes.
|
|
399
|
+
*
|
|
400
|
+
* A status may be published from inside the transition window (the runtime
|
|
401
|
+
* subscription and the heartbeat both reach `publishStatus`), where the live
|
|
402
|
+
* runtime tuple is mid-apply: the model is set but the thinking level is not,
|
|
403
|
+
* so reading it would report a pair that was never requested and never
|
|
404
|
+
* persisted. `ClientSessionModelPolicyStatus.resolved` promises the last
|
|
405
|
+
* confirmed tuple, so the window reports this instead of live state. Keyed by
|
|
406
|
+
* session id to match the mutation counter it is scoped by.
|
|
407
|
+
*/
|
|
408
|
+
this.modelPolicyConfirmedSelections = new Map();
|
|
294
409
|
/** Runtime-identity gate held while Pi may await abandoned-branch summarization. */
|
|
295
410
|
this.treeNavigations = new WeakSet();
|
|
296
411
|
/** Counts async operations that may append an entry before they settle. */
|
|
@@ -321,42 +436,90 @@ export class PiSessionService {
|
|
|
321
436
|
this.pendingUnreadMutations = [];
|
|
322
437
|
this.unreadPublicationFlushRequested = false;
|
|
323
438
|
this.unreadPublicationStopped = false;
|
|
439
|
+
/**
|
|
440
|
+
* Serializes reorder against every other mutation that can change sibling
|
|
441
|
+
* membership, pin state, or current/archive membership (pin, unpin,
|
|
442
|
+
* detach-parent, archive, archiveMany, archiveTree, restore, and cleanup),
|
|
443
|
+
* so no such mutation can interleave with a reorder's before/after catalog
|
|
444
|
+
* reads and its atomic metadata write.
|
|
445
|
+
*/
|
|
446
|
+
this.sessionOrderMutationQueue = Promise.resolve();
|
|
324
447
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
325
448
|
this.metadataStore = deps.metadataStore ?? new SessionMetadataStore();
|
|
449
|
+
this.clearParentSessionFile = deps.clearParentSession ?? clearParentSession;
|
|
326
450
|
this.agentDir = deps.agentDir;
|
|
327
451
|
this.sessionManager = deps.sessionManager;
|
|
328
452
|
this.modelRuntime = deps.modelRuntime;
|
|
329
453
|
this.spawnTargets = deps.spawnTargets;
|
|
330
454
|
this.logger = deps.logger ?? noopLogger;
|
|
455
|
+
this.utilityModelResolver =
|
|
456
|
+
deps.utilityModelResolver ??
|
|
457
|
+
createUtilityModelResolver({
|
|
458
|
+
loadConfig: () => {
|
|
459
|
+
const loaded = loadPiWebUiConfig();
|
|
460
|
+
return {
|
|
461
|
+
...(loaded.config.utilityModels === undefined
|
|
462
|
+
? {}
|
|
463
|
+
: { utilityModels: loaded.config.utilityModels }),
|
|
464
|
+
...(loaded.utilityModelsError === undefined
|
|
465
|
+
? {}
|
|
466
|
+
: { utilityModelsError: loaded.utilityModelsError }),
|
|
467
|
+
};
|
|
468
|
+
},
|
|
469
|
+
modelRuntime: this.modelRuntime,
|
|
470
|
+
logger: this.logger,
|
|
471
|
+
});
|
|
331
472
|
this.now = deps.now ?? (() => new Date());
|
|
332
|
-
this.notificationStore =
|
|
473
|
+
this.notificationStore =
|
|
474
|
+
deps.notificationStore ?? new SessionNotificationStore();
|
|
333
475
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
334
476
|
this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
|
|
335
477
|
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
336
478
|
// Subsessions are a beta capability gated behind their own flag, and they
|
|
337
479
|
// also require the spawn capability (they share its project-scope resolver).
|
|
338
480
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
339
|
-
this.createRuntime =
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
481
|
+
this.createRuntime =
|
|
482
|
+
deps.createRuntime ??
|
|
483
|
+
createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.utilityModelResolver, this.logger, this.spawnTargets === undefined
|
|
484
|
+
? undefined
|
|
485
|
+
: (input) => this.spawnSession(input), !subsessionsActive
|
|
486
|
+
? undefined
|
|
487
|
+
: {
|
|
488
|
+
spawn: (input) => this.spawnSubsession(input),
|
|
489
|
+
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
490
|
+
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
491
|
+
read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
|
|
492
|
+
}, !subsessionsActive
|
|
493
|
+
? undefined
|
|
494
|
+
: {
|
|
495
|
+
inspect: (sessionId) => this.modelPolicyCapability(sessionId),
|
|
496
|
+
});
|
|
497
|
+
this.createAgentRuntime =
|
|
498
|
+
deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
499
|
+
this.modelTierRegistry =
|
|
500
|
+
deps.modelTierRegistry ??
|
|
501
|
+
createModelTierRegistry({
|
|
502
|
+
loadConfig: () => {
|
|
503
|
+
const loaded = loadPiWebUiConfig();
|
|
504
|
+
return {
|
|
505
|
+
...(loaded.config.modelTiers === undefined
|
|
506
|
+
? {}
|
|
507
|
+
: { modelTiers: loaded.config.modelTiers }),
|
|
508
|
+
...(loaded.modelTiersError === undefined
|
|
509
|
+
? {}
|
|
510
|
+
: { modelTiersError: loaded.modelTiersError }),
|
|
511
|
+
};
|
|
512
|
+
},
|
|
513
|
+
models: () => this.modelRuntime.getAvailableSnapshot(),
|
|
514
|
+
supportedThinkingLevels: runtimeThinkingLevels,
|
|
515
|
+
});
|
|
357
516
|
this.workspaceActivity = deps.workspaceActivity;
|
|
358
|
-
this.heartbeat = setInterval(() => {
|
|
359
|
-
|
|
517
|
+
this.heartbeat = setInterval(() => {
|
|
518
|
+
this.publishHeartbeats();
|
|
519
|
+
}, deps.heartbeatIntervalMs ?? 2000);
|
|
520
|
+
this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, {
|
|
521
|
+
echoUserMessage: false,
|
|
522
|
+
}), events, {
|
|
360
523
|
onCompactionStart: (session) => {
|
|
361
524
|
this.beginSessionEntryMutation(session, "compact the session");
|
|
362
525
|
this.publishActivity(session, "compacting", "active");
|
|
@@ -369,7 +532,8 @@ export class PiSessionService {
|
|
|
369
532
|
},
|
|
370
533
|
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
371
534
|
getSessionTree: (session) => {
|
|
372
|
-
if (typeof session.sessionManager.getTree !== "function" ||
|
|
535
|
+
if (typeof session.sessionManager.getTree !== "function" ||
|
|
536
|
+
typeof session.navigateTree !== "function")
|
|
373
537
|
return undefined;
|
|
374
538
|
return projectSessionTree(session.sessionManager.getTree(), session.sessionManager.getLeafId());
|
|
375
539
|
},
|
|
@@ -413,47 +577,58 @@ export class PiSessionService {
|
|
|
413
577
|
return previewResponseFromPlan(await this.cleanupPlan(request));
|
|
414
578
|
}
|
|
415
579
|
async cleanup(request) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
580
|
+
return await this.runSessionOrderMutation(async () => {
|
|
581
|
+
const plan = await this.cleanupPlan(request);
|
|
582
|
+
if (plan.deleteRecords.length > 0 &&
|
|
583
|
+
this.archiveStore.deleteArchived === undefined &&
|
|
584
|
+
this.archiveStore.deleteArchivedMany === undefined)
|
|
585
|
+
throw new Error("Archive store does not support deletion");
|
|
586
|
+
const archiveInputs = [];
|
|
587
|
+
const readyArchiveInputs = [];
|
|
588
|
+
const deleteRecords = [];
|
|
589
|
+
const readyDeleteRecords = [];
|
|
590
|
+
const skippedBusySessionIds = new Set(plan.skippedBusySessionIds);
|
|
591
|
+
for (const input of plan.archiveInputs) {
|
|
592
|
+
if (this.activeSessionHasWork(input.sessionId)) {
|
|
593
|
+
skippedBusySessionIds.add(input.sessionId);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
await this.closeActive(input.sessionId, {
|
|
597
|
+
kind: "clear",
|
|
598
|
+
reason: "archive",
|
|
599
|
+
});
|
|
600
|
+
readyArchiveInputs.push(input);
|
|
428
601
|
}
|
|
429
|
-
await this.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
602
|
+
await this.archiveStoreArchiveMany(readyArchiveInputs);
|
|
603
|
+
archiveInputs.push(...readyArchiveInputs);
|
|
604
|
+
await this.forgetUnreadSessions(readyArchiveInputs);
|
|
605
|
+
for (const record of plan.deleteRecords) {
|
|
606
|
+
if (this.activeSessionHasWork(record.sessionId)) {
|
|
607
|
+
skippedBusySessionIds.add(record.sessionId);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
await this.closeActive(record.sessionId, {
|
|
611
|
+
kind: "clear",
|
|
612
|
+
reason: "delete",
|
|
613
|
+
});
|
|
614
|
+
readyDeleteRecords.push(record);
|
|
439
615
|
}
|
|
440
|
-
await this.
|
|
441
|
-
readyDeleteRecords.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
generatedAt: plan.generatedAt,
|
|
452
|
-
skippedBusySessionIds: [...skippedBusySessionIds],
|
|
616
|
+
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
617
|
+
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
618
|
+
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
619
|
+
await this.forgetUnreadSessions(deleteRecords);
|
|
620
|
+
return summarizeSessionCleanupExecution({
|
|
621
|
+
archiveInputs,
|
|
622
|
+
deleteRecords,
|
|
623
|
+
thresholds: plan.thresholds,
|
|
624
|
+
generatedAt: plan.generatedAt,
|
|
625
|
+
skippedBusySessionIds: [...skippedBusySessionIds],
|
|
626
|
+
});
|
|
453
627
|
});
|
|
454
628
|
}
|
|
455
629
|
async forceCleanup() {
|
|
456
|
-
if (this.archiveStore.deleteArchived === undefined &&
|
|
630
|
+
if (this.archiveStore.deleteArchived === undefined &&
|
|
631
|
+
this.archiveStore.deleteArchivedMany === undefined)
|
|
457
632
|
throw new Error("Archive store does not support deletion");
|
|
458
633
|
const archivedRecords = await this.archiveStore.list();
|
|
459
634
|
const plan = planForceCleanup({
|
|
@@ -468,7 +643,10 @@ export class PiSessionService {
|
|
|
468
643
|
skippedBusySessionIds.add(record.sessionId);
|
|
469
644
|
continue;
|
|
470
645
|
}
|
|
471
|
-
await this.closeActive(record.sessionId, {
|
|
646
|
+
await this.closeActive(record.sessionId, {
|
|
647
|
+
kind: "clear",
|
|
648
|
+
reason: "delete",
|
|
649
|
+
});
|
|
472
650
|
readyDeleteRecords.push(record);
|
|
473
651
|
}
|
|
474
652
|
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
@@ -496,6 +674,9 @@ export class PiSessionService {
|
|
|
496
674
|
this.pendingSessionOpens.clear();
|
|
497
675
|
this.activities.clear();
|
|
498
676
|
this.compactionPromptQueues.clear();
|
|
677
|
+
this.modelPolicyRuntimeBlocks.clear();
|
|
678
|
+
this.modelPolicyMutationCounts.clear();
|
|
679
|
+
this.modelPolicyConfirmedSelections.clear();
|
|
499
680
|
this.authLossWarnings.clear();
|
|
500
681
|
this.subsessionParents.clear();
|
|
501
682
|
this.subsessionChildren.clear();
|
|
@@ -517,12 +698,68 @@ export class PiSessionService {
|
|
|
517
698
|
await this.publishUnreadMutations([]);
|
|
518
699
|
}
|
|
519
700
|
async list(cwd) {
|
|
520
|
-
const [sessions, archivedRecords,
|
|
701
|
+
const [sessions, archivedRecords, metadata] = await Promise.all([
|
|
521
702
|
this.sessionManager.list(cwd),
|
|
522
703
|
this.archiveStore.list(),
|
|
523
|
-
this.metadataStore.
|
|
704
|
+
this.metadataStore.snapshot(),
|
|
705
|
+
]);
|
|
706
|
+
return this.listFromSnapshots(cwd, sessions, archivedRecords, metadata);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Run `operation` inside the exclusive ordering queue shared by every
|
|
710
|
+
* mutation that can change sibling membership, pin state, or
|
|
711
|
+
* current/archive membership, so reorder's before/after catalog reads
|
|
712
|
+
* never interleave with a concurrent pin, detach, archive, or restore.
|
|
713
|
+
*/
|
|
714
|
+
async runSessionOrderMutation(operation) {
|
|
715
|
+
const previous = this.sessionOrderMutationQueue;
|
|
716
|
+
let release = () => undefined;
|
|
717
|
+
this.sessionOrderMutationQueue = new Promise((resolve) => { release = resolve; });
|
|
718
|
+
await previous.catch(() => undefined);
|
|
719
|
+
try {
|
|
720
|
+
return await operation();
|
|
721
|
+
}
|
|
722
|
+
finally {
|
|
723
|
+
release();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async reorder(ref, request) {
|
|
727
|
+
return await this.runSessionOrderMutation(async () => {
|
|
728
|
+
const target = { id: ref.id, cwd: canonicalizeStoredCwd(ref.cwd) };
|
|
729
|
+
const before = await this.listReorderCatalog(request.catalogCwds);
|
|
730
|
+
const validated = validateSessionReorder(target, request, uniqueSessionsByIdentity(before));
|
|
731
|
+
try {
|
|
732
|
+
await this.metadataStore.replaceOrder(validated.sessionPaths, request.scope, request.pinned);
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
if (error instanceof SessionMetadataOrderConflictError) {
|
|
736
|
+
throw new SessionReorderDomainError("conflict", error.message);
|
|
737
|
+
}
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
const after = await this.listReorderCatalog(request.catalogCwds);
|
|
741
|
+
assertSubmittedSessionsCurrent(target, request, uniqueSessionsByIdentity(after));
|
|
742
|
+
return validated.response;
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Read one coherent metadata/archive view across every requested CWD.
|
|
747
|
+
* Gateway session-list reads run in parallel per CWD, but the metadata and
|
|
748
|
+
* archive snapshots are each taken exactly once for the whole call, so the
|
|
749
|
+
* result is one atomic before/after catalog rather than one per CWD.
|
|
750
|
+
*/
|
|
751
|
+
async listReorderCatalog(cwds) {
|
|
752
|
+
const metadataPromise = this.metadataStore.snapshot();
|
|
753
|
+
const archivePromise = this.archiveStore.list();
|
|
754
|
+
const sessionListsPromise = Promise.all(cwds.map((cwd) => this.sessionManager.list(cwd)));
|
|
755
|
+
const [metadata, archivedRecords, sessionLists] = await Promise.all([
|
|
756
|
+
metadataPromise,
|
|
757
|
+
archivePromise,
|
|
758
|
+
sessionListsPromise,
|
|
524
759
|
]);
|
|
525
|
-
|
|
760
|
+
return (await Promise.all(cwds.map((cwd, index) => this.listFromSnapshots(cwd, sessionLists[index] ?? [], archivedRecords, metadata)))).flat();
|
|
761
|
+
}
|
|
762
|
+
async listFromSnapshots(cwd, sessions, archivedRecords, metadata) {
|
|
526
763
|
const sessionsById = new Map(sessions.map((session) => [session.id, session]));
|
|
527
764
|
const archivedForCwd = await Promise.all(archivedRecords
|
|
528
765
|
.filter((record) => record.cwd === cwd)
|
|
@@ -533,7 +770,7 @@ export class PiSessionService {
|
|
|
533
770
|
}
|
|
534
771
|
const unarchivedSessions = sessions
|
|
535
772
|
.filter((session) => !archivedById.has(session.id))
|
|
536
|
-
.map((entry) => mergeSessionMetadata(clientSessionFromListEntry(entry),
|
|
773
|
+
.map((entry) => mergeSessionMetadata(clientSessionFromListEntry(entry), metadata));
|
|
537
774
|
const reconcilableSessionIds = this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById);
|
|
538
775
|
this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds);
|
|
539
776
|
await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
|
|
@@ -541,17 +778,31 @@ export class PiSessionService {
|
|
|
541
778
|
.sort(compareArchivedRecords)
|
|
542
779
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
543
780
|
.filter(isDefined)
|
|
544
|
-
.map((session) => mergeSessionMetadata(session,
|
|
781
|
+
.map((session) => mergeSessionMetadata(session, metadata));
|
|
545
782
|
return [...unarchivedSessions, ...archivedSessions];
|
|
546
783
|
}
|
|
547
|
-
async start(cwd, options
|
|
548
|
-
|
|
784
|
+
async start(cwd, options) {
|
|
785
|
+
const modelPolicy = options?.modelPolicy;
|
|
786
|
+
return this.startSession(cwd, {
|
|
787
|
+
initializeModelPolicy: modelPolicy ?? true,
|
|
788
|
+
});
|
|
549
789
|
}
|
|
550
790
|
async startSession(cwd, options) {
|
|
551
|
-
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
...(options.
|
|
791
|
+
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined
|
|
792
|
+
? undefined
|
|
793
|
+
: { parentSession: options.parentSession }), cwd, {
|
|
794
|
+
...(options.initialModel === undefined
|
|
795
|
+
? {}
|
|
796
|
+
: { initialModel: options.initialModel }),
|
|
797
|
+
...(options.initialThinkingLevel === undefined
|
|
798
|
+
? {}
|
|
799
|
+
: { initialThinkingLevel: options.initialThinkingLevel }),
|
|
800
|
+
...(options.creationProvenance === undefined
|
|
801
|
+
? {}
|
|
802
|
+
: { creationProvenance: options.creationProvenance }),
|
|
803
|
+
...(options.initializeModelPolicy === undefined
|
|
804
|
+
? {}
|
|
805
|
+
: { initializeModelPolicy: options.initializeModelPolicy }),
|
|
555
806
|
});
|
|
556
807
|
const { session } = active.runtime;
|
|
557
808
|
const created = {
|
|
@@ -565,7 +816,9 @@ export class PiSessionService {
|
|
|
565
816
|
firstMessage: "",
|
|
566
817
|
// Include the parent so listeners can nest the new session in the tree
|
|
567
818
|
// immediately, instead of showing it flat until the next reload.
|
|
568
|
-
...(options.parentSession === undefined
|
|
819
|
+
...(options.parentSession === undefined
|
|
820
|
+
? {}
|
|
821
|
+
: { parentSessionPath: options.parentSession }),
|
|
569
822
|
};
|
|
570
823
|
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
571
824
|
// session to their list without a manual reload.
|
|
@@ -583,9 +836,14 @@ export class PiSessionService {
|
|
|
583
836
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
584
837
|
if (!decision.allowed)
|
|
585
838
|
throw spawnTargetError(decision);
|
|
586
|
-
const created = await this.
|
|
839
|
+
const created = await this.startSession(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
|
|
587
840
|
await this.prompt(created.id, input.prompt);
|
|
588
|
-
this.logger.info({
|
|
841
|
+
this.logger.info({
|
|
842
|
+
spawningCwd: input.spawningCwd,
|
|
843
|
+
sessionId: created.id,
|
|
844
|
+
cwd: decision.cwd,
|
|
845
|
+
promptLength: input.prompt.length,
|
|
846
|
+
}, "spawn_session started a new session");
|
|
589
847
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
590
848
|
}
|
|
591
849
|
/**
|
|
@@ -603,9 +861,9 @@ export class PiSessionService {
|
|
|
603
861
|
let initialModel = input.model;
|
|
604
862
|
let initialThinkingLevel;
|
|
605
863
|
if (input.tier !== undefined) {
|
|
606
|
-
const echoedTier =
|
|
864
|
+
const echoedTier = leadingTierEcho(input.prompt);
|
|
607
865
|
if (echoedTier !== undefined && echoedTier !== input.tier) {
|
|
608
|
-
throw new Error(`Leading tier
|
|
866
|
+
throw new Error(`Leading Model tier: ${echoedTier} label disagrees with typed tier ${input.tier}`);
|
|
609
867
|
}
|
|
610
868
|
const resolved = this.modelTierRegistry.resolve(input.tier);
|
|
611
869
|
if (!isKnownThinkingLevel(resolved.thinkingLevel)) {
|
|
@@ -615,7 +873,9 @@ export class PiSessionService {
|
|
|
615
873
|
initialThinkingLevel = resolved.thinkingLevel;
|
|
616
874
|
}
|
|
617
875
|
const created = await this.startSession(decision.cwd, {
|
|
618
|
-
...(input.parentSessionFile === undefined
|
|
876
|
+
...(input.parentSessionFile === undefined
|
|
877
|
+
? {}
|
|
878
|
+
: { parentSession: input.parentSessionFile }),
|
|
619
879
|
...(initialModel === undefined ? {} : { initialModel }),
|
|
620
880
|
...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
|
|
621
881
|
creationProvenance: "tracked-subsession",
|
|
@@ -632,7 +892,12 @@ export class PiSessionService {
|
|
|
632
892
|
this.persistSubsessionLink(link);
|
|
633
893
|
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
634
894
|
await this.prompt(created.id, input.prompt);
|
|
635
|
-
this.logger.info({
|
|
895
|
+
this.logger.info({
|
|
896
|
+
parentSessionId: input.parentSessionId,
|
|
897
|
+
sessionId: created.id,
|
|
898
|
+
cwd: decision.cwd,
|
|
899
|
+
promptLength: input.prompt.length,
|
|
900
|
+
}, "spawn_subsession started a tracked child session");
|
|
636
901
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
637
902
|
}
|
|
638
903
|
/** Summaries of the tracked subsessions spawned by `parentSessionId`. */
|
|
@@ -643,7 +908,10 @@ export class PiSessionService {
|
|
|
643
908
|
if (childIds === undefined)
|
|
644
909
|
return [];
|
|
645
910
|
const authorizedChildIds = [...childIds].filter((childId) => this.subsessionLinkBelongsToParent(parentSessionId, parentFile, childId));
|
|
646
|
-
return Promise.all(authorizedChildIds.map(async (childId) => ({
|
|
911
|
+
return Promise.all(authorizedChildIds.map(async (childId) => ({
|
|
912
|
+
sessionId: childId,
|
|
913
|
+
...(await this.subsessionSummaryFields(childId)),
|
|
914
|
+
})));
|
|
647
915
|
}
|
|
648
916
|
/** Status and final result of a subsession, scoped to the caller's children. */
|
|
649
917
|
async checkSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
@@ -672,7 +940,8 @@ export class PiSessionService {
|
|
|
672
940
|
async openSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
673
941
|
const parentFile = nonEmptyString(parentSessionFile);
|
|
674
942
|
await this.hydrateSubsessionsForParent(parentSessionId, parentFile);
|
|
675
|
-
if (this.subsessionParents.get(sessionId) !== parentSessionId ||
|
|
943
|
+
if (this.subsessionParents.get(sessionId) !== parentSessionId ||
|
|
944
|
+
!this.subsessionLinkBelongsToParent(parentSessionId, parentFile, sessionId)) {
|
|
676
945
|
throw new Error(`Session ${sessionId} is not one of your subsessions`);
|
|
677
946
|
}
|
|
678
947
|
return this.getOrOpenTrackedSubsession(sessionId);
|
|
@@ -681,19 +950,24 @@ export class PiSessionService {
|
|
|
681
950
|
const link = this.subsessionLinks.get(childSessionId);
|
|
682
951
|
if (link?.parentSessionId !== parentSessionId)
|
|
683
952
|
return false;
|
|
684
|
-
return parentSessionFile === undefined ||
|
|
953
|
+
return (parentSessionFile === undefined ||
|
|
954
|
+
trackedLinkParentFileMatches(link, parentSessionFile));
|
|
685
955
|
}
|
|
686
956
|
activeChildForSubsessionLink(link) {
|
|
687
957
|
const active = this.active.get(link.childSessionId);
|
|
688
958
|
if (active === undefined)
|
|
689
959
|
return undefined;
|
|
690
|
-
return activeSessionFileMatches(active, link.childSessionFile)
|
|
960
|
+
return activeSessionFileMatches(active, link.childSessionFile)
|
|
961
|
+
? active
|
|
962
|
+
: undefined;
|
|
691
963
|
}
|
|
692
964
|
activeParentForSubsessionLink(link) {
|
|
693
965
|
const active = this.active.get(link.parentSessionId);
|
|
694
966
|
if (active === undefined)
|
|
695
967
|
return undefined;
|
|
696
|
-
return activeSessionFileMatches(active, link.parentSessionFile)
|
|
968
|
+
return activeSessionFileMatches(active, link.parentSessionFile)
|
|
969
|
+
? active
|
|
970
|
+
: undefined;
|
|
697
971
|
}
|
|
698
972
|
subsessionLinkForActiveChild(session) {
|
|
699
973
|
const childId = session.sessionId;
|
|
@@ -701,12 +975,15 @@ export class PiSessionService {
|
|
|
701
975
|
const link = this.subsessionLinks.get(childId);
|
|
702
976
|
if (parentId === undefined || link?.parentSessionId !== parentId)
|
|
703
977
|
return undefined;
|
|
704
|
-
return sessionFileMatches(session, link.childSessionFile)
|
|
978
|
+
return sessionFileMatches(session, link.childSessionFile)
|
|
979
|
+
? link
|
|
980
|
+
: undefined;
|
|
705
981
|
}
|
|
706
982
|
async registerVerifiedSubsession(link) {
|
|
707
983
|
const { childSessionId, parentSessionId } = link;
|
|
708
984
|
const previousParentId = this.subsessionParents.get(childSessionId);
|
|
709
|
-
if (previousParentId !== undefined &&
|
|
985
|
+
if (previousParentId !== undefined &&
|
|
986
|
+
previousParentId !== parentSessionId) {
|
|
710
987
|
const previousChildren = this.subsessionChildren.get(previousParentId);
|
|
711
988
|
previousChildren?.delete(childSessionId);
|
|
712
989
|
if (previousChildren?.size === 0)
|
|
@@ -760,7 +1037,11 @@ export class PiSessionService {
|
|
|
760
1037
|
parent.sessionManager.appendCustomEntry(SUBSESSION_LINK_CUSTOM_TYPE, persistedParentSubsessionLinkData(link));
|
|
761
1038
|
}
|
|
762
1039
|
catch (error) {
|
|
763
|
-
this.logger.info({
|
|
1040
|
+
this.logger.info({
|
|
1041
|
+
parentSessionId: link.parentSessionId,
|
|
1042
|
+
sessionId: link.childSessionId,
|
|
1043
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1044
|
+
}, "failed to persist subsession link");
|
|
764
1045
|
}
|
|
765
1046
|
}
|
|
766
1047
|
persistSubsessionChildMarker(parentSessionId, childSessionId) {
|
|
@@ -773,7 +1054,11 @@ export class PiSessionService {
|
|
|
773
1054
|
child.sessionManager.appendCustomEntry(SUBSESSION_CHILD_LINK_CUSTOM_TYPE, persistedChildSubsessionLinkData(parentSessionId, childSessionId));
|
|
774
1055
|
}
|
|
775
1056
|
catch (error) {
|
|
776
|
-
this.logger.info({
|
|
1057
|
+
this.logger.info({
|
|
1058
|
+
parentSessionId,
|
|
1059
|
+
sessionId: childSessionId,
|
|
1060
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1061
|
+
}, "failed to persist subsession child marker");
|
|
777
1062
|
}
|
|
778
1063
|
}
|
|
779
1064
|
async hydrateSubsessionsForParent(parentSessionId, parentSessionFile) {
|
|
@@ -781,7 +1066,9 @@ export class PiSessionService {
|
|
|
781
1066
|
if (this.subsessionHydratedParents.has(hydrationKey))
|
|
782
1067
|
return;
|
|
783
1068
|
const activeParent = this.active.get(parentSessionId);
|
|
784
|
-
if (activeParent !== undefined &&
|
|
1069
|
+
if (activeParent !== undefined &&
|
|
1070
|
+
(parentSessionFile === undefined ||
|
|
1071
|
+
activeSessionFileMatches(activeParent, parentSessionFile))) {
|
|
785
1072
|
const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
|
|
786
1073
|
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
787
1074
|
if (complete)
|
|
@@ -790,7 +1077,8 @@ export class PiSessionService {
|
|
|
790
1077
|
}
|
|
791
1078
|
if (parentSessionFile === undefined)
|
|
792
1079
|
return;
|
|
793
|
-
if ((await readSessionHeaderSummary(parentSessionFile))?.id !==
|
|
1080
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !==
|
|
1081
|
+
parentSessionId)
|
|
794
1082
|
return;
|
|
795
1083
|
let parentManager;
|
|
796
1084
|
try {
|
|
@@ -832,8 +1120,11 @@ export class PiSessionService {
|
|
|
832
1120
|
return trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile);
|
|
833
1121
|
}
|
|
834
1122
|
async parentLinkHasValidChildTarget(parentSessionFile, link) {
|
|
835
|
-
return link.spawnedSessionFile !== undefined
|
|
836
|
-
|
|
1123
|
+
return (link.spawnedSessionFile !== undefined &&
|
|
1124
|
+
(await sessionFileHeaderMatches(link.spawnedSessionFile, {
|
|
1125
|
+
sessionId: link.spawnedSessionId,
|
|
1126
|
+
parentSessionFile,
|
|
1127
|
+
})));
|
|
837
1128
|
}
|
|
838
1129
|
async recoverSubsessionTrackingForOpenedSession(session) {
|
|
839
1130
|
const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
|
|
@@ -857,7 +1148,10 @@ export class PiSessionService {
|
|
|
857
1148
|
if (active !== undefined)
|
|
858
1149
|
return active.runtime.session;
|
|
859
1150
|
if (link.childSessionFile !== undefined) {
|
|
860
|
-
if (!(await sessionFileHeaderMatches(link.childSessionFile, {
|
|
1151
|
+
if (!(await sessionFileHeaderMatches(link.childSessionFile, {
|
|
1152
|
+
sessionId,
|
|
1153
|
+
parentSessionFile: link.parentSessionFile,
|
|
1154
|
+
})))
|
|
861
1155
|
throw new Error("Session not found");
|
|
862
1156
|
const sessionManager = this.sessionManager.open(link.childSessionFile);
|
|
863
1157
|
return (await this.create(sessionManager, link.cwd ?? sessionManager.getCwd())).runtime.session;
|
|
@@ -868,9 +1162,16 @@ export class PiSessionService {
|
|
|
868
1162
|
const link = this.subsessionLinks.get(childSessionId);
|
|
869
1163
|
const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
|
|
870
1164
|
if (active !== undefined) {
|
|
871
|
-
return {
|
|
1165
|
+
return {
|
|
1166
|
+
cwd: active.runtime.cwd,
|
|
1167
|
+
status: this.subsessionStatus(active.runtime.session),
|
|
1168
|
+
};
|
|
872
1169
|
}
|
|
873
|
-
if (link?.childSessionFile !== undefined &&
|
|
1170
|
+
if (link?.childSessionFile !== undefined &&
|
|
1171
|
+
(await sessionFileHeaderMatches(link.childSessionFile, {
|
|
1172
|
+
sessionId: childSessionId,
|
|
1173
|
+
parentSessionFile: link.parentSessionFile,
|
|
1174
|
+
}))) {
|
|
874
1175
|
return { cwd: link.cwd ?? "", status: "idle" };
|
|
875
1176
|
}
|
|
876
1177
|
if (link?.cwd !== undefined)
|
|
@@ -890,7 +1191,9 @@ export class PiSessionService {
|
|
|
890
1191
|
return [];
|
|
891
1192
|
return [...childIds].filter((childId) => {
|
|
892
1193
|
const link = this.subsessionLinks.get(childId);
|
|
893
|
-
const active = link === undefined
|
|
1194
|
+
const active = link === undefined
|
|
1195
|
+
? undefined
|
|
1196
|
+
: this.activeChildForSubsessionLink(link);
|
|
894
1197
|
return active !== undefined && this.hasActiveWork(active.runtime.session);
|
|
895
1198
|
});
|
|
896
1199
|
}
|
|
@@ -932,11 +1235,13 @@ export class PiSessionService {
|
|
|
932
1235
|
const parentSessionFile = link.parentSessionFile;
|
|
933
1236
|
if (parentSessionFile === undefined)
|
|
934
1237
|
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
935
|
-
if ((await readSessionHeaderSummary(parentSessionFile))?.id !==
|
|
1238
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !==
|
|
1239
|
+
parentSessionId) {
|
|
936
1240
|
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
937
1241
|
}
|
|
938
1242
|
const sessionManager = this.sessionManager.open(parentSessionFile);
|
|
939
|
-
return (await this.create(sessionManager, sessionManager.getCwd())).runtime
|
|
1243
|
+
return (await this.create(sessionManager, sessionManager.getCwd())).runtime
|
|
1244
|
+
.session;
|
|
940
1245
|
}
|
|
941
1246
|
/**
|
|
942
1247
|
* Deliver a subsession-completion notice to the parent as a system-authored
|
|
@@ -954,18 +1259,31 @@ export class PiSessionService {
|
|
|
954
1259
|
this.deferredSubsessionNotifications.set(session, pending);
|
|
955
1260
|
return;
|
|
956
1261
|
}
|
|
957
|
-
await this.deliverSubsessionNotification(session, {
|
|
1262
|
+
await this.deliverSubsessionNotification(session, {
|
|
1263
|
+
parentId,
|
|
1264
|
+
childId,
|
|
1265
|
+
text,
|
|
1266
|
+
});
|
|
958
1267
|
}
|
|
959
1268
|
catch (error) {
|
|
960
1269
|
this.logSubsessionNotificationFailure(parentId, childId, error);
|
|
961
1270
|
}
|
|
962
1271
|
}
|
|
963
1272
|
async deliverSubsessionNotification(session, notification) {
|
|
964
|
-
await this.runSessionEntryMutation(session, "deliver a subsession notification", () => session.sendCustomMessage({
|
|
1273
|
+
await this.runSessionEntryMutation(session, "deliver a subsession notification", () => session.sendCustomMessage({
|
|
1274
|
+
customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE,
|
|
1275
|
+
content: notification.text,
|
|
1276
|
+
display: true,
|
|
1277
|
+
details: { sessionId: notification.childId },
|
|
1278
|
+
}, { triggerTurn: true, deliverAs: "followUp" }));
|
|
965
1279
|
this.publishStatus(session);
|
|
966
1280
|
}
|
|
967
1281
|
logSubsessionNotificationFailure(parentId, childId, error) {
|
|
968
|
-
this.logger.info({
|
|
1282
|
+
this.logger.info({
|
|
1283
|
+
parentSessionId: parentId,
|
|
1284
|
+
sessionId: childId,
|
|
1285
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1286
|
+
}, "failed to notify parent of subsession completion");
|
|
969
1287
|
}
|
|
970
1288
|
async exportHistory(ref) {
|
|
971
1289
|
return exportSessionHistoryHtml(await this.sessionFileForHistory(ref));
|
|
@@ -977,6 +1295,43 @@ export class PiSessionService {
|
|
|
977
1295
|
async status(ref) {
|
|
978
1296
|
return this.statusFromSession(await this.getOrOpen(ref));
|
|
979
1297
|
}
|
|
1298
|
+
async modelPolicy(ref) {
|
|
1299
|
+
const session = await this.getOrOpen(ref);
|
|
1300
|
+
const inspection = this.inspectAndCacheSessionModelPolicy(session);
|
|
1301
|
+
return {
|
|
1302
|
+
contractVersion: 1,
|
|
1303
|
+
...(inspection.kind === "invalid" ? {} : { policy: inspection.policy }),
|
|
1304
|
+
session: this.statusFromSession(session),
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* The only interface that changes a session's Exact/Tiered policy. Applies one
|
|
1309
|
+
* complete transition atomically and publishes a single confirmed status; a
|
|
1310
|
+
* corrupt newest entry is repaired only by the explicit update supplied here,
|
|
1311
|
+
* never executed as a silent fallback. A transition that fails after the plan
|
|
1312
|
+
* is resolved publishes one error/blocked status before the route error, so
|
|
1313
|
+
* other clients learn the session refused the change.
|
|
1314
|
+
*/
|
|
1315
|
+
async setModelPolicy(ref, update) {
|
|
1316
|
+
const action = "change the session model policy";
|
|
1317
|
+
await this.assertWritable(ref);
|
|
1318
|
+
const session = await this.getOrOpen(ref);
|
|
1319
|
+
this.assertModelPolicyMutationIdle(session, action);
|
|
1320
|
+
const inspection = this.inspectAndCacheSessionModelPolicy(session);
|
|
1321
|
+
// A malformed newest entry contributes only a starting exact branch for the
|
|
1322
|
+
// new explicit entry; it is never treated as an executable current policy.
|
|
1323
|
+
const current = inspection.kind === "invalid" ? inspection.fallback : inspection.policy;
|
|
1324
|
+
const policy = await this.applySessionModelPolicy(session, current, update, {
|
|
1325
|
+
action,
|
|
1326
|
+
onTransitionFailure: (reason) => {
|
|
1327
|
+
this.publishModelPolicyTransitionFailure(session, reason);
|
|
1328
|
+
},
|
|
1329
|
+
});
|
|
1330
|
+
const resolved = this.exactSelectionFromSession(session);
|
|
1331
|
+
this.publishActivity(session, `model policy: ${policy.mode}`, "idle", policy.mode === "tiered" ? policy.tier : resolved.model.id);
|
|
1332
|
+
this.publishStatus(session);
|
|
1333
|
+
return this.modelPolicy(ref);
|
|
1334
|
+
}
|
|
980
1335
|
async systemPrompt(ref) {
|
|
981
1336
|
const systemPrompt = (await this.getOrOpen(ref)).state.systemPrompt;
|
|
982
1337
|
return systemPrompt === undefined ? {} : { systemPrompt };
|
|
@@ -1011,17 +1366,16 @@ export class PiSessionService {
|
|
|
1011
1366
|
async setModel(ref, provider, modelId) {
|
|
1012
1367
|
await this.assertWritable(ref);
|
|
1013
1368
|
const session = await this.getOrOpen(ref);
|
|
1014
|
-
this.
|
|
1369
|
+
const currentPolicy = this.assertExactModelPolicyMutationAllowed(session, "change models");
|
|
1015
1370
|
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1016
|
-
this.
|
|
1371
|
+
this.assertModelPolicyMutationIdle(session, "change models");
|
|
1017
1372
|
const candidates = session.scopedModels.length > 0
|
|
1018
1373
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
1019
1374
|
: session.modelRuntime.getAvailableSnapshot();
|
|
1020
|
-
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
|
|
1021
|
-
?? session.modelRuntime.getModel(provider, modelId);
|
|
1375
|
+
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId) ?? session.modelRuntime.getModel(provider, modelId);
|
|
1022
1376
|
if (model === undefined)
|
|
1023
1377
|
throw new Error(`Model not found: ${provider}/${modelId}`);
|
|
1024
|
-
await this.
|
|
1378
|
+
await this.runExactModelPolicyMutation(session, "change models", currentPolicy, () => session.setModel(model));
|
|
1025
1379
|
this.publishActivity(session, `model: ${model.id}`, "idle", model.provider);
|
|
1026
1380
|
this.publishStatus(session);
|
|
1027
1381
|
return this.statusFromSession(session);
|
|
@@ -1029,9 +1383,16 @@ export class PiSessionService {
|
|
|
1029
1383
|
async cycleModel(ref, direction) {
|
|
1030
1384
|
await this.assertWritable(ref);
|
|
1031
1385
|
const session = await this.getOrOpen(ref);
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
|
|
1386
|
+
const currentPolicy = this.assertExactModelPolicyMutationAllowed(session, "change models");
|
|
1387
|
+
const result = await this.runExactModelPolicyMutation(session, "change models", currentPolicy, async () => {
|
|
1388
|
+
const cycled = await session.cycleModel(direction);
|
|
1389
|
+
if (cycled === undefined) {
|
|
1390
|
+
throw new Error(session.scopedModels.length > 0
|
|
1391
|
+
? "Only one model in scope"
|
|
1392
|
+
: "Only one model available");
|
|
1393
|
+
}
|
|
1394
|
+
return cycled;
|
|
1395
|
+
});
|
|
1035
1396
|
this.publishActivity(session, `model: ${result.model.id}`, "idle", result.model.provider);
|
|
1036
1397
|
this.publishStatus(session);
|
|
1037
1398
|
return this.statusFromSession(session);
|
|
@@ -1043,14 +1404,17 @@ export class PiSessionService {
|
|
|
1043
1404
|
async setThinkingLevel(ref, level) {
|
|
1044
1405
|
await this.assertWritable(ref);
|
|
1045
1406
|
const session = await this.getOrOpen(ref);
|
|
1046
|
-
this.
|
|
1407
|
+
const currentPolicy = this.assertExactModelPolicyMutationAllowed(session, "change the thinking level");
|
|
1047
1408
|
// pi owns the valid set; validate against the session's live levels rather
|
|
1048
1409
|
// than a hardcoded union so this stays correct if pi changes the set.
|
|
1049
1410
|
const available = session.getAvailableThinkingLevels();
|
|
1050
1411
|
const match = available.find((candidate) => candidate === level);
|
|
1051
1412
|
if (match === undefined)
|
|
1052
1413
|
throw new Error(`Invalid thinking level: ${level}`);
|
|
1053
|
-
|
|
1414
|
+
await this.runExactModelPolicyMutation(session, "change the thinking level", currentPolicy, () => {
|
|
1415
|
+
session.setThinkingLevel(match);
|
|
1416
|
+
return Promise.resolve();
|
|
1417
|
+
});
|
|
1054
1418
|
this.publishActivity(session, `thinking: ${session.thinkingLevel}`, "idle");
|
|
1055
1419
|
this.publishStatus(session);
|
|
1056
1420
|
return this.statusFromSession(session);
|
|
@@ -1058,10 +1422,13 @@ export class PiSessionService {
|
|
|
1058
1422
|
async cycleThinkingLevel(ref) {
|
|
1059
1423
|
await this.assertWritable(ref);
|
|
1060
1424
|
const session = await this.getOrOpen(ref);
|
|
1061
|
-
this.
|
|
1062
|
-
const level =
|
|
1063
|
-
|
|
1064
|
-
|
|
1425
|
+
const currentPolicy = this.assertExactModelPolicyMutationAllowed(session, "change the thinking level");
|
|
1426
|
+
const level = await this.runExactModelPolicyMutation(session, "change the thinking level", currentPolicy, () => {
|
|
1427
|
+
const cycled = session.cycleThinkingLevel();
|
|
1428
|
+
if (cycled === undefined)
|
|
1429
|
+
throw new Error("Current model does not support thinking");
|
|
1430
|
+
return Promise.resolve(cycled);
|
|
1431
|
+
});
|
|
1065
1432
|
this.publishActivity(session, `thinking: ${level}`, "idle");
|
|
1066
1433
|
this.publishStatus(session);
|
|
1067
1434
|
return this.statusFromSession(session);
|
|
@@ -1070,13 +1437,31 @@ export class PiSessionService {
|
|
|
1070
1437
|
const session = await this.getOrOpen(ref);
|
|
1071
1438
|
const commands = [...BUILTIN_COMMANDS];
|
|
1072
1439
|
for (const command of session.extensionRunner.getRegisteredCommands()) {
|
|
1073
|
-
commands.push({
|
|
1440
|
+
commands.push({
|
|
1441
|
+
name: command.invocationName,
|
|
1442
|
+
...(command.description === undefined
|
|
1443
|
+
? {}
|
|
1444
|
+
: { description: command.description }),
|
|
1445
|
+
source: "extension",
|
|
1446
|
+
});
|
|
1074
1447
|
}
|
|
1075
1448
|
for (const template of session.promptTemplates) {
|
|
1076
|
-
commands.push({
|
|
1449
|
+
commands.push({
|
|
1450
|
+
name: template.name,
|
|
1451
|
+
...(template.description === undefined
|
|
1452
|
+
? {}
|
|
1453
|
+
: { description: template.description }),
|
|
1454
|
+
source: "prompt",
|
|
1455
|
+
});
|
|
1077
1456
|
}
|
|
1078
1457
|
for (const skill of session.resourceLoader.getSkills().skills) {
|
|
1079
|
-
commands.push({
|
|
1458
|
+
commands.push({
|
|
1459
|
+
name: `skill:${skill.name}`,
|
|
1460
|
+
...(skill.description === undefined
|
|
1461
|
+
? {}
|
|
1462
|
+
: { description: skill.description }),
|
|
1463
|
+
source: "skill",
|
|
1464
|
+
});
|
|
1080
1465
|
}
|
|
1081
1466
|
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
1082
1467
|
}
|
|
@@ -1088,15 +1473,20 @@ export class PiSessionService {
|
|
|
1088
1473
|
// as a transient line that vanishes on reload.
|
|
1089
1474
|
const echoUserMessage = options?.echoUserMessage !== false;
|
|
1090
1475
|
const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
|
|
1091
|
-
const parsedAttachments = parsePromptAttachments(attachments, {
|
|
1476
|
+
const parsedAttachments = parsePromptAttachments(attachments, {
|
|
1477
|
+
enforceInlineSizeLimit: false,
|
|
1478
|
+
});
|
|
1092
1479
|
const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
|
|
1093
1480
|
await this.assertWritable(ref);
|
|
1094
1481
|
const session = await this.getOrOpen(ref);
|
|
1095
1482
|
this.assertTreeNavigationInactive(session, "send a prompt");
|
|
1483
|
+
this.assertPromptModelPolicyAllowed(session);
|
|
1096
1484
|
this.maybeGenerateSessionName(session, promptText);
|
|
1097
1485
|
const isQueued = session.isStreaming || session.isCompacting;
|
|
1098
1486
|
const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
|
|
1099
|
-
if (isQueued &&
|
|
1487
|
+
if (isQueued &&
|
|
1488
|
+
images.length === 0 &&
|
|
1489
|
+
this.hasQueuedMessageText(session, promptText)) {
|
|
1100
1490
|
this.publishActivity(session, "duplicate queued message ignored", "active");
|
|
1101
1491
|
this.publishStatus(session);
|
|
1102
1492
|
return;
|
|
@@ -1105,30 +1495,95 @@ export class PiSessionService {
|
|
|
1105
1495
|
this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp", images, echoUserMessage);
|
|
1106
1496
|
return;
|
|
1107
1497
|
}
|
|
1498
|
+
if (this.isModelPolicyMutationActive(session)) {
|
|
1499
|
+
this.retainPromptDuringModelPolicyMutation(session, promptText, behavior ?? "followUp", images, echoUserMessage);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1108
1502
|
void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
|
|
1109
1503
|
}
|
|
1110
1504
|
submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1111
|
-
|
|
1505
|
+
// A policy transition may have opened while this prompt waited in the
|
|
1506
|
+
// compaction queue. The runtime tuple is transient until the transition
|
|
1507
|
+
// persists, so retain the input instead of reaching Pi with a partial pair.
|
|
1508
|
+
if (this.isModelPolicyMutationActive(session)) {
|
|
1509
|
+
this.retainPromptDuringModelPolicyMutation(session, text, behavior ?? "followUp", images, echoUserMessage, "front");
|
|
1510
|
+
return Promise.resolve();
|
|
1511
|
+
}
|
|
1512
|
+
// Re-check before any activity publication or provider call: a prompt held
|
|
1513
|
+
// in the compaction queue may have waited while the policy entry changed.
|
|
1514
|
+
try {
|
|
1515
|
+
this.assertPromptModelPolicyAllowed(session);
|
|
1516
|
+
}
|
|
1517
|
+
catch (error) {
|
|
1518
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1519
|
+
this.publishActivity(session, "error", "error", message);
|
|
1520
|
+
this.events.publish(session.sessionId, {
|
|
1521
|
+
type: "session.error",
|
|
1522
|
+
message,
|
|
1523
|
+
});
|
|
1524
|
+
return Promise.resolve();
|
|
1525
|
+
}
|
|
1526
|
+
this.publishActivity(session, behavior === "steer"
|
|
1527
|
+
? "steering queued"
|
|
1528
|
+
: behavior === "followUp"
|
|
1529
|
+
? "message queued"
|
|
1530
|
+
: "prompt accepted", "active");
|
|
1112
1531
|
if (behavior === undefined && echoUserMessage)
|
|
1113
|
-
this.events.publish(session.sessionId, {
|
|
1532
|
+
this.events.publish(session.sessionId, {
|
|
1533
|
+
type: "message.append",
|
|
1534
|
+
message: userMessage(text, images),
|
|
1535
|
+
});
|
|
1114
1536
|
const promptOptions = buildPromptOptions(behavior, images);
|
|
1115
1537
|
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions)).catch((error) => {
|
|
1116
1538
|
const message = error instanceof Error ? error.message : String(error);
|
|
1117
1539
|
this.publishActivity(session, "error", "error", message);
|
|
1118
|
-
this.events.publish(session.sessionId, {
|
|
1540
|
+
this.events.publish(session.sessionId, {
|
|
1541
|
+
type: "session.error",
|
|
1542
|
+
message,
|
|
1543
|
+
});
|
|
1119
1544
|
});
|
|
1120
1545
|
void promptPromise;
|
|
1121
1546
|
return promptPromise;
|
|
1122
1547
|
}
|
|
1123
1548
|
enqueuePromptDuringCompaction(session, text, kind, images = [], echoUserMessage = true) {
|
|
1124
1549
|
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
1125
|
-
queue.push({
|
|
1550
|
+
queue.push({
|
|
1551
|
+
kind,
|
|
1552
|
+
text,
|
|
1553
|
+
...(images.length > 0 ? { images } : {}),
|
|
1554
|
+
...(echoUserMessage ? {} : { echoUserMessage: false }),
|
|
1555
|
+
});
|
|
1126
1556
|
this.compactionPromptQueues.set(session.sessionId, queue);
|
|
1127
1557
|
this.publishActivity(session, "message queued during compaction", "active");
|
|
1128
1558
|
this.publishStatus(session);
|
|
1129
1559
|
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Hold prompt input for the duration of a policy transition, reusing the
|
|
1562
|
+
* existing queue so the post-mutation drain submits it once the confirmed
|
|
1563
|
+
* model/thinking pair is persisted. A prompt taken back out of the queue is
|
|
1564
|
+
* re-retained at the front so relative order is preserved.
|
|
1565
|
+
*/
|
|
1566
|
+
retainPromptDuringModelPolicyMutation(session, text, kind, images = [], echoUserMessage = true, position = "back") {
|
|
1567
|
+
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
1568
|
+
const queued = {
|
|
1569
|
+
kind,
|
|
1570
|
+
text,
|
|
1571
|
+
...(images.length > 0 ? { images } : {}),
|
|
1572
|
+
...(echoUserMessage ? {} : { echoUserMessage: false }),
|
|
1573
|
+
};
|
|
1574
|
+
if (position === "front")
|
|
1575
|
+
queue.unshift(queued);
|
|
1576
|
+
else
|
|
1577
|
+
queue.push(queued);
|
|
1578
|
+
this.compactionPromptQueues.set(session.sessionId, queue);
|
|
1579
|
+
this.publishActivity(session, "message queued during model policy change", "active");
|
|
1580
|
+
this.publishStatus(session);
|
|
1581
|
+
}
|
|
1130
1582
|
async saveAttachments(ref, attachments, folder) {
|
|
1131
|
-
const parsed = parsePromptAttachments(attachments, {
|
|
1583
|
+
const parsed = parsePromptAttachments(attachments, {
|
|
1584
|
+
enforceInlineSizeLimit: false,
|
|
1585
|
+
allowFileAttachments: true,
|
|
1586
|
+
});
|
|
1132
1587
|
if (parsed.length === 0)
|
|
1133
1588
|
return [];
|
|
1134
1589
|
await this.assertWritable(ref);
|
|
@@ -1147,26 +1602,46 @@ export class PiSessionService {
|
|
|
1147
1602
|
if (session.isBashRunning)
|
|
1148
1603
|
throw new Error("A bash command is already running");
|
|
1149
1604
|
this.publishActivity(session, "running bash", "active", command);
|
|
1150
|
-
this.events.publish(session.sessionId, {
|
|
1605
|
+
this.events.publish(session.sessionId, {
|
|
1606
|
+
type: "shell.start",
|
|
1607
|
+
command,
|
|
1608
|
+
excludeFromContext: isExcluded,
|
|
1609
|
+
});
|
|
1151
1610
|
void this.runSessionEntryMutation(session, "run a shell command", () => session.executeBash(command, (chunk) => {
|
|
1152
|
-
this.events.publish(session.sessionId, {
|
|
1611
|
+
this.events.publish(session.sessionId, {
|
|
1612
|
+
type: "shell.chunk",
|
|
1613
|
+
chunk,
|
|
1614
|
+
});
|
|
1153
1615
|
this.publishActivity(session, "running bash", "active", command);
|
|
1154
1616
|
this.publishStatus(session);
|
|
1155
|
-
}, { excludeFromContext: isExcluded }))
|
|
1617
|
+
}, { excludeFromContext: isExcluded }))
|
|
1618
|
+
.then((result) => {
|
|
1156
1619
|
this.events.publish(session.sessionId, {
|
|
1157
1620
|
type: "shell.end",
|
|
1158
1621
|
output: result.output,
|
|
1159
|
-
...(result.exitCode === undefined
|
|
1622
|
+
...(result.exitCode === undefined
|
|
1623
|
+
? {}
|
|
1624
|
+
: { exitCode: result.exitCode }),
|
|
1160
1625
|
cancelled: result.cancelled,
|
|
1161
1626
|
truncated: result.truncated,
|
|
1162
|
-
...(result.fullOutputPath === undefined
|
|
1627
|
+
...(result.fullOutputPath === undefined
|
|
1628
|
+
? {}
|
|
1629
|
+
: { fullOutputPath: result.fullOutputPath }),
|
|
1163
1630
|
});
|
|
1164
1631
|
this.publishActivity(session, "bash complete", result.exitCode === 0 ? "idle" : "error", command);
|
|
1165
1632
|
this.publishStatus(session);
|
|
1166
|
-
})
|
|
1633
|
+
})
|
|
1634
|
+
.catch((error) => {
|
|
1167
1635
|
const message = error instanceof Error ? error.message : String(error);
|
|
1168
|
-
this.events.publish(session.sessionId, {
|
|
1169
|
-
|
|
1636
|
+
this.events.publish(session.sessionId, {
|
|
1637
|
+
type: "shell.end",
|
|
1638
|
+
output: message,
|
|
1639
|
+
isError: true,
|
|
1640
|
+
});
|
|
1641
|
+
this.events.publish(session.sessionId, {
|
|
1642
|
+
type: "session.error",
|
|
1643
|
+
message,
|
|
1644
|
+
});
|
|
1170
1645
|
this.publishActivity(session, "bash failed", "error", message);
|
|
1171
1646
|
this.publishStatus(session);
|
|
1172
1647
|
});
|
|
@@ -1198,7 +1673,13 @@ export class PiSessionService {
|
|
|
1198
1673
|
await this.assertWritable(ref);
|
|
1199
1674
|
const active = await this.getActive(ref);
|
|
1200
1675
|
const sourceSession = active.runtime.session;
|
|
1201
|
-
const result = await this.runTreeExclusiveOperation([
|
|
1676
|
+
const result = await this.runTreeExclusiveOperation([
|
|
1677
|
+
{
|
|
1678
|
+
sessionId: sourceSession.sessionId,
|
|
1679
|
+
session: sourceSession,
|
|
1680
|
+
runtime: active.runtime,
|
|
1681
|
+
},
|
|
1682
|
+
], "Stop current session activity before replacing the session", () => active.runtime.fork(entryId));
|
|
1202
1683
|
if (result.cancelled)
|
|
1203
1684
|
return { cancelled: true };
|
|
1204
1685
|
const session = clientSessionFromRuntime(active.runtime);
|
|
@@ -1221,7 +1702,8 @@ export class PiSessionService {
|
|
|
1221
1702
|
// may enter this runtime until Pi's potentially asynchronous summary settles.
|
|
1222
1703
|
this.treeNavigations.add(session);
|
|
1223
1704
|
try {
|
|
1224
|
-
if (expectedLeafId !== undefined &&
|
|
1705
|
+
if (expectedLeafId !== undefined &&
|
|
1706
|
+
session.sessionManager.getLeafId() !== expectedLeafId) {
|
|
1225
1707
|
throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
|
|
1226
1708
|
}
|
|
1227
1709
|
this.publishActivity(session, options.summarize ? "summarizing branch" : "navigating session tree", "active");
|
|
@@ -1229,19 +1711,32 @@ export class PiSessionService {
|
|
|
1229
1711
|
const result = await session.navigateTree(targetId, options);
|
|
1230
1712
|
if (result.cancelled) {
|
|
1231
1713
|
if (this.isCurrentActiveSession(session)) {
|
|
1232
|
-
this.publishActivity(session, result.aborted === true
|
|
1714
|
+
this.publishActivity(session, result.aborted === true
|
|
1715
|
+
? "branch summary aborted"
|
|
1716
|
+
: "tree navigation cancelled", "idle");
|
|
1233
1717
|
}
|
|
1234
|
-
return {
|
|
1718
|
+
return {
|
|
1719
|
+
cancelled: true,
|
|
1720
|
+
...(result.aborted === undefined ? {} : { aborted: result.aborted }),
|
|
1721
|
+
};
|
|
1235
1722
|
}
|
|
1236
1723
|
if (this.isCurrentActiveSession(session))
|
|
1237
1724
|
this.publishActivity(session, "session tree navigated", "idle");
|
|
1238
|
-
return {
|
|
1725
|
+
return {
|
|
1726
|
+
cancelled: false,
|
|
1727
|
+
...(result.editorText === undefined
|
|
1728
|
+
? {}
|
|
1729
|
+
: { editorText: result.editorText }),
|
|
1730
|
+
};
|
|
1239
1731
|
}
|
|
1240
1732
|
catch (error) {
|
|
1241
1733
|
const message = error instanceof Error ? error.message : String(error);
|
|
1242
1734
|
if (this.isCurrentActiveSession(session)) {
|
|
1243
1735
|
this.publishActivity(session, "tree navigation failed", "error", message);
|
|
1244
|
-
this.events.publish(session.sessionId, {
|
|
1736
|
+
this.events.publish(session.sessionId, {
|
|
1737
|
+
type: "session.error",
|
|
1738
|
+
message,
|
|
1739
|
+
});
|
|
1245
1740
|
}
|
|
1246
1741
|
throw error;
|
|
1247
1742
|
}
|
|
@@ -1265,13 +1760,16 @@ export class PiSessionService {
|
|
|
1265
1760
|
const priorGeneration = this.notificationGenerationBySession.get(session);
|
|
1266
1761
|
let candidateGeneration;
|
|
1267
1762
|
try {
|
|
1268
|
-
await session.reload(priorGeneration === undefined
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1763
|
+
await session.reload(priorGeneration === undefined
|
|
1764
|
+
? undefined
|
|
1765
|
+
: {
|
|
1766
|
+
beforeSessionStart: () => {
|
|
1767
|
+
candidateGeneration =
|
|
1768
|
+
this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
1769
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1770
|
+
this.replaceSessionNotificationContext(session, candidateGeneration);
|
|
1771
|
+
},
|
|
1772
|
+
});
|
|
1275
1773
|
if (candidateGeneration !== undefined) {
|
|
1276
1774
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1277
1775
|
}
|
|
@@ -1285,141 +1783,191 @@ export class PiSessionService {
|
|
|
1285
1783
|
}
|
|
1286
1784
|
const message = error instanceof Error ? error.message : String(error);
|
|
1287
1785
|
this.publishActivity(session, "reload failed", "error", message);
|
|
1288
|
-
this.events.publish(session.sessionId, {
|
|
1786
|
+
this.events.publish(session.sessionId, {
|
|
1787
|
+
type: "session.error",
|
|
1788
|
+
message,
|
|
1789
|
+
});
|
|
1289
1790
|
this.publishStatus(session);
|
|
1290
1791
|
throw error;
|
|
1291
1792
|
}
|
|
1292
1793
|
});
|
|
1293
1794
|
}
|
|
1294
1795
|
async archive(ref) {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1796
|
+
await this.runSessionOrderMutation(async () => {
|
|
1797
|
+
const session = await this.getOrOpen(ref);
|
|
1798
|
+
if (this.hasActiveWork(session))
|
|
1799
|
+
throw new Error("Stop current session activity before archiving");
|
|
1800
|
+
await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before archiving", async () => {
|
|
1801
|
+
const archiveInput = await this.archiveInputForSession(session);
|
|
1802
|
+
await this.closeActive(session.sessionId, {
|
|
1803
|
+
kind: "clear",
|
|
1804
|
+
reason: "archive",
|
|
1805
|
+
});
|
|
1806
|
+
await this.archiveStore.archive(archiveInput);
|
|
1807
|
+
await this.forgetUnreadSessions([archiveInput]);
|
|
1808
|
+
});
|
|
1303
1809
|
});
|
|
1304
1810
|
}
|
|
1305
1811
|
async archiveMany(refs) {
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
const active = this.activeForLookup(bulkRefToLookup(ref));
|
|
1324
|
-
const listed = findListedSessionForBulkRef(sessionContext, ref);
|
|
1325
|
-
const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
|
|
1326
|
-
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1327
|
-
failures.push({ sessionId: resolvedSessionId, error: "Stop current session activity before archiving" });
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
|
-
try {
|
|
1331
|
-
if (listed !== undefined) {
|
|
1332
|
-
planItems.push({ input: archiveInputFromListEntry(listed) });
|
|
1812
|
+
return await this.runSessionOrderMutation(async () => {
|
|
1813
|
+
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
1814
|
+
const [archivedRecords, sessionContext] = await Promise.all([
|
|
1815
|
+
this.archiveStore.list(),
|
|
1816
|
+
this.bulkSessionLookupContext(uniqueRefs),
|
|
1817
|
+
]);
|
|
1818
|
+
const failures = [];
|
|
1819
|
+
const alreadyArchivedSessionIds = [];
|
|
1820
|
+
const unreadArchivedIdentities = [];
|
|
1821
|
+
const planItems = [];
|
|
1822
|
+
for (const ref of uniqueRefs) {
|
|
1823
|
+
const archived = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
1824
|
+
if (archived !== undefined) {
|
|
1825
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(archived.sessionId, "archive"));
|
|
1826
|
+
alreadyArchivedSessionIds.push(archived.sessionId);
|
|
1827
|
+
unreadArchivedIdentities.push(archived);
|
|
1828
|
+
continue;
|
|
1333
1829
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1830
|
+
const active = this.activeForLookup(bulkRefToLookup(ref));
|
|
1831
|
+
const listed = findListedSessionForBulkRef(sessionContext, ref);
|
|
1832
|
+
const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
|
|
1833
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1834
|
+
failures.push({
|
|
1835
|
+
sessionId: resolvedSessionId,
|
|
1836
|
+
error: "Stop current session activity before archiving",
|
|
1837
|
+
});
|
|
1838
|
+
continue;
|
|
1336
1839
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1840
|
+
try {
|
|
1841
|
+
if (listed !== undefined) {
|
|
1842
|
+
planItems.push({ input: archiveInputFromListEntry(listed) });
|
|
1843
|
+
}
|
|
1844
|
+
else if (active !== undefined) {
|
|
1845
|
+
planItems.push({
|
|
1846
|
+
input: archiveInputFromActiveSession(active.runtime.session),
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
else {
|
|
1850
|
+
failures.push({ sessionId: ref.id, error: "Session not found" });
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
catch (error) {
|
|
1854
|
+
failures.push({
|
|
1855
|
+
sessionId: resolvedSessionId,
|
|
1856
|
+
error: errorMessage(error),
|
|
1857
|
+
});
|
|
1339
1858
|
}
|
|
1340
1859
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1860
|
+
const readyPlanItems = [];
|
|
1861
|
+
for (const item of planItems) {
|
|
1862
|
+
const active = this.activeForLookup({
|
|
1863
|
+
id: item.input.sessionId,
|
|
1864
|
+
cwd: item.input.cwd,
|
|
1865
|
+
});
|
|
1866
|
+
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1867
|
+
failures.push({
|
|
1868
|
+
sessionId: item.input.sessionId,
|
|
1869
|
+
error: "Stop current session activity before archiving",
|
|
1870
|
+
});
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
readyPlanItems.push(active === undefined ? item : { ...item, active });
|
|
1351
1874
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1875
|
+
const readyInputs = [];
|
|
1876
|
+
const archivedSessionIds = [...alreadyArchivedSessionIds];
|
|
1877
|
+
await this.runTreeExclusiveOperation(readyPlanItems.map(({ input, active }) => ({
|
|
1878
|
+
sessionId: input.sessionId,
|
|
1879
|
+
...(active === undefined
|
|
1880
|
+
? {}
|
|
1881
|
+
: { session: active.runtime.session, runtime: active.runtime }),
|
|
1882
|
+
})), "Stop current session activity before archiving", async () => {
|
|
1883
|
+
for (const item of readyPlanItems) {
|
|
1884
|
+
try {
|
|
1885
|
+
await this.closeActive(item.input.sessionId, {
|
|
1886
|
+
kind: "clear",
|
|
1887
|
+
reason: "archive",
|
|
1888
|
+
});
|
|
1889
|
+
readyInputs.push(item.input);
|
|
1890
|
+
}
|
|
1891
|
+
catch (error) {
|
|
1892
|
+
failures.push({
|
|
1893
|
+
sessionId: item.input.sessionId,
|
|
1894
|
+
error: errorMessage(error),
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1361
1898
|
try {
|
|
1362
|
-
await this.
|
|
1363
|
-
|
|
1899
|
+
const archived = await this.archiveStoreArchiveMany(readyInputs);
|
|
1900
|
+
archivedSessionIds.push(...archived.map((record) => record.sessionId));
|
|
1901
|
+
unreadArchivedIdentities.push(...archived);
|
|
1364
1902
|
}
|
|
1365
1903
|
catch (error) {
|
|
1366
|
-
|
|
1904
|
+
for (const input of readyInputs)
|
|
1905
|
+
failures.push({
|
|
1906
|
+
sessionId: input.sessionId,
|
|
1907
|
+
error: errorMessage(error),
|
|
1908
|
+
});
|
|
1367
1909
|
}
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
|
|
1377
|
-
}
|
|
1910
|
+
});
|
|
1911
|
+
await this.forgetUnreadSessions(unreadArchivedIdentities);
|
|
1912
|
+
return {
|
|
1913
|
+
archived: true,
|
|
1914
|
+
archivedSessionIds: uniqueStrings(archivedSessionIds),
|
|
1915
|
+
failures,
|
|
1916
|
+
generatedAt: new Date().toISOString(),
|
|
1917
|
+
};
|
|
1378
1918
|
});
|
|
1379
|
-
await this.forgetUnreadSessions(unreadArchivedIdentities);
|
|
1380
|
-
return {
|
|
1381
|
-
archived: true,
|
|
1382
|
-
archivedSessionIds: uniqueStrings(archivedSessionIds),
|
|
1383
|
-
failures,
|
|
1384
|
-
generatedAt: new Date().toISOString(),
|
|
1385
|
-
};
|
|
1386
1919
|
}
|
|
1387
1920
|
async archiveTree(ref) {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1921
|
+
return await this.runSessionOrderMutation(async () => {
|
|
1922
|
+
const session = await this.getOrOpen(ref);
|
|
1923
|
+
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
1924
|
+
const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ??
|
|
1925
|
+
archiveCandidateFromActiveSession(session, false);
|
|
1926
|
+
const plan = planSessionArchiveTree(root, catalog);
|
|
1927
|
+
const busy = plan.targets
|
|
1928
|
+
.map((target) => target.activeSession)
|
|
1929
|
+
.find((target) => target !== undefined && this.hasActiveWork(target));
|
|
1930
|
+
if (busy !== undefined)
|
|
1931
|
+
throw new Error(`Stop current session activity before archiving ${sessionDisplayName(busy)}`);
|
|
1932
|
+
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
1933
|
+
await this.runTreeExclusiveOperation(plan.unarchivedTargets.map((target) => ({
|
|
1934
|
+
sessionId: target.id,
|
|
1935
|
+
...(target.activeSession === undefined
|
|
1936
|
+
? {}
|
|
1937
|
+
: { session: target.activeSession }),
|
|
1938
|
+
})), `Stop current session activity before archiving ${sessionDisplayName(session)}`, async () => {
|
|
1939
|
+
for (const target of plan.targets) {
|
|
1940
|
+
if (target.archived)
|
|
1941
|
+
this.publishNotificationMutations(this.notificationStore.clearSession(target.id, "archive"));
|
|
1942
|
+
}
|
|
1943
|
+
for (const input of archiveInputs)
|
|
1944
|
+
await this.closeActive(input.sessionId, {
|
|
1945
|
+
kind: "clear",
|
|
1946
|
+
reason: "archive",
|
|
1947
|
+
});
|
|
1948
|
+
await this.archiveStoreArchiveMany(archiveInputs);
|
|
1949
|
+
});
|
|
1950
|
+
await this.forgetUnreadSessions(plan.targets.map((target) => ({ sessionId: target.id, cwd: target.cwd })));
|
|
1951
|
+
return {
|
|
1952
|
+
archived: true,
|
|
1953
|
+
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
1954
|
+
archivedCount: archiveInputs.length,
|
|
1955
|
+
skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
|
|
1956
|
+
};
|
|
1407
1957
|
});
|
|
1408
|
-
await this.forgetUnreadSessions(plan.targets.map((target) => ({ sessionId: target.id, cwd: target.cwd })));
|
|
1409
|
-
return {
|
|
1410
|
-
archived: true,
|
|
1411
|
-
sessionIds: archiveInputs.map((input) => input.sessionId),
|
|
1412
|
-
archivedCount: archiveInputs.length,
|
|
1413
|
-
skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
|
|
1414
|
-
};
|
|
1415
1958
|
}
|
|
1416
1959
|
async restore(ref) {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1960
|
+
await this.runSessionOrderMutation(async () => {
|
|
1961
|
+
const archived = await this.getArchived(ref);
|
|
1962
|
+
if (archived === undefined)
|
|
1963
|
+
throw new Error("Session not found");
|
|
1964
|
+
await this.closeActive(archived.sessionId, {
|
|
1965
|
+
kind: "clear",
|
|
1966
|
+
reason: "restore",
|
|
1967
|
+
});
|
|
1968
|
+
await this.archiveStore.restore(archived.sessionId);
|
|
1969
|
+
await this.forgetUnreadSessions([archived]);
|
|
1970
|
+
});
|
|
1423
1971
|
}
|
|
1424
1972
|
async deleteArchived(ref) {
|
|
1425
1973
|
const record = await this.getArchived(ref);
|
|
@@ -1427,14 +1975,18 @@ export class PiSessionService {
|
|
|
1427
1975
|
throw new Error("Archived session not found");
|
|
1428
1976
|
if (this.archiveStore.deleteArchived === undefined)
|
|
1429
1977
|
throw new Error("Archive store does not support deletion");
|
|
1430
|
-
await this.closeActive(record.sessionId, {
|
|
1978
|
+
await this.closeActive(record.sessionId, {
|
|
1979
|
+
kind: "clear",
|
|
1980
|
+
reason: "delete",
|
|
1981
|
+
});
|
|
1431
1982
|
if (record.archivePath === undefined)
|
|
1432
1983
|
await this.ensureArchivedRecordMoved(record);
|
|
1433
1984
|
await this.archiveStore.deleteArchived(record.sessionId);
|
|
1434
1985
|
await this.forgetUnreadSessions([record]);
|
|
1435
1986
|
}
|
|
1436
1987
|
async deleteArchivedMany(refs) {
|
|
1437
|
-
if (this.archiveStore.deleteArchived === undefined &&
|
|
1988
|
+
if (this.archiveStore.deleteArchived === undefined &&
|
|
1989
|
+
this.archiveStore.deleteArchivedMany === undefined)
|
|
1438
1990
|
throw new Error("Archive store does not support deletion");
|
|
1439
1991
|
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
1440
1992
|
const archivedRecords = await this.archiveStore.list();
|
|
@@ -1443,12 +1995,21 @@ export class PiSessionService {
|
|
|
1443
1995
|
for (const ref of uniqueRefs) {
|
|
1444
1996
|
const record = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
1445
1997
|
if (record === undefined) {
|
|
1446
|
-
failures.push({
|
|
1998
|
+
failures.push({
|
|
1999
|
+
sessionId: ref.id,
|
|
2000
|
+
error: "Archived session not found",
|
|
2001
|
+
});
|
|
1447
2002
|
continue;
|
|
1448
2003
|
}
|
|
1449
|
-
const active = this.activeForLookup({
|
|
2004
|
+
const active = this.activeForLookup({
|
|
2005
|
+
id: record.sessionId,
|
|
2006
|
+
cwd: record.cwd,
|
|
2007
|
+
});
|
|
1450
2008
|
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1451
|
-
failures.push({
|
|
2009
|
+
failures.push({
|
|
2010
|
+
sessionId: record.sessionId,
|
|
2011
|
+
error: "Stop current session activity before deleting archived session",
|
|
2012
|
+
});
|
|
1452
2013
|
continue;
|
|
1453
2014
|
}
|
|
1454
2015
|
planItems.push({ record });
|
|
@@ -1456,11 +2017,17 @@ export class PiSessionService {
|
|
|
1456
2017
|
const readyRecords = [];
|
|
1457
2018
|
for (const item of planItems) {
|
|
1458
2019
|
try {
|
|
1459
|
-
await this.closeActive(item.record.sessionId, {
|
|
2020
|
+
await this.closeActive(item.record.sessionId, {
|
|
2021
|
+
kind: "clear",
|
|
2022
|
+
reason: "delete",
|
|
2023
|
+
});
|
|
1460
2024
|
readyRecords.push(item.record);
|
|
1461
2025
|
}
|
|
1462
2026
|
catch (error) {
|
|
1463
|
-
failures.push({
|
|
2027
|
+
failures.push({
|
|
2028
|
+
sessionId: item.record.sessionId,
|
|
2029
|
+
error: errorMessage(error),
|
|
2030
|
+
});
|
|
1464
2031
|
}
|
|
1465
2032
|
}
|
|
1466
2033
|
const moveFailures = await this.moveLegacyArchivedRecordsForDelete(readyRecords);
|
|
@@ -1496,11 +2063,19 @@ export class PiSessionService {
|
|
|
1496
2063
|
const { sessionId, cwd } = notificationIdentityForSession(session);
|
|
1497
2064
|
let candidateGeneration;
|
|
1498
2065
|
try {
|
|
1499
|
-
await this.closeActive(sessionId, priorGeneration === undefined
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
2066
|
+
await this.closeActive(sessionId, priorGeneration === undefined
|
|
2067
|
+
? CLEAR_RUNTIME_NOTIFICATIONS
|
|
2068
|
+
: DEFER_RUNTIME_NOTIFICATIONS);
|
|
2069
|
+
candidateGeneration =
|
|
2070
|
+
priorGeneration === undefined
|
|
2071
|
+
? undefined
|
|
2072
|
+
: this.notificationStore.beginReplacement(priorGeneration, {
|
|
2073
|
+
sessionId,
|
|
2074
|
+
cwd,
|
|
2075
|
+
});
|
|
2076
|
+
const reopened = await this.getActive(ref, candidateGeneration === undefined
|
|
2077
|
+
? {}
|
|
2078
|
+
: { notificationGeneration: candidateGeneration });
|
|
1504
2079
|
if (candidateGeneration !== undefined) {
|
|
1505
2080
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1506
2081
|
}
|
|
@@ -1516,30 +2091,39 @@ export class PiSessionService {
|
|
|
1516
2091
|
this.publishStatus(reopenedSession);
|
|
1517
2092
|
}
|
|
1518
2093
|
async detachParent(ref) {
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
2094
|
+
await this.runSessionOrderMutation(async () => {
|
|
2095
|
+
const session = await this.getOrOpen(ref);
|
|
2096
|
+
const sessionFile = session.sessionFile;
|
|
2097
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
2098
|
+
throw new Error("Session is not persisted");
|
|
2099
|
+
await this.clearParentSessionFile(sessionFile);
|
|
2100
|
+
clearParentSessionHeader(session.sessionManager);
|
|
2101
|
+
await this.metadataStore.clearOrder(sessionFile);
|
|
2102
|
+
this.unregisterSubsession(session.sessionId);
|
|
2103
|
+
await this.forgetUnreadSessions([
|
|
2104
|
+
{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() },
|
|
2105
|
+
]);
|
|
2106
|
+
});
|
|
1527
2107
|
}
|
|
1528
2108
|
async pin(ref) {
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
2109
|
+
return await this.runSessionOrderMutation(async () => {
|
|
2110
|
+
const session = await this.getOrOpen(ref);
|
|
2111
|
+
const sessionFile = session.sessionFile;
|
|
2112
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
2113
|
+
throw new Error("Session is not persisted");
|
|
2114
|
+
await this.metadataStore.pin(sessionFile);
|
|
2115
|
+
return this.sessionInfoFromActive(session, { pinned: true });
|
|
2116
|
+
});
|
|
1535
2117
|
}
|
|
1536
2118
|
async unpin(ref) {
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
2119
|
+
return await this.runSessionOrderMutation(async () => {
|
|
2120
|
+
const session = await this.getOrOpen(ref);
|
|
2121
|
+
const sessionFile = session.sessionFile;
|
|
2122
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
2123
|
+
throw new Error("Session is not persisted");
|
|
2124
|
+
await this.metadataStore.unpin(sessionFile);
|
|
2125
|
+
return this.sessionInfoFromActive(session, { pinned: false });
|
|
2126
|
+
});
|
|
1543
2127
|
}
|
|
1544
2128
|
sessionInfoFromActive(session, overrides) {
|
|
1545
2129
|
const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
|
|
@@ -1552,7 +2136,9 @@ export class PiSessionService {
|
|
|
1552
2136
|
modified: new Date().toISOString(),
|
|
1553
2137
|
messageCount: session.messages.length,
|
|
1554
2138
|
firstMessage: "",
|
|
1555
|
-
...(session.sessionName === undefined
|
|
2139
|
+
...(session.sessionName === undefined
|
|
2140
|
+
? {}
|
|
2141
|
+
: { name: session.sessionName }),
|
|
1556
2142
|
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
1557
2143
|
...overrides,
|
|
1558
2144
|
};
|
|
@@ -1614,9 +2200,13 @@ export class PiSessionService {
|
|
|
1614
2200
|
}
|
|
1615
2201
|
const [sessionsByCwd, allSessions] = await Promise.all([
|
|
1616
2202
|
this.listSessionsByCwd([...cwdSet]),
|
|
1617
|
-
needsAllSessions
|
|
2203
|
+
needsAllSessions
|
|
2204
|
+
? this.sessionManager.listAll?.() ?? Promise.resolve([])
|
|
2205
|
+
: Promise.resolve(undefined),
|
|
1618
2206
|
]);
|
|
1619
|
-
return allSessions === undefined
|
|
2207
|
+
return allSessions === undefined
|
|
2208
|
+
? { sessionsByCwd }
|
|
2209
|
+
: { sessionsByCwd, allSessions };
|
|
1620
2210
|
}
|
|
1621
2211
|
async listSessionsByCwd(cwds) {
|
|
1622
2212
|
const uniqueCwds = uniqueStrings(cwds);
|
|
@@ -1653,7 +2243,10 @@ export class PiSessionService {
|
|
|
1653
2243
|
sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1654
2244
|
}
|
|
1655
2245
|
catch (error) {
|
|
1656
|
-
return legacyRecords.map((record) => ({
|
|
2246
|
+
return legacyRecords.map((record) => ({
|
|
2247
|
+
sessionId: record.sessionId,
|
|
2248
|
+
error: errorMessage(error),
|
|
2249
|
+
}));
|
|
1657
2250
|
}
|
|
1658
2251
|
const moveInputs = legacyRecords
|
|
1659
2252
|
.map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
|
|
@@ -1669,17 +2262,25 @@ export class PiSessionService {
|
|
|
1669
2262
|
const failedIds = new Set(moveInputs.map((input) => input.sessionId));
|
|
1670
2263
|
return legacyRecords
|
|
1671
2264
|
.filter((record) => failedIds.has(record.sessionId))
|
|
1672
|
-
.map((record) => ({
|
|
2265
|
+
.map((record) => ({
|
|
2266
|
+
sessionId: record.sessionId,
|
|
2267
|
+
error: errorMessage(error),
|
|
2268
|
+
}));
|
|
1673
2269
|
}
|
|
1674
2270
|
}
|
|
1675
2271
|
async cleanupPlan(request) {
|
|
1676
|
-
const [sessions, archivedRecords] = await Promise.all([
|
|
2272
|
+
const [sessions, archivedRecords] = await Promise.all([
|
|
2273
|
+
this.sessionManager.listAll?.() ?? [],
|
|
2274
|
+
this.archiveStore.list(),
|
|
2275
|
+
]);
|
|
1677
2276
|
return planSessionCleanup({
|
|
1678
2277
|
sessions,
|
|
1679
2278
|
archivedRecords,
|
|
1680
2279
|
activeSessions: this.cleanupActiveSessionStatuses(),
|
|
1681
2280
|
thresholds: request.thresholds,
|
|
1682
|
-
...(request.projectCwds === undefined
|
|
2281
|
+
...(request.projectCwds === undefined
|
|
2282
|
+
? {}
|
|
2283
|
+
: { projectCwds: request.projectCwds }),
|
|
1683
2284
|
now: this.now(),
|
|
1684
2285
|
});
|
|
1685
2286
|
}
|
|
@@ -1697,7 +2298,8 @@ export class PiSessionService {
|
|
|
1697
2298
|
const sessionIds = new Set(listedSessionIds);
|
|
1698
2299
|
for (const active of new Set(this.active.values())) {
|
|
1699
2300
|
const session = active.runtime.session;
|
|
1700
|
-
if (session.sessionManager.getCwd() === cwd &&
|
|
2301
|
+
if (session.sessionManager.getCwd() === cwd &&
|
|
2302
|
+
!archivedById.has(session.sessionId))
|
|
1701
2303
|
sessionIds.add(session.sessionId);
|
|
1702
2304
|
}
|
|
1703
2305
|
return [...sessionIds];
|
|
@@ -1716,7 +2318,9 @@ export class PiSessionService {
|
|
|
1716
2318
|
const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
|
|
1717
2319
|
if (session === undefined)
|
|
1718
2320
|
return record;
|
|
1719
|
-
const [moved] = await this.archiveStoreArchiveMany([
|
|
2321
|
+
const [moved] = await this.archiveStoreArchiveMany([
|
|
2322
|
+
archiveInputFromListEntry(session),
|
|
2323
|
+
]);
|
|
1720
2324
|
return moved ?? record;
|
|
1721
2325
|
}
|
|
1722
2326
|
async ensureArchivedRecordsMoved(records) {
|
|
@@ -1725,7 +2329,9 @@ export class PiSessionService {
|
|
|
1725
2329
|
return;
|
|
1726
2330
|
const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1727
2331
|
const moveInputs = legacyRecords
|
|
1728
|
-
.map((record) => sessionsByCwd
|
|
2332
|
+
.map((record) => sessionsByCwd
|
|
2333
|
+
.get(record.cwd)
|
|
2334
|
+
?.find((candidate) => candidate.id === record.sessionId))
|
|
1729
2335
|
.filter(isDefined)
|
|
1730
2336
|
.map(archiveInputFromListEntry);
|
|
1731
2337
|
await this.archiveStoreArchiveMany(moveInputs);
|
|
@@ -1741,7 +2347,10 @@ export class PiSessionService {
|
|
|
1741
2347
|
return archiveInputFromActiveSession(session);
|
|
1742
2348
|
}
|
|
1743
2349
|
async workspaceArchiveCandidates(cwd) {
|
|
1744
|
-
const [sessions, archivedRecords] = await Promise.all([
|
|
2350
|
+
const [sessions, archivedRecords] = await Promise.all([
|
|
2351
|
+
this.sessionManager.list(cwd),
|
|
2352
|
+
this.archiveStore.list(),
|
|
2353
|
+
]);
|
|
1745
2354
|
const candidates = new Map();
|
|
1746
2355
|
const archivedById = new Map();
|
|
1747
2356
|
for (const record of archivedRecords) {
|
|
@@ -1767,15 +2376,22 @@ export class PiSessionService {
|
|
|
1767
2376
|
}
|
|
1768
2377
|
for (const active of new Set(this.active.values())) {
|
|
1769
2378
|
const session = active.runtime.session;
|
|
1770
|
-
if (session.sessionManager.getCwd() !== cwd ||
|
|
2379
|
+
if (session.sessionManager.getCwd() !== cwd ||
|
|
2380
|
+
archivedById.has(session.sessionId))
|
|
1771
2381
|
continue;
|
|
1772
2382
|
const existing = candidates.get(session.sessionId);
|
|
1773
|
-
candidates.set(session.sessionId, {
|
|
2383
|
+
candidates.set(session.sessionId, {
|
|
2384
|
+
...(existing ?? archiveCandidateFromActiveSession(session, false)),
|
|
2385
|
+
activeSession: session,
|
|
2386
|
+
});
|
|
1774
2387
|
}
|
|
1775
2388
|
return [...candidates.values()];
|
|
1776
2389
|
}
|
|
1777
2390
|
async listSessionNames(cwd) {
|
|
1778
|
-
const [sessions, archivedRecords] = await Promise.all([
|
|
2391
|
+
const [sessions, archivedRecords] = await Promise.all([
|
|
2392
|
+
this.sessionManager.list(cwd),
|
|
2393
|
+
this.archiveStore.list(),
|
|
2394
|
+
]);
|
|
1779
2395
|
const names = new Set();
|
|
1780
2396
|
for (const session of sessions)
|
|
1781
2397
|
addSessionName(names, session.name);
|
|
@@ -1796,7 +2412,9 @@ export class PiSessionService {
|
|
|
1796
2412
|
await Promise.allSettled(pendingOpens);
|
|
1797
2413
|
const active = this.active.get(sessionId);
|
|
1798
2414
|
if (notificationPolicy.kind === "clear") {
|
|
1799
|
-
const generation = active === undefined
|
|
2415
|
+
const generation = active === undefined
|
|
2416
|
+
? undefined
|
|
2417
|
+
: this.notificationGenerationBySession.get(active.runtime.session);
|
|
1800
2418
|
const mutations = generation === undefined
|
|
1801
2419
|
? this.notificationStore.clearSession(sessionId, notificationPolicy.reason)
|
|
1802
2420
|
: this.notificationStore.clearGeneration(generation, notificationPolicy.reason);
|
|
@@ -1848,7 +2466,7 @@ export class PiSessionService {
|
|
|
1848
2466
|
throw branchSummaryAbortError;
|
|
1849
2467
|
}
|
|
1850
2468
|
async assertWritable(ref) {
|
|
1851
|
-
if (await this.getArchived(ref) !== undefined)
|
|
2469
|
+
if ((await this.getArchived(ref)) !== undefined)
|
|
1852
2470
|
throw new Error("Archived sessions are read-only. Restore the session to continue.");
|
|
1853
2471
|
}
|
|
1854
2472
|
async getOrOpen(ref) {
|
|
@@ -1867,7 +2485,7 @@ export class PiSessionService {
|
|
|
1867
2485
|
return archived.archivePath;
|
|
1868
2486
|
const match = isPiSessionRef(ref)
|
|
1869
2487
|
? (await this.sessionManager.list(ref.cwd)).find((session) => session.id === ref.id || session.id.startsWith(ref.id))
|
|
1870
|
-
: (await this.sessionManager.listAll?.() ?? []).find((session) => session.id === ref || session.id.startsWith(ref));
|
|
2488
|
+
: ((await this.sessionManager.listAll?.()) ?? []).find((session) => session.id === ref || session.id.startsWith(ref));
|
|
1871
2489
|
if (match === undefined || match.path === "")
|
|
1872
2490
|
throw new Error("Session not found");
|
|
1873
2491
|
return match.path;
|
|
@@ -1883,7 +2501,7 @@ export class PiSessionService {
|
|
|
1883
2501
|
}
|
|
1884
2502
|
const match = isPiSessionRef(ref)
|
|
1885
2503
|
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
1886
|
-
: (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
2504
|
+
: ((await this.sessionManager.listAll?.()) ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
1887
2505
|
if (!match)
|
|
1888
2506
|
throw new Error("Session not found");
|
|
1889
2507
|
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
@@ -1929,23 +2547,31 @@ export class PiSessionService {
|
|
|
1929
2547
|
if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
|
|
1930
2548
|
return exact;
|
|
1931
2549
|
for (const [candidateId, active] of this.active.entries()) {
|
|
1932
|
-
if (candidateId.startsWith(sessionId) &&
|
|
2550
|
+
if (candidateId.startsWith(sessionId) &&
|
|
2551
|
+
lookupMatchesActiveSession(ref, active))
|
|
1933
2552
|
return active;
|
|
1934
2553
|
}
|
|
1935
2554
|
return undefined;
|
|
1936
2555
|
}
|
|
1937
2556
|
async create(sessionManager, cwd, options = {}) {
|
|
1938
|
-
const delegationToolsEnabled = options.creationProvenance !== "tracked-subsession"
|
|
1939
|
-
|
|
2557
|
+
const delegationToolsEnabled = options.creationProvenance !== "tracked-subsession" &&
|
|
2558
|
+
(await sessionAllowsDelegationTools(sessionManager, this.sessionManager));
|
|
1940
2559
|
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
1941
2560
|
cwd,
|
|
1942
2561
|
agentDir: this.agentDir,
|
|
1943
2562
|
sessionManager,
|
|
1944
2563
|
delegationToolsEnabled,
|
|
1945
|
-
...(options.initialModel === undefined
|
|
1946
|
-
|
|
2564
|
+
...(options.initialModel === undefined
|
|
2565
|
+
? {}
|
|
2566
|
+
: { initialModel: options.initialModel }),
|
|
2567
|
+
...(options.initialThinkingLevel === undefined
|
|
2568
|
+
? {}
|
|
2569
|
+
: { initialThinkingLevel: options.initialThinkingLevel }),
|
|
1947
2570
|
});
|
|
1948
|
-
const active = {
|
|
2571
|
+
const active = {
|
|
2572
|
+
runtime,
|
|
2573
|
+
unsubscribe: noop,
|
|
2574
|
+
};
|
|
1949
2575
|
let boundSession = runtime.session;
|
|
1950
2576
|
let notificationGeneration = options.notificationGeneration;
|
|
1951
2577
|
let notificationOwnership = options.notifications === "disabled"
|
|
@@ -1969,6 +2595,7 @@ export class PiSessionService {
|
|
|
1969
2595
|
if (notificationGeneration !== undefined)
|
|
1970
2596
|
this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
|
|
1971
2597
|
try {
|
|
2598
|
+
this.inspectAndCacheSessionModelPolicy(runtime.session);
|
|
1972
2599
|
if (options.creationProvenance === "tracked-subsession") {
|
|
1973
2600
|
await this.publishUnreadMutations(this.unreadStore.excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
|
|
1974
2601
|
}
|
|
@@ -1987,6 +2614,7 @@ export class PiSessionService {
|
|
|
1987
2614
|
candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
1988
2615
|
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1989
2616
|
}
|
|
2617
|
+
this.inspectAndCacheSessionModelPolicy(session);
|
|
1990
2618
|
this.bindRuntime(active, session);
|
|
1991
2619
|
boundSession = session;
|
|
1992
2620
|
await this.bindSessionExtensions(session, candidateGeneration);
|
|
@@ -2005,7 +2633,11 @@ export class PiSessionService {
|
|
|
2005
2633
|
}
|
|
2006
2634
|
});
|
|
2007
2635
|
this.active.set(runtime.session.sessionId, active);
|
|
2008
|
-
if (
|
|
2636
|
+
if (options.initializeModelPolicy !== undefined) {
|
|
2637
|
+
await this.initializeSessionModelPolicy(runtime.session, options.initializeModelPolicy);
|
|
2638
|
+
}
|
|
2639
|
+
if (notificationOwnership === "replacement" &&
|
|
2640
|
+
notificationGeneration !== undefined) {
|
|
2009
2641
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
|
|
2010
2642
|
notificationOwnership = "external";
|
|
2011
2643
|
}
|
|
@@ -2053,7 +2685,10 @@ export class PiSessionService {
|
|
|
2053
2685
|
onError: (error) => {
|
|
2054
2686
|
const message = `${error.extensionPath}: ${error.error}`;
|
|
2055
2687
|
this.publishActivity(session, "extension error", "error", message);
|
|
2056
|
-
this.events.publish(session.sessionId, {
|
|
2688
|
+
this.events.publish(session.sessionId, {
|
|
2689
|
+
type: "session.error",
|
|
2690
|
+
message,
|
|
2691
|
+
});
|
|
2057
2692
|
},
|
|
2058
2693
|
});
|
|
2059
2694
|
}
|
|
@@ -2106,7 +2741,8 @@ export class PiSessionService {
|
|
|
2106
2741
|
const previousCwd = canonicalizeStoredCwd(previous.sessionManager.getCwd());
|
|
2107
2742
|
this.unreadStore.forgetActivity(previous.sessionId, previousCwd);
|
|
2108
2743
|
const nextCwd = canonicalizeStoredCwd(next.sessionManager.getCwd());
|
|
2109
|
-
if (previous.sessionId === next.sessionId &&
|
|
2744
|
+
if (previous.sessionId === next.sessionId &&
|
|
2745
|
+
cwdPathsEqual(previousCwd, nextCwd))
|
|
2110
2746
|
return;
|
|
2111
2747
|
await this.publishUnreadMutations(this.unreadStore.forgetSession(previous.sessionId, previousCwd));
|
|
2112
2748
|
}
|
|
@@ -2129,11 +2765,14 @@ export class PiSessionService {
|
|
|
2129
2765
|
publishUnreadMutations(mutations) {
|
|
2130
2766
|
this.enqueueUnreadMutations(mutations);
|
|
2131
2767
|
this.unreadPublicationFlushRequested = true;
|
|
2132
|
-
if (this.unreadPublication === undefined &&
|
|
2768
|
+
if (this.unreadPublication === undefined &&
|
|
2769
|
+
this.unreadPublicationRetryTimer !== undefined) {
|
|
2133
2770
|
const failure = this.unreadPublicationFailure;
|
|
2134
2771
|
return Promise.reject(failure instanceof Error
|
|
2135
2772
|
? failure
|
|
2136
|
-
: new Error("Session unread publication is awaiting retry", {
|
|
2773
|
+
: new Error("Session unread publication is awaiting retry", {
|
|
2774
|
+
cause: failure,
|
|
2775
|
+
}));
|
|
2137
2776
|
}
|
|
2138
2777
|
return this.ensureUnreadPublication();
|
|
2139
2778
|
}
|
|
@@ -2156,7 +2795,8 @@ export class PiSessionService {
|
|
|
2156
2795
|
return publication;
|
|
2157
2796
|
}
|
|
2158
2797
|
async drainUnreadPublication() {
|
|
2159
|
-
while (this.unreadPublicationFlushRequested ||
|
|
2798
|
+
while (this.unreadPublicationFlushRequested ||
|
|
2799
|
+
this.pendingUnreadMutations.length > 0) {
|
|
2160
2800
|
this.unreadPublicationFlushRequested = false;
|
|
2161
2801
|
const batch = this.pendingUnreadMutations.splice(0);
|
|
2162
2802
|
let publishedCount = 0;
|
|
@@ -2190,7 +2830,8 @@ export class PiSessionService {
|
|
|
2190
2830
|
this.pendingUnreadMutations.splice(0, excess);
|
|
2191
2831
|
}
|
|
2192
2832
|
scheduleUnreadPublicationRetry() {
|
|
2193
|
-
if (this.unreadPublicationStopped ||
|
|
2833
|
+
if (this.unreadPublicationStopped ||
|
|
2834
|
+
this.unreadPublicationRetryTimer !== undefined)
|
|
2194
2835
|
return;
|
|
2195
2836
|
const delay = this.unreadPublicationRetryDelayMs;
|
|
2196
2837
|
this.unreadPublicationRetryDelayMs = Math.min(Math.max(delay * 2, this.unreadPublicationRetryInitialMs), Math.max(MAX_UNREAD_PUBLICATION_RETRY_MS, this.unreadPublicationRetryInitialMs));
|
|
@@ -2259,8 +2900,11 @@ export class PiSessionService {
|
|
|
2259
2900
|
recordGenerationProgress(session, message) {
|
|
2260
2901
|
const current = this.generationMetrics.get(session);
|
|
2261
2902
|
const providerTokens = providerOutputTokens(message);
|
|
2262
|
-
const estimatedTokens = providerTokens === undefined && current?.estimated !== false
|
|
2263
|
-
|
|
2903
|
+
const estimatedTokens = providerTokens === undefined && current?.estimated !== false
|
|
2904
|
+
? estimatedOutputTokens(message)
|
|
2905
|
+
: undefined;
|
|
2906
|
+
const outputTokens = providerTokens ??
|
|
2907
|
+
(current?.estimated === false ? current.outputTokens : estimatedTokens);
|
|
2264
2908
|
if (outputTokens === undefined)
|
|
2265
2909
|
return;
|
|
2266
2910
|
this.generationMetrics.set(session, {
|
|
@@ -2274,15 +2918,20 @@ export class PiSessionService {
|
|
|
2274
2918
|
if (metrics === undefined)
|
|
2275
2919
|
return undefined;
|
|
2276
2920
|
const elapsedMs = this.now().getTime() - metrics.startedAt;
|
|
2277
|
-
const tokensPerSecond = elapsedMs >= MIN_GENERATION_RATE_ELAPSED_MS
|
|
2921
|
+
const tokensPerSecond = elapsedMs >= MIN_GENERATION_RATE_ELAPSED_MS
|
|
2922
|
+
? (metrics.outputTokens * 1_000) / elapsedMs
|
|
2923
|
+
: undefined;
|
|
2278
2924
|
return {
|
|
2279
2925
|
outputTokens: metrics.outputTokens,
|
|
2280
|
-
...(tokensPerSecond === undefined || !Number.isFinite(tokensPerSecond)
|
|
2926
|
+
...(tokensPerSecond === undefined || !Number.isFinite(tokensPerSecond)
|
|
2927
|
+
? {}
|
|
2928
|
+
: { tokensPerSecond }),
|
|
2281
2929
|
...(metrics.estimated ? { estimated: true } : {}),
|
|
2282
2930
|
};
|
|
2283
2931
|
}
|
|
2284
2932
|
scheduleCompactionQueueDrain(sessionId, delayMs = 0) {
|
|
2285
|
-
if (!this.compactionPromptQueues.has(sessionId) ||
|
|
2933
|
+
if (!this.compactionPromptQueues.has(sessionId) ||
|
|
2934
|
+
this.compactionDrainTimers.has(sessionId))
|
|
2286
2935
|
return;
|
|
2287
2936
|
const timer = setTimeout(() => {
|
|
2288
2937
|
this.compactionDrainTimers.delete(sessionId);
|
|
@@ -2295,7 +2944,7 @@ export class PiSessionService {
|
|
|
2295
2944
|
if (active === undefined)
|
|
2296
2945
|
return;
|
|
2297
2946
|
const { session } = active.runtime;
|
|
2298
|
-
if (session.isCompacting) {
|
|
2947
|
+
if (session.isCompacting || this.isModelPolicyMutationActive(session)) {
|
|
2299
2948
|
this.scheduleCompactionQueueDrain(sessionId, 100);
|
|
2300
2949
|
return;
|
|
2301
2950
|
}
|
|
@@ -2313,7 +2962,9 @@ export class PiSessionService {
|
|
|
2313
2962
|
return;
|
|
2314
2963
|
this.publishStatus(session);
|
|
2315
2964
|
const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images, prompt.echoUserMessage ?? true);
|
|
2316
|
-
void submitted.finally(() => {
|
|
2965
|
+
void submitted.finally(() => {
|
|
2966
|
+
this.scheduleCompactionQueueDrain(sessionId);
|
|
2967
|
+
});
|
|
2317
2968
|
}
|
|
2318
2969
|
takeCompactionPromptQueue(sessionId) {
|
|
2319
2970
|
const queued = this.compactionPromptQueues.get(sessionId) ?? [];
|
|
@@ -2341,7 +2992,10 @@ export class PiSessionService {
|
|
|
2341
2992
|
this.compactionDrainTimers.clear();
|
|
2342
2993
|
}
|
|
2343
2994
|
maybeGenerateSessionName(session, firstMessage) {
|
|
2344
|
-
if (session.sessionName !== undefined ||
|
|
2995
|
+
if (session.sessionName !== undefined ||
|
|
2996
|
+
session.messages.length !== 0 ||
|
|
2997
|
+
session.isStreaming ||
|
|
2998
|
+
session.isCompacting)
|
|
2345
2999
|
return;
|
|
2346
3000
|
const deterministicName = deterministicSessionName(firstMessage);
|
|
2347
3001
|
if (deterministicName !== undefined) {
|
|
@@ -2351,10 +3005,21 @@ export class PiSessionService {
|
|
|
2351
3005
|
const model = session.model;
|
|
2352
3006
|
if (model === undefined)
|
|
2353
3007
|
return;
|
|
2354
|
-
void generateShortSessionName(session.agent.streamFunction,
|
|
3008
|
+
void runWithUtilityModelFallback(this.utilityModelResolver, "lightweight", model, (candidate) => generateShortSessionName(session.agent.streamFunction, candidate, firstMessage), (candidate, error) => {
|
|
3009
|
+
this.logger.info({
|
|
3010
|
+
provider: candidate.provider,
|
|
3011
|
+
modelId: candidate.id,
|
|
3012
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3013
|
+
}, "utility title model failed");
|
|
3014
|
+
})
|
|
3015
|
+
.then((name) => {
|
|
2355
3016
|
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
2356
|
-
})
|
|
2357
|
-
|
|
3017
|
+
})
|
|
3018
|
+
.catch((error) => {
|
|
3019
|
+
this.logger.info({
|
|
3020
|
+
sessionId: session.sessionId,
|
|
3021
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3022
|
+
}, "failed to apply generated session name");
|
|
2358
3023
|
});
|
|
2359
3024
|
}
|
|
2360
3025
|
applyGeneratedSessionName(session, name) {
|
|
@@ -2375,7 +3040,10 @@ export class PiSessionService {
|
|
|
2375
3040
|
this.applyGeneratedSessionName(session, generatedName);
|
|
2376
3041
|
}
|
|
2377
3042
|
catch (error) {
|
|
2378
|
-
this.logger.info({
|
|
3043
|
+
this.logger.info({
|
|
3044
|
+
sessionId: session.sessionId,
|
|
3045
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3046
|
+
}, "failed to apply deferred session name");
|
|
2379
3047
|
}
|
|
2380
3048
|
}
|
|
2381
3049
|
const notifications = this.deferredSubsessionNotifications.get(session) ?? [];
|
|
@@ -2410,7 +3078,9 @@ export class PiSessionService {
|
|
|
2410
3078
|
this.authLossWarnings.delete(warningKey);
|
|
2411
3079
|
return;
|
|
2412
3080
|
}
|
|
2413
|
-
if (removedProviderId === undefined ||
|
|
3081
|
+
if (removedProviderId === undefined ||
|
|
3082
|
+
model.provider !== removedProviderId ||
|
|
3083
|
+
this.authLossWarnings.has(warningKey))
|
|
2414
3084
|
return;
|
|
2415
3085
|
this.authLossWarnings.add(warningKey);
|
|
2416
3086
|
this.events.publish(session.sessionId, {
|
|
@@ -2429,7 +3099,11 @@ export class PiSessionService {
|
|
|
2429
3099
|
publishSessionName(session) {
|
|
2430
3100
|
const event = session.sessionName === undefined
|
|
2431
3101
|
? { type: "session.name", sessionId: session.sessionId }
|
|
2432
|
-
: {
|
|
3102
|
+
: {
|
|
3103
|
+
type: "session.name",
|
|
3104
|
+
sessionId: session.sessionId,
|
|
3105
|
+
name: session.sessionName,
|
|
3106
|
+
};
|
|
2433
3107
|
this.events.publish(session.sessionId, event);
|
|
2434
3108
|
this.events.publishGlobal(event);
|
|
2435
3109
|
}
|
|
@@ -2469,17 +3143,20 @@ export class PiSessionService {
|
|
|
2469
3143
|
return "active";
|
|
2470
3144
|
}
|
|
2471
3145
|
hasActiveWork(session) {
|
|
2472
|
-
return this.treeNavigations.has(session)
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
3146
|
+
return (this.treeNavigations.has(session) ||
|
|
3147
|
+
this.isSessionEntryMutationActive(session) ||
|
|
3148
|
+
this.isTreeExclusiveOperationActive(session) ||
|
|
3149
|
+
sessionHasActiveWork(session, this.compactionQueuedMessages(session.sessionId).length));
|
|
2476
3150
|
}
|
|
2477
3151
|
async runTreeExclusiveOperation(targets, activeError, operation) {
|
|
2478
3152
|
const sessionIds = new Set();
|
|
2479
3153
|
const runtimes = new Set();
|
|
2480
3154
|
const sessions = new Set();
|
|
2481
3155
|
for (const target of targets) {
|
|
2482
|
-
const runtime = target.runtime ??
|
|
3156
|
+
const runtime = target.runtime ??
|
|
3157
|
+
(target.session === undefined
|
|
3158
|
+
? undefined
|
|
3159
|
+
: this.activeRuntimeForSession(target.session));
|
|
2483
3160
|
const session = target.session ?? runtime?.session;
|
|
2484
3161
|
if (session !== undefined && this.hasActiveWork(session))
|
|
2485
3162
|
throw new Error(activeError);
|
|
@@ -2518,7 +3195,8 @@ export class PiSessionService {
|
|
|
2518
3195
|
if (this.isTreeExclusiveSessionIdentityActive(session.sessionId))
|
|
2519
3196
|
return true;
|
|
2520
3197
|
const runtime = this.activeRuntimeForSession(session);
|
|
2521
|
-
return runtime !== undefined &&
|
|
3198
|
+
return (runtime !== undefined &&
|
|
3199
|
+
(this.treeExclusiveRuntimeOperationCounts.get(runtime) ?? 0) > 0);
|
|
2522
3200
|
}
|
|
2523
3201
|
activeRuntimeForSession(session) {
|
|
2524
3202
|
for (const active of new Set(this.active.values())) {
|
|
@@ -2610,11 +3288,18 @@ export class PiSessionService {
|
|
|
2610
3288
|
}
|
|
2611
3289
|
publishActivity(session, label, phase, detail) {
|
|
2612
3290
|
const at = new Date().toISOString();
|
|
2613
|
-
const stored = detail === undefined
|
|
3291
|
+
const stored = detail === undefined
|
|
3292
|
+
? { phase, label, at }
|
|
3293
|
+
: { phase, label, detail, at };
|
|
2614
3294
|
this.activities.set(session.sessionId, stored);
|
|
2615
|
-
const activity = detail === undefined
|
|
3295
|
+
const activity = detail === undefined
|
|
3296
|
+
? { sessionId: session.sessionId, phase, label, at }
|
|
3297
|
+
: { sessionId: session.sessionId, phase, label, detail, at };
|
|
2616
3298
|
this.workspaceActivity?.applySessionActivity(session.sessionManager.getCwd(), activity);
|
|
2617
|
-
this.events.publish(session.sessionId, {
|
|
3299
|
+
this.events.publish(session.sessionId, {
|
|
3300
|
+
type: "activity.update",
|
|
3301
|
+
activity,
|
|
3302
|
+
});
|
|
2618
3303
|
this.events.publishGlobal({ type: "activity.update", activity });
|
|
2619
3304
|
this.observeUnreadActivityState(session);
|
|
2620
3305
|
}
|
|
@@ -2634,12 +3319,393 @@ export class PiSessionService {
|
|
|
2634
3319
|
const stored = { phase: "idle", label: "idle", at };
|
|
2635
3320
|
this.activities.set(session.sessionId, stored);
|
|
2636
3321
|
const activity = { sessionId: session.sessionId, ...stored };
|
|
2637
|
-
this.events.publish(session.sessionId, {
|
|
3322
|
+
this.events.publish(session.sessionId, {
|
|
3323
|
+
type: "activity.update",
|
|
3324
|
+
activity,
|
|
3325
|
+
});
|
|
2638
3326
|
this.events.publishGlobal({ type: "activity.update", activity });
|
|
2639
3327
|
}
|
|
3328
|
+
/**
|
|
3329
|
+
* Read-only capability view for `get_model_policy`, built from the session's
|
|
3330
|
+
* own cached inspection so it cannot disagree with what the HTTP route reports.
|
|
3331
|
+
*/
|
|
3332
|
+
modelPolicyCapability(sessionId) {
|
|
3333
|
+
// The tool only ever asks about its own session, which is live by definition
|
|
3334
|
+
// while a tool call is executing. A miss means the runtime was torn down
|
|
3335
|
+
// mid-call, which is a terminal read failure rather than a policy answer.
|
|
3336
|
+
const session = this.active.get(sessionId)?.runtime.session;
|
|
3337
|
+
if (session === undefined)
|
|
3338
|
+
throw new Error(`Session ${sessionId} is not active`);
|
|
3339
|
+
const inspection = this.inspectAndCacheSessionModelPolicy(session);
|
|
3340
|
+
return buildModelPolicyCapability({
|
|
3341
|
+
policy: inspection.kind === "invalid" ? undefined : inspection.policy,
|
|
3342
|
+
currentRuntime: this.exactSelectionFromSession(session),
|
|
3343
|
+
ladder: this.modelTierRegistry.validate(),
|
|
3344
|
+
resolveTier: (tier) => {
|
|
3345
|
+
const resolved = this.modelTierRegistry.resolve(tier);
|
|
3346
|
+
return {
|
|
3347
|
+
model: { provider: resolved.model.provider, id: resolved.model.id },
|
|
3348
|
+
thinkingLevel: resolved.thinkingLevel,
|
|
3349
|
+
};
|
|
3350
|
+
},
|
|
3351
|
+
blockedReason: this.modelPolicyBlockedReason(session),
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
exactSelectionFromSession(session) {
|
|
3355
|
+
const model = session.model;
|
|
3356
|
+
if (model === undefined ||
|
|
3357
|
+
typeof model.provider !== "string" ||
|
|
3358
|
+
model.provider.trim() === "" ||
|
|
3359
|
+
typeof model.id !== "string" ||
|
|
3360
|
+
model.id.trim() === "") {
|
|
3361
|
+
throw new Error("Session model policy requires a resolved runtime model provider and id");
|
|
3362
|
+
}
|
|
3363
|
+
if (typeof session.thinkingLevel !== "string" ||
|
|
3364
|
+
session.thinkingLevel.trim() === "") {
|
|
3365
|
+
throw new Error("Session model policy requires a resolved runtime thinking level");
|
|
3366
|
+
}
|
|
3367
|
+
return {
|
|
3368
|
+
model: { provider: model.provider, id: model.id },
|
|
3369
|
+
thinkingLevel: session.thinkingLevel,
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
policyEntries(session) {
|
|
3373
|
+
return (session.sessionManager.getEntries?.() ??
|
|
3374
|
+
session.sessionManager.getBranch());
|
|
3375
|
+
}
|
|
3376
|
+
inspectAndCacheSessionModelPolicy(session) {
|
|
3377
|
+
const inspection = inspectSessionModelPolicy(this.policyEntries(session), this.exactSelectionFromSession(session));
|
|
3378
|
+
const ladderValidation = this.modelTierRegistry.validate();
|
|
3379
|
+
this.modelPolicyInspections.set(session, inspection);
|
|
3380
|
+
this.modelPolicyLadderValidations.set(session, ladderValidation);
|
|
3381
|
+
if (inspection.kind === "invalid")
|
|
3382
|
+
this.modelPolicyEntryReasons.set(session, inspection.reason);
|
|
3383
|
+
else
|
|
3384
|
+
this.modelPolicyEntryReasons.delete(session);
|
|
3385
|
+
return inspection;
|
|
3386
|
+
}
|
|
3387
|
+
/** Effective block: an unproven runtime restoration outranks an entry defect. */
|
|
3388
|
+
modelPolicyBlockedReason(session) {
|
|
3389
|
+
return (this.modelPolicyRuntimeBlocks.get(session.sessionId) ??
|
|
3390
|
+
this.modelPolicyEntryReasons.get(session));
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Root-session policy initialization. `true` records the runtime's own tuple;
|
|
3394
|
+
* an update applies the requested transition. Both complete before the caller
|
|
3395
|
+
* of `start()` sees the session, so no prompt can precede the policy.
|
|
3396
|
+
*/
|
|
3397
|
+
async initializeSessionModelPolicy(session, initializer) {
|
|
3398
|
+
const current = {
|
|
3399
|
+
mode: "exact",
|
|
3400
|
+
exact: this.exactSelectionFromSession(session),
|
|
3401
|
+
};
|
|
3402
|
+
if (initializer === true) {
|
|
3403
|
+
this.appendSessionModelPolicy(session, current);
|
|
3404
|
+
this.inspectAndCacheSessionModelPolicy(session);
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3407
|
+
// Root creation reports failure through its own abort/dispose cleanup rather
|
|
3408
|
+
// than publishing a failed transition for a session no client has seen.
|
|
3409
|
+
await this.applySessionModelPolicy(session, current, initializer, {
|
|
3410
|
+
action: "initialize the session model policy",
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* The single policy transition path. Validates the complete target before any
|
|
3415
|
+
* setter, re-checks that no conflicting work started during that async
|
|
3416
|
+
* validation, applies model then thinking under the serialized entry-mutation
|
|
3417
|
+
* seam, verifies the effective pair, and only then persists and caches.
|
|
3418
|
+
*/
|
|
3419
|
+
async applySessionModelPolicy(session, current, update, options) {
|
|
3420
|
+
const plan = planSessionModelPolicyUpdate(current, update, (tier) => {
|
|
3421
|
+
const resolved = this.modelTierRegistry.resolve(tier);
|
|
3422
|
+
return {
|
|
3423
|
+
model: { provider: resolved.model.provider, id: resolved.model.id },
|
|
3424
|
+
thinkingLevel: resolved.thinkingLevel,
|
|
3425
|
+
};
|
|
3426
|
+
});
|
|
3427
|
+
// Validation happens before the mutation opens, so a rejected target leaves
|
|
3428
|
+
// the runtime and the persisted policy untouched.
|
|
3429
|
+
const target = await this.resolveAvailableExactSelection(session, plan.target);
|
|
3430
|
+
// Refresh/validation awaited above, so a prompt or another mutation may have
|
|
3431
|
+
// started meanwhile. Re-check here — synchronously adjacent to the mutation
|
|
3432
|
+
// open, and *outside* it so the mutation is never its own conflict — to close
|
|
3433
|
+
// the await-to-setter race without touching tree-navigation behavior.
|
|
3434
|
+
this.assertModelPolicyMutationIdle(session, options.action);
|
|
3435
|
+
return this.runSessionModelPolicyMutation(session, "change session model policy", async () => {
|
|
3436
|
+
const previous = this.exactSelectionFromSession(session);
|
|
3437
|
+
try {
|
|
3438
|
+
await this.applyExactSelection(session, target);
|
|
3439
|
+
this.appendSessionModelPolicy(session, plan.policy);
|
|
3440
|
+
}
|
|
3441
|
+
catch (error) {
|
|
3442
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3443
|
+
if (!(await this.restoreExactSelection(session, previous))) {
|
|
3444
|
+
this.modelPolicyRuntimeBlocks.set(session.sessionId, `MODEL_POLICY_BLOCKED: ${reason}; the previous model and thinking pair could not be restored`);
|
|
3445
|
+
}
|
|
3446
|
+
this.inspectAndCacheSessionModelPolicy(session);
|
|
3447
|
+
options.onTransitionFailure?.(reason);
|
|
3448
|
+
throw error;
|
|
3449
|
+
}
|
|
3450
|
+
this.modelPolicyRuntimeBlocks.delete(session.sessionId);
|
|
3451
|
+
this.inspectAndCacheSessionModelPolicy(session);
|
|
3452
|
+
return plan.policy;
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
/**
|
|
3456
|
+
* Serialized policy-transition seam. Adds the policy-mutation marker on top of
|
|
3457
|
+
* the shared entry-mutation lifecycle so prompt submission can tell a transient
|
|
3458
|
+
* policy window apart from ordinary session work, and drains any prompt input
|
|
3459
|
+
* retained during that window once the tuple is settled again.
|
|
3460
|
+
*/
|
|
3461
|
+
async runSessionModelPolicyMutation(session, action, operation) {
|
|
3462
|
+
// Captured before the first setter runs, so this is the confirmed tuple the
|
|
3463
|
+
// window reports. Only the outermost mutation records it: a nested mutation
|
|
3464
|
+
// opens on an already-transient tuple, which would not be confirmed.
|
|
3465
|
+
if (!this.isModelPolicyMutationActive(session))
|
|
3466
|
+
this.recordConfirmedExactSelection(session);
|
|
3467
|
+
this.modelPolicyMutationCounts.set(session.sessionId, (this.modelPolicyMutationCounts.get(session.sessionId) ?? 0) + 1);
|
|
3468
|
+
try {
|
|
3469
|
+
return await this.runSessionEntryMutation(session, action, operation);
|
|
3470
|
+
}
|
|
3471
|
+
finally {
|
|
3472
|
+
decrementMapCount(this.modelPolicyMutationCounts, session.sessionId);
|
|
3473
|
+
if (!this.isModelPolicyMutationActive(session)) {
|
|
3474
|
+
// The tuple is settled again (applied, restored, or blocked), so live
|
|
3475
|
+
// runtime state is authoritative once more.
|
|
3476
|
+
this.modelPolicyConfirmedSelections.delete(session.sessionId);
|
|
3477
|
+
this.scheduleCompactionQueueDrain(session.sessionId);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
/**
|
|
3482
|
+
* Remember the pre-transition tuple for the status projection. A session whose
|
|
3483
|
+
* runtime has no resolved model has no confirmed tuple to report; that case is
|
|
3484
|
+
* left to the existing `exactSelectionFromSession` failure rather than
|
|
3485
|
+
* substituting anything.
|
|
3486
|
+
*/
|
|
3487
|
+
recordConfirmedExactSelection(session) {
|
|
3488
|
+
try {
|
|
3489
|
+
this.modelPolicyConfirmedSelections.set(session.sessionId, this.exactSelectionFromSession(session));
|
|
3490
|
+
}
|
|
3491
|
+
catch {
|
|
3492
|
+
this.modelPolicyConfirmedSelections.delete(session.sessionId);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
isModelPolicyMutationActive(session) {
|
|
3496
|
+
return (this.modelPolicyMutationCounts.get(session.sessionId) ?? 0) > 0;
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Tell other clients a policy transition failed. Called after the failure state
|
|
3500
|
+
* (including any `MODEL_POLICY_BLOCKED`) is recorded and before the route error
|
|
3501
|
+
* propagates, so the published status already carries the blocked reason.
|
|
3502
|
+
*/
|
|
3503
|
+
publishModelPolicyTransitionFailure(session, reason) {
|
|
3504
|
+
this.publishActivity(session, "model policy change failed", "error", reason);
|
|
3505
|
+
this.publishStatus(session);
|
|
3506
|
+
}
|
|
3507
|
+
/**
|
|
3508
|
+
* Reject a policy mutation that could race Pi's own entry writes. Tree
|
|
3509
|
+
* navigation is reported first because it is the more specific state.
|
|
3510
|
+
*/
|
|
3511
|
+
assertModelPolicyMutationIdle(session, action) {
|
|
3512
|
+
this.assertTreeNavigationInactive(session, action);
|
|
3513
|
+
if (this.hasActiveWork(session))
|
|
3514
|
+
throw new Error(`Stop current session activity before you ${action}`);
|
|
3515
|
+
}
|
|
3516
|
+
/**
|
|
3517
|
+
* Gate for the direct Exact model/thinking routes. They act only in Exact
|
|
3518
|
+
* mode: while Tiered is active the ladder owns the runtime tuple, and while
|
|
3519
|
+
* the runtime is blocked no tuple change may be recorded as authoritative.
|
|
3520
|
+
*/
|
|
3521
|
+
assertExactModelPolicyMutationAllowed(session, action) {
|
|
3522
|
+
this.assertModelPolicyMutationIdle(session, action);
|
|
3523
|
+
const inspection = this.inspectAndCacheSessionModelPolicy(session);
|
|
3524
|
+
const runtimeBlock = this.modelPolicyRuntimeBlocks.get(session.sessionId);
|
|
3525
|
+
if (runtimeBlock !== undefined)
|
|
3526
|
+
throw new Error(`Cannot ${action}: ${runtimeBlock}`);
|
|
3527
|
+
if (inspection.kind === "invalid") {
|
|
3528
|
+
throw new Error(`Cannot ${action}: the session model policy entry is invalid (${inspection.reason}). Repair the policy first.`);
|
|
3529
|
+
}
|
|
3530
|
+
if (inspection.policy.mode === "tiered") {
|
|
3531
|
+
throw new Error(`Cannot ${action} directly while the session model policy is Tiered. Change the model policy instead.`);
|
|
3532
|
+
}
|
|
3533
|
+
return inspection.policy;
|
|
3534
|
+
}
|
|
3535
|
+
/**
|
|
3536
|
+
* Run one Exact route operation and record Pi's *confirmed* resulting pair as
|
|
3537
|
+
* the new authoritative policy inside the same serialized mutation. Pi's own
|
|
3538
|
+
* model-selection clamp is therefore persisted as-is rather than replaced by
|
|
3539
|
+
* an invented target thinking level.
|
|
3540
|
+
*/
|
|
3541
|
+
async runExactModelPolicyMutation(session, action, currentPolicy, operation) {
|
|
3542
|
+
let transitionFailure;
|
|
3543
|
+
try {
|
|
3544
|
+
return await this.runSessionModelPolicyMutation(session, action, async () => {
|
|
3545
|
+
const previous = this.exactSelectionFromSession(session);
|
|
3546
|
+
const result = await operation();
|
|
3547
|
+
const policy = {
|
|
3548
|
+
mode: "exact",
|
|
3549
|
+
exact: this.exactSelectionFromSession(session),
|
|
3550
|
+
...(currentPolicy.tier === undefined
|
|
3551
|
+
? {}
|
|
3552
|
+
: { tier: currentPolicy.tier }),
|
|
3553
|
+
};
|
|
3554
|
+
try {
|
|
3555
|
+
this.appendSessionModelPolicy(session, policy);
|
|
3556
|
+
}
|
|
3557
|
+
catch (error) {
|
|
3558
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3559
|
+
if (!(await this.restoreExactSelection(session, previous))) {
|
|
3560
|
+
this.modelPolicyRuntimeBlocks.set(session.sessionId, `MODEL_POLICY_BLOCKED: ${reason}; the previous model and thinking pair could not be restored`);
|
|
3561
|
+
}
|
|
3562
|
+
this.inspectAndCacheSessionModelPolicy(session);
|
|
3563
|
+
// Only a failure that already moved the runtime tuple is announced; an
|
|
3564
|
+
// operation precondition ("Only one model available") keeps its plain
|
|
3565
|
+
// route error and publishes nothing.
|
|
3566
|
+
transitionFailure = reason;
|
|
3567
|
+
throw error;
|
|
3568
|
+
}
|
|
3569
|
+
this.inspectAndCacheSessionModelPolicy(session);
|
|
3570
|
+
return result;
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3573
|
+
catch (error) {
|
|
3574
|
+
// Published after the mutation closed so the status reports the settled
|
|
3575
|
+
// (blocked or restored) state rather than "updating session".
|
|
3576
|
+
if (transitionFailure !== undefined)
|
|
3577
|
+
this.publishModelPolicyTransitionFailure(session, transitionFailure);
|
|
3578
|
+
throw error;
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* Refuse to reach a provider while the authoritative policy is unusable. This
|
|
3583
|
+
* covers a malformed newest entry and an unproven restoration; it deliberately
|
|
3584
|
+
* does no tier remapping.
|
|
3585
|
+
*/
|
|
3586
|
+
assertPromptModelPolicyAllowed(session) {
|
|
3587
|
+
const inspection = this.inspectAndCacheSessionModelPolicy(session);
|
|
3588
|
+
const runtimeBlock = this.modelPolicyRuntimeBlocks.get(session.sessionId);
|
|
3589
|
+
if (runtimeBlock !== undefined)
|
|
3590
|
+
throw new Error(`Cannot send a prompt: ${runtimeBlock}`);
|
|
3591
|
+
if (inspection.kind === "invalid") {
|
|
3592
|
+
throw new Error(`Cannot send a prompt: the session model policy entry is invalid (${inspection.reason}). Repair the policy first.`);
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* Bind an exact selection to a currently available runtime model and prove the
|
|
3597
|
+
* target thinking level is supported by that *incoming* model. Pi's
|
|
3598
|
+
* `setThinkingLevel` clamps silently, so this check must precede every setter.
|
|
3599
|
+
*/
|
|
3600
|
+
async resolveAvailableExactSelection(session, selection) {
|
|
3601
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
3602
|
+
const candidates = session.scopedModels.length > 0
|
|
3603
|
+
? session.scopedModels.map((scoped) => scoped.model)
|
|
3604
|
+
: session.modelRuntime.getAvailableSnapshot();
|
|
3605
|
+
const model = candidates.find((candidate) => candidate.provider === selection.model.provider &&
|
|
3606
|
+
candidate.id === selection.model.id);
|
|
3607
|
+
const described = `${selection.model.provider}/${selection.model.id}`;
|
|
3608
|
+
if (model === undefined)
|
|
3609
|
+
throw new Error(`Model not found: ${described}`);
|
|
3610
|
+
if (!isKnownThinkingLevel(selection.thinkingLevel)) {
|
|
3611
|
+
throw new Error(`Unknown thinking level ${selection.thinkingLevel} for ${described}`);
|
|
3612
|
+
}
|
|
3613
|
+
if (!runtimeThinkingLevels(model).includes(selection.thinkingLevel)) {
|
|
3614
|
+
throw new Error(`Thinking level ${selection.thinkingLevel} is unsupported by ${described}`);
|
|
3615
|
+
}
|
|
3616
|
+
return {
|
|
3617
|
+
model,
|
|
3618
|
+
selection: {
|
|
3619
|
+
model: { provider: model.provider, id: model.id },
|
|
3620
|
+
thinkingLevel: selection.thinkingLevel,
|
|
3621
|
+
},
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
/**
|
|
3625
|
+
* Model first, then thinking: `setThinkingLevel` clamps against the currently
|
|
3626
|
+
* selected model, so the reverse order can discard a level the incoming model
|
|
3627
|
+
* supports. The effective pair is verified afterwards; a clamped substitute is
|
|
3628
|
+
* a failed transition, never a success.
|
|
3629
|
+
*/
|
|
3630
|
+
async applyExactSelection(session, target) {
|
|
3631
|
+
const level = target.selection.thinkingLevel;
|
|
3632
|
+
if (!isKnownThinkingLevel(level))
|
|
3633
|
+
throw new Error(`Unknown thinking level ${level}`);
|
|
3634
|
+
await session.setModel(target.model);
|
|
3635
|
+
session.setThinkingLevel(level);
|
|
3636
|
+
const effective = this.exactSelectionFromSession(session);
|
|
3637
|
+
if (effective.model.provider !== target.selection.model.provider ||
|
|
3638
|
+
effective.model.id !== target.selection.model.id ||
|
|
3639
|
+
effective.thinkingLevel !== level) {
|
|
3640
|
+
throw new Error(`Session model policy could not activate ${target.selection.model.provider}/${target.selection.model.id} at thinking level ${level}` +
|
|
3641
|
+
`; the runtime reports ${effective.model.provider}/${effective.model.id} at thinking level ${effective.thinkingLevel}`);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Put the previous exact pair back in the same model-then-thinking order.
|
|
3646
|
+
* Returns whether the previous pair is *proven* active again; a false result
|
|
3647
|
+
* means the runtime tuple is ambiguous and the session must refuse prompts.
|
|
3648
|
+
*/
|
|
3649
|
+
async restoreExactSelection(session, previous) {
|
|
3650
|
+
try {
|
|
3651
|
+
const current = this.exactSelectionFromSession(session);
|
|
3652
|
+
if (current.model.provider === previous.model.provider &&
|
|
3653
|
+
current.model.id === previous.model.id &&
|
|
3654
|
+
current.thinkingLevel === previous.thinkingLevel) {
|
|
3655
|
+
return true;
|
|
3656
|
+
}
|
|
3657
|
+
const target = await this.resolveAvailableExactSelection(session, previous);
|
|
3658
|
+
await this.applyExactSelection(session, target);
|
|
3659
|
+
return true;
|
|
3660
|
+
}
|
|
3661
|
+
catch {
|
|
3662
|
+
return false;
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
appendSessionModelPolicy(session, policy) {
|
|
3666
|
+
if (session.sessionManager.appendCustomEntry === undefined) {
|
|
3667
|
+
throw new Error("Cannot persist the session model policy: session persistence is unavailable");
|
|
3668
|
+
}
|
|
3669
|
+
session.sessionManager.appendCustomEntry(SESSION_MODEL_POLICY_CUSTOM_TYPE, serializeSessionModelPolicy(policy));
|
|
3670
|
+
}
|
|
3671
|
+
modelPolicyStatusFromSession(session) {
|
|
3672
|
+
const inspection = this.modelPolicyInspections.get(session);
|
|
3673
|
+
if (inspection === undefined)
|
|
3674
|
+
throw new Error("Session model policy inspection is unavailable");
|
|
3675
|
+
const ladderValidation = this.modelPolicyLadderValidations.get(session);
|
|
3676
|
+
if (ladderValidation === undefined)
|
|
3677
|
+
throw new Error("Session model tier validation is unavailable");
|
|
3678
|
+
// Inside a transition the live tuple is half-applied, so report the tuple
|
|
3679
|
+
// this session last confirmed. `mode` already comes from the unchanged
|
|
3680
|
+
// entry, keeping the published pair internally consistent.
|
|
3681
|
+
const confirmed = this.isModelPolicyMutationActive(session)
|
|
3682
|
+
? this.modelPolicyConfirmedSelections.get(session.sessionId)
|
|
3683
|
+
: undefined;
|
|
3684
|
+
const resolved = confirmed ?? this.exactSelectionFromSession(session);
|
|
3685
|
+
const blockedReason = this.modelPolicyBlockedReason(session);
|
|
3686
|
+
if (inspection.kind === "invalid") {
|
|
3687
|
+
return {
|
|
3688
|
+
mode: "exact",
|
|
3689
|
+
resolved,
|
|
3690
|
+
ladderValid: ladderValidation.valid,
|
|
3691
|
+
...(blockedReason === undefined ? {} : { blockedReason }),
|
|
3692
|
+
};
|
|
3693
|
+
}
|
|
3694
|
+
return {
|
|
3695
|
+
mode: inspection.policy.mode,
|
|
3696
|
+
...(inspection.policy.tier === undefined
|
|
3697
|
+
? {}
|
|
3698
|
+
: { tier: inspection.policy.tier }),
|
|
3699
|
+
resolved,
|
|
3700
|
+
ladderValid: ladderValidation.valid,
|
|
3701
|
+
...(blockedReason === undefined ? {} : { blockedReason }),
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
2640
3704
|
statusFromSession(session) {
|
|
2641
3705
|
const stats = session.getSessionStats();
|
|
2642
|
-
const model = session.model === undefined
|
|
3706
|
+
const model = session.model === undefined
|
|
3707
|
+
? undefined
|
|
3708
|
+
: modelToClientModel(session.model);
|
|
2643
3709
|
const generation = this.generationForStatus(session);
|
|
2644
3710
|
const contextUsage = session.getContextUsage();
|
|
2645
3711
|
const warnings = this.warningsForSession(session);
|
|
@@ -2648,6 +3714,7 @@ export class PiSessionService {
|
|
|
2648
3714
|
persisted: sessionFileExists(session.sessionFile),
|
|
2649
3715
|
...(model === undefined ? {} : { model }),
|
|
2650
3716
|
thinkingLevel: session.thinkingLevel,
|
|
3717
|
+
modelPolicy: this.modelPolicyStatusFromSession(session),
|
|
2651
3718
|
isStreaming: session.isStreaming,
|
|
2652
3719
|
isCompacting: session.isCompacting,
|
|
2653
3720
|
isBashRunning: session.isBashRunning,
|
|
@@ -2676,7 +3743,8 @@ export class PiSessionService {
|
|
|
2676
3743
|
return warnings;
|
|
2677
3744
|
}
|
|
2678
3745
|
pendingMessageCount(session) {
|
|
2679
|
-
return session.pendingMessageCount +
|
|
3746
|
+
return (session.pendingMessageCount +
|
|
3747
|
+
this.compactionQueuedMessages(session.sessionId).length);
|
|
2680
3748
|
}
|
|
2681
3749
|
compactionQueuedMessages(sessionId) {
|
|
2682
3750
|
return this.compactionPromptQueues.get(sessionId) ?? [];
|
|
@@ -2691,7 +3759,9 @@ function previewResponseFromPlan(plan) {
|
|
|
2691
3759
|
thresholds: plan.thresholds,
|
|
2692
3760
|
projects: plan.projects,
|
|
2693
3761
|
totals: plan.totals,
|
|
2694
|
-
...(plan.skippedBusySessionIds.length === 0
|
|
3762
|
+
...(plan.skippedBusySessionIds.length === 0
|
|
3763
|
+
? {}
|
|
3764
|
+
: { skippedBusySessionIds: plan.skippedBusySessionIds }),
|
|
2695
3765
|
};
|
|
2696
3766
|
}
|
|
2697
3767
|
function uniqueBulkSessionRefs(refs) {
|
|
@@ -2710,19 +3780,36 @@ function bulkRefToLookup(ref) {
|
|
|
2710
3780
|
return ref.cwd === undefined ? ref.id : { id: ref.id, cwd: ref.cwd };
|
|
2711
3781
|
}
|
|
2712
3782
|
function findArchivedRecordForBulkRef(records, ref) {
|
|
2713
|
-
return records.find((record) => (ref.cwd === undefined || record.cwd === ref.cwd) &&
|
|
3783
|
+
return records.find((record) => (ref.cwd === undefined || record.cwd === ref.cwd) &&
|
|
3784
|
+
(record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
|
|
2714
3785
|
}
|
|
2715
3786
|
function findListedSessionForBulkRef(context, ref) {
|
|
2716
3787
|
if (ref.cwd !== undefined)
|
|
2717
3788
|
return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
2718
|
-
return context.allSessions === undefined
|
|
3789
|
+
return context.allSessions === undefined
|
|
3790
|
+
? undefined
|
|
3791
|
+
: findSessionByIdOrPrefix(context.allSessions, ref.id);
|
|
2719
3792
|
}
|
|
2720
3793
|
function findSessionByIdOrPrefix(sessions, sessionId) {
|
|
2721
|
-
return sessions.find((session) => session.id === sessionId) ??
|
|
3794
|
+
return (sessions.find((session) => session.id === sessionId) ??
|
|
3795
|
+
sessions.find((session) => session.id.startsWith(sessionId)));
|
|
2722
3796
|
}
|
|
2723
3797
|
function uniqueStrings(values) {
|
|
2724
3798
|
return [...new Set(values)];
|
|
2725
3799
|
}
|
|
3800
|
+
/**
|
|
3801
|
+
* Reconcile duplicate catalog entries by canonical CWD + id, matching the
|
|
3802
|
+
* last-writer-wins convention other catalog reconciliation helpers use. The
|
|
3803
|
+
* reorder loader can read the same session more than once when overlapping
|
|
3804
|
+
* catalog CWDs are requested (e.g. a cross-workspace children scope).
|
|
3805
|
+
*/
|
|
3806
|
+
function uniqueSessionsByIdentity(sessions) {
|
|
3807
|
+
const byIdentity = new Map();
|
|
3808
|
+
for (const session of sessions) {
|
|
3809
|
+
byIdentity.set(JSON.stringify([canonicalizeStoredCwd(session.cwd), session.id]), session);
|
|
3810
|
+
}
|
|
3811
|
+
return [...byIdentity.values()];
|
|
3812
|
+
}
|
|
2726
3813
|
function errorMessage(error) {
|
|
2727
3814
|
return error instanceof Error ? error.message : String(error);
|
|
2728
3815
|
}
|
|
@@ -2756,7 +3843,9 @@ function clientSessionFromListEntry(session) {
|
|
|
2756
3843
|
modified: session.modified.toISOString(),
|
|
2757
3844
|
messageCount: session.messageCount,
|
|
2758
3845
|
firstMessage: session.firstMessage,
|
|
2759
|
-
...(session.parentSessionPath === undefined
|
|
3846
|
+
...(session.parentSessionPath === undefined
|
|
3847
|
+
? {}
|
|
3848
|
+
: { parentSessionPath: session.parentSessionPath }),
|
|
2760
3849
|
};
|
|
2761
3850
|
}
|
|
2762
3851
|
function archiveInputFromListEntry(session) {
|
|
@@ -2769,7 +3858,9 @@ function archiveInputFromListEntry(session) {
|
|
|
2769
3858
|
messageCount: session.messageCount,
|
|
2770
3859
|
firstMessage: session.firstMessage,
|
|
2771
3860
|
...(session.name === undefined ? {} : { name: session.name }),
|
|
2772
|
-
...(session.parentSessionPath === undefined
|
|
3861
|
+
...(session.parentSessionPath === undefined
|
|
3862
|
+
? {}
|
|
3863
|
+
: { parentSessionPath: session.parentSessionPath }),
|
|
2773
3864
|
};
|
|
2774
3865
|
}
|
|
2775
3866
|
function archiveInputFromActiveSession(session) {
|
|
@@ -2796,7 +3887,9 @@ function archiveCandidateFromListEntry(session) {
|
|
|
2796
3887
|
cwd: session.cwd,
|
|
2797
3888
|
archived: false,
|
|
2798
3889
|
listEntry: session,
|
|
2799
|
-
...(session.parentSessionPath === undefined
|
|
3890
|
+
...(session.parentSessionPath === undefined
|
|
3891
|
+
? {}
|
|
3892
|
+
: { parentSessionPath: session.parentSessionPath }),
|
|
2800
3893
|
};
|
|
2801
3894
|
}
|
|
2802
3895
|
function archiveCandidateFromArchivedRecord(record, fallback) {
|
|
@@ -2835,7 +3928,10 @@ function archiveInputFromCandidate(candidate) {
|
|
|
2835
3928
|
throw new Error(`Session is not available for archiving: ${candidate.id}`);
|
|
2836
3929
|
}
|
|
2837
3930
|
function sessionHasActiveWork(session, extraQueuedMessageCount = 0) {
|
|
2838
|
-
return session.isStreaming ||
|
|
3931
|
+
return (session.isStreaming ||
|
|
3932
|
+
session.isCompacting ||
|
|
3933
|
+
session.isBashRunning ||
|
|
3934
|
+
session.pendingMessageCount + extraQueuedMessageCount > 0);
|
|
2839
3935
|
}
|
|
2840
3936
|
function sessionDisplayName(session) {
|
|
2841
3937
|
return session.sessionName ?? session.sessionId;
|
|
@@ -2846,7 +3942,11 @@ function clientSessionFromArchivedRecord(record, fallback) {
|
|
|
2846
3942
|
const modified = record.modified ?? fallback?.modified.toISOString();
|
|
2847
3943
|
const messageCount = record.messageCount ?? fallback?.messageCount;
|
|
2848
3944
|
const firstMessage = record.firstMessage ?? fallback?.firstMessage;
|
|
2849
|
-
if (path === undefined ||
|
|
3945
|
+
if (path === undefined ||
|
|
3946
|
+
created === undefined ||
|
|
3947
|
+
modified === undefined ||
|
|
3948
|
+
messageCount === undefined ||
|
|
3949
|
+
firstMessage === undefined)
|
|
2850
3950
|
return undefined;
|
|
2851
3951
|
const name = record.name ?? fallback?.name;
|
|
2852
3952
|
const parentSessionPath = record.parentSessionPath ?? fallback?.parentSessionPath;
|
|
@@ -2879,10 +3979,28 @@ function archivedTimestamp(record) {
|
|
|
2879
3979
|
function isDefined(value) {
|
|
2880
3980
|
return value !== undefined;
|
|
2881
3981
|
}
|
|
2882
|
-
function mergeSessionMetadata(session,
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
3982
|
+
function mergeSessionMetadata(session, metadata) {
|
|
3983
|
+
const entry = metadata[session.path];
|
|
3984
|
+
const pinned = entry?.pinned === true;
|
|
3985
|
+
const scope = session.parentSessionPath === undefined
|
|
3986
|
+
? { kind: "root", cwd: canonicalizeStoredCwd(session.cwd) }
|
|
3987
|
+
: { kind: "children", parentSessionPath: session.parentSessionPath };
|
|
3988
|
+
const order = entry?.order;
|
|
3989
|
+
const manualOrder = session.archived !== true
|
|
3990
|
+
&& order?.pinned === pinned
|
|
3991
|
+
&& sessionReorderScopesEqual(order.scope, scope)
|
|
3992
|
+
? order.position
|
|
3993
|
+
: undefined;
|
|
3994
|
+
return {
|
|
3995
|
+
...session,
|
|
3996
|
+
...(pinned ? { pinned: true } : {}),
|
|
3997
|
+
...(manualOrder === undefined ? {} : { manualOrder }),
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
function sessionReorderScopesEqual(a, b) {
|
|
4001
|
+
if (a.kind === "root")
|
|
4002
|
+
return b.kind === "root" && a.cwd === b.cwd;
|
|
4003
|
+
return b.kind === "children" && a.parentSessionPath === b.parentSessionPath;
|
|
2886
4004
|
}
|
|
2887
4005
|
/**
|
|
2888
4006
|
* Resolve the delegation capability from server-owned, persisted session
|
|
@@ -2946,9 +4064,11 @@ function findReciprocalParentSubsessionLink(managers, parentSessionFile, parentS
|
|
|
2946
4064
|
const link = parsePersistedParentSubsessionLink(entry);
|
|
2947
4065
|
if (link === undefined)
|
|
2948
4066
|
continue;
|
|
2949
|
-
if (link.spawnedBySessionId !== parentSessionId ||
|
|
4067
|
+
if (link.spawnedBySessionId !== parentSessionId ||
|
|
4068
|
+
link.spawnedSessionId !== childSessionId)
|
|
2950
4069
|
continue;
|
|
2951
|
-
if (link.spawnedSessionFile === undefined ||
|
|
4070
|
+
if (link.spawnedSessionFile === undefined ||
|
|
4071
|
+
!sessionPathsEqual(link.spawnedSessionFile, childSessionFile))
|
|
2952
4072
|
continue;
|
|
2953
4073
|
return link;
|
|
2954
4074
|
}
|
|
@@ -2958,7 +4078,9 @@ function trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessio
|
|
|
2958
4078
|
return {
|
|
2959
4079
|
parentSessionId,
|
|
2960
4080
|
childSessionId: link.spawnedSessionId,
|
|
2961
|
-
...(link.spawnedSessionFile === undefined
|
|
4081
|
+
...(link.spawnedSessionFile === undefined
|
|
4082
|
+
? {}
|
|
4083
|
+
: { childSessionFile: link.spawnedSessionFile }),
|
|
2962
4084
|
parentSessionFile,
|
|
2963
4085
|
...(link.cwd === undefined ? {} : { cwd: link.cwd }),
|
|
2964
4086
|
};
|
|
@@ -2968,7 +4090,9 @@ function persistedParentSubsessionLinkData(link) {
|
|
|
2968
4090
|
version: 1,
|
|
2969
4091
|
spawnedBySessionId: link.parentSessionId,
|
|
2970
4092
|
spawnedSessionId: link.childSessionId,
|
|
2971
|
-
...(link.childSessionFile === undefined
|
|
4093
|
+
...(link.childSessionFile === undefined
|
|
4094
|
+
? {}
|
|
4095
|
+
: { spawnedSessionFile: link.childSessionFile }),
|
|
2972
4096
|
...(link.cwd === undefined ? {} : { cwd: link.cwd }),
|
|
2973
4097
|
};
|
|
2974
4098
|
}
|
|
@@ -2980,33 +4104,45 @@ function persistedChildSubsessionLinkData(parentSessionId, childSessionId) {
|
|
|
2980
4104
|
};
|
|
2981
4105
|
}
|
|
2982
4106
|
function parsePersistedParentSubsessionLink(entry) {
|
|
2983
|
-
if (!isRecord(entry) ||
|
|
4107
|
+
if (!isRecord(entry) ||
|
|
4108
|
+
entry["type"] !== "custom" ||
|
|
4109
|
+
entry["customType"] !== SUBSESSION_LINK_CUSTOM_TYPE)
|
|
2984
4110
|
return undefined;
|
|
2985
4111
|
const data = entry["data"];
|
|
2986
4112
|
if (!isRecord(data))
|
|
2987
4113
|
return undefined;
|
|
2988
4114
|
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
2989
4115
|
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
2990
|
-
if (spawnedBySessionId === undefined ||
|
|
4116
|
+
if (spawnedBySessionId === undefined ||
|
|
4117
|
+
spawnedBySessionId === "" ||
|
|
4118
|
+
spawnedSessionId === undefined ||
|
|
4119
|
+
spawnedSessionId === "")
|
|
2991
4120
|
return undefined;
|
|
2992
4121
|
const spawnedSessionFile = getString(data, "spawnedSessionFile");
|
|
2993
4122
|
const cwd = getString(data, "cwd");
|
|
2994
4123
|
return {
|
|
2995
4124
|
spawnedBySessionId,
|
|
2996
4125
|
spawnedSessionId,
|
|
2997
|
-
...(spawnedSessionFile === undefined || spawnedSessionFile === ""
|
|
4126
|
+
...(spawnedSessionFile === undefined || spawnedSessionFile === ""
|
|
4127
|
+
? {}
|
|
4128
|
+
: { spawnedSessionFile }),
|
|
2998
4129
|
...(cwd === undefined || cwd === "" ? {} : { cwd }),
|
|
2999
4130
|
};
|
|
3000
4131
|
}
|
|
3001
4132
|
function parsePersistedChildSubsessionLink(entry) {
|
|
3002
|
-
if (!isRecord(entry) ||
|
|
4133
|
+
if (!isRecord(entry) ||
|
|
4134
|
+
entry["type"] !== "custom" ||
|
|
4135
|
+
entry["customType"] !== SUBSESSION_CHILD_LINK_CUSTOM_TYPE)
|
|
3003
4136
|
return undefined;
|
|
3004
4137
|
const data = entry["data"];
|
|
3005
4138
|
if (!isRecord(data))
|
|
3006
4139
|
return undefined;
|
|
3007
4140
|
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
3008
4141
|
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
3009
|
-
if (spawnedBySessionId === undefined ||
|
|
4142
|
+
if (spawnedBySessionId === undefined ||
|
|
4143
|
+
spawnedBySessionId === "" ||
|
|
4144
|
+
spawnedSessionId === undefined ||
|
|
4145
|
+
spawnedSessionId === "")
|
|
3010
4146
|
return undefined;
|
|
3011
4147
|
return { spawnedBySessionId, spawnedSessionId };
|
|
3012
4148
|
}
|
|
@@ -3031,13 +4167,16 @@ function sessionFileExists(sessionFile) {
|
|
|
3031
4167
|
}
|
|
3032
4168
|
function sessionFileMatches(session, expectedSessionFile) {
|
|
3033
4169
|
const sessionFile = nonEmptyString(session.sessionFile);
|
|
3034
|
-
return sessionFile !== undefined &&
|
|
4170
|
+
return (sessionFile !== undefined &&
|
|
4171
|
+
expectedSessionFile !== undefined &&
|
|
4172
|
+
sessionPathsEqual(sessionFile, expectedSessionFile));
|
|
3035
4173
|
}
|
|
3036
4174
|
function activeSessionFileMatches(active, expectedSessionFile) {
|
|
3037
4175
|
return sessionFileMatches(active.runtime.session, expectedSessionFile);
|
|
3038
4176
|
}
|
|
3039
4177
|
function trackedLinkParentFileMatches(link, parentSessionFile) {
|
|
3040
|
-
return link.parentSessionFile !== undefined &&
|
|
4178
|
+
return (link.parentSessionFile !== undefined &&
|
|
4179
|
+
sessionPathsEqual(link.parentSessionFile, parentSessionFile));
|
|
3041
4180
|
}
|
|
3042
4181
|
async function readSessionHeaderSummary(sessionFile) {
|
|
3043
4182
|
let file;
|
|
@@ -3049,10 +4188,15 @@ async function readSessionHeaderSummary(sessionFile) {
|
|
|
3049
4188
|
if (firstLine === undefined || firstLine === "")
|
|
3050
4189
|
return undefined;
|
|
3051
4190
|
const header = JSON.parse(firstLine);
|
|
3052
|
-
if (!isRecord(header) ||
|
|
4191
|
+
if (!isRecord(header) ||
|
|
4192
|
+
header["type"] !== "session" ||
|
|
4193
|
+
typeof header["id"] !== "string")
|
|
3053
4194
|
return undefined;
|
|
3054
4195
|
const parentSession = getString(header, "parentSession");
|
|
3055
|
-
return {
|
|
4196
|
+
return {
|
|
4197
|
+
id: header["id"],
|
|
4198
|
+
...(parentSession === undefined ? {} : { parentSession }),
|
|
4199
|
+
};
|
|
3056
4200
|
}
|
|
3057
4201
|
catch {
|
|
3058
4202
|
return undefined;
|
|
@@ -3067,7 +4211,8 @@ async function sessionFileHeaderMatches(sessionFile, expected) {
|
|
|
3067
4211
|
return false;
|
|
3068
4212
|
if (expected.parentSessionFile === undefined)
|
|
3069
4213
|
return true;
|
|
3070
|
-
return header.parentSession !== undefined &&
|
|
4214
|
+
return (header.parentSession !== undefined &&
|
|
4215
|
+
sessionPathsEqual(header.parentSession, expected.parentSessionFile));
|
|
3071
4216
|
}
|
|
3072
4217
|
async function clearParentSession(sessionFile) {
|
|
3073
4218
|
const content = await readFile(sessionFile, "utf8");
|
|
@@ -3092,8 +4237,12 @@ function clearSessionQueue(session) {
|
|
|
3092
4237
|
}
|
|
3093
4238
|
function queuedMessagesFromSession(session, extraQueuedMessages = []) {
|
|
3094
4239
|
return [
|
|
3095
|
-
...session
|
|
3096
|
-
|
|
4240
|
+
...session
|
|
4241
|
+
.getSteeringMessages()
|
|
4242
|
+
.map((text) => ({ kind: "steer", text })),
|
|
4243
|
+
...session
|
|
4244
|
+
.getFollowUpMessages()
|
|
4245
|
+
.map((text) => ({ kind: "followUp", text })),
|
|
3097
4246
|
...extraQueuedMessages,
|
|
3098
4247
|
];
|
|
3099
4248
|
}
|
|
@@ -3145,7 +4294,8 @@ function historyMessages(session) {
|
|
|
3145
4294
|
messages.push({
|
|
3146
4295
|
...historyMessage,
|
|
3147
4296
|
entryId,
|
|
3148
|
-
...(previousMessage?.role === "assistant" &&
|
|
4297
|
+
...(previousMessage?.role === "assistant" &&
|
|
4298
|
+
previousMessage.entryId !== undefined
|
|
3149
4299
|
? { previousAssistantEntryId: previousMessage.entryId }
|
|
3150
4300
|
: {}),
|
|
3151
4301
|
...(messages.length > 0 ? { canFork: true } : {}),
|
|
@@ -3158,17 +4308,30 @@ function historyMessages(session) {
|
|
|
3158
4308
|
continue;
|
|
3159
4309
|
}
|
|
3160
4310
|
if (entry["type"] === "custom_message" && entry["display"] === true) {
|
|
3161
|
-
messages.push({
|
|
4311
|
+
messages.push({
|
|
4312
|
+
role: "custom",
|
|
4313
|
+
content: entry["content"],
|
|
4314
|
+
customType: entry["customType"],
|
|
4315
|
+
details: entry["details"],
|
|
4316
|
+
});
|
|
3162
4317
|
previousMessage = { role: "custom", entryId: undefined };
|
|
3163
4318
|
continue;
|
|
3164
4319
|
}
|
|
3165
4320
|
if (entry["type"] === "compaction") {
|
|
3166
|
-
messages.push({
|
|
4321
|
+
messages.push({
|
|
4322
|
+
role: "system",
|
|
4323
|
+
source: "compaction",
|
|
4324
|
+
content: `Compacted history:\n\n${stringValue(entry["summary"])}`,
|
|
4325
|
+
});
|
|
3167
4326
|
previousMessage = { role: "system", entryId: undefined };
|
|
3168
4327
|
continue;
|
|
3169
4328
|
}
|
|
3170
4329
|
if (entry["type"] === "branch_summary") {
|
|
3171
|
-
messages.push({
|
|
4330
|
+
messages.push({
|
|
4331
|
+
role: "system",
|
|
4332
|
+
source: "branch_summary",
|
|
4333
|
+
content: `Branch summary:\n\n${stringValue(entry["summary"])}`,
|
|
4334
|
+
});
|
|
3172
4335
|
previousMessage = { role: "system", entryId: undefined };
|
|
3173
4336
|
}
|
|
3174
4337
|
}
|
|
@@ -3201,7 +4364,9 @@ function finalAssistantText(messages) {
|
|
|
3201
4364
|
continue;
|
|
3202
4365
|
const texts = [];
|
|
3203
4366
|
for (const part of content) {
|
|
3204
|
-
if (isRecord(part) &&
|
|
4367
|
+
if (isRecord(part) &&
|
|
4368
|
+
part["type"] === "text" &&
|
|
4369
|
+
typeof part["text"] === "string")
|
|
3205
4370
|
texts.push(part["text"]);
|
|
3206
4371
|
}
|
|
3207
4372
|
if (texts.length > 0)
|
|
@@ -3212,23 +4377,52 @@ function finalAssistantText(messages) {
|
|
|
3212
4377
|
function toClientEvent(event) {
|
|
3213
4378
|
const eventType = getString(event, "type");
|
|
3214
4379
|
const assistantMessageEvent = getProperty(event, "assistantMessageEvent");
|
|
3215
|
-
if (eventType === "message_update" &&
|
|
3216
|
-
|
|
4380
|
+
if (eventType === "message_update" &&
|
|
4381
|
+
getString(assistantMessageEvent, "type") === "text_delta") {
|
|
4382
|
+
return {
|
|
4383
|
+
type: "assistant.delta",
|
|
4384
|
+
text: getString(assistantMessageEvent, "delta") ?? "",
|
|
4385
|
+
};
|
|
3217
4386
|
}
|
|
3218
|
-
if (eventType === "message_update" &&
|
|
3219
|
-
|
|
4387
|
+
if (eventType === "message_update" &&
|
|
4388
|
+
getString(assistantMessageEvent, "type") === "thinking_delta") {
|
|
4389
|
+
return {
|
|
4390
|
+
type: "assistant.thinking.delta",
|
|
4391
|
+
text: getString(assistantMessageEvent, "delta") ?? "",
|
|
4392
|
+
};
|
|
3220
4393
|
}
|
|
3221
4394
|
if (eventType === "tool_execution_start") {
|
|
3222
4395
|
const args = getProperty(event, "args");
|
|
3223
|
-
return {
|
|
4396
|
+
return {
|
|
4397
|
+
type: "tool.start",
|
|
4398
|
+
toolName: getString(event, "toolName") ?? "",
|
|
4399
|
+
toolCallId: getString(event, "toolCallId") ?? "",
|
|
4400
|
+
summary: summarizeToolArgs(args),
|
|
4401
|
+
args,
|
|
4402
|
+
};
|
|
3224
4403
|
}
|
|
3225
4404
|
if (eventType === "tool_execution_update") {
|
|
3226
4405
|
const partialResult = getProperty(event, "partialResult");
|
|
3227
|
-
return {
|
|
4406
|
+
return {
|
|
4407
|
+
type: "tool.update",
|
|
4408
|
+
toolName: getString(event, "toolName") ?? "",
|
|
4409
|
+
toolCallId: getString(event, "toolCallId") ?? "",
|
|
4410
|
+
text: stringifyToolResult(partialResult),
|
|
4411
|
+
content: toolResultContent(partialResult),
|
|
4412
|
+
details: toolResultDetails(partialResult),
|
|
4413
|
+
};
|
|
3228
4414
|
}
|
|
3229
4415
|
if (eventType === "tool_execution_end") {
|
|
3230
4416
|
const result = getProperty(event, "result");
|
|
3231
|
-
return {
|
|
4417
|
+
return {
|
|
4418
|
+
type: "tool.end",
|
|
4419
|
+
toolName: getString(event, "toolName") ?? "",
|
|
4420
|
+
toolCallId: getString(event, "toolCallId") ?? "",
|
|
4421
|
+
text: stringifyToolResult(result),
|
|
4422
|
+
content: toolResultContent(result),
|
|
4423
|
+
details: toolResultDetails(result),
|
|
4424
|
+
isError: getBoolean(event, "isError") === true,
|
|
4425
|
+
};
|
|
3232
4426
|
}
|
|
3233
4427
|
if (eventType === "agent_start")
|
|
3234
4428
|
return { type: "agent.start" };
|
|
@@ -3236,7 +4430,9 @@ function toClientEvent(event) {
|
|
|
3236
4430
|
return { type: "agent.end" };
|
|
3237
4431
|
if (eventType === "message_end") {
|
|
3238
4432
|
const message = getProperty(event, "message");
|
|
3239
|
-
return message === undefined
|
|
4433
|
+
return message === undefined
|
|
4434
|
+
? { type: "message.end" }
|
|
4435
|
+
: { type: "message.end", message };
|
|
3240
4436
|
}
|
|
3241
4437
|
return { type: "pi.event", eventType: eventType ?? "unknown" };
|
|
3242
4438
|
}
|
|
@@ -3249,13 +4445,18 @@ function summarizeToolArgs(args) {
|
|
|
3249
4445
|
const path = getString(args, "path");
|
|
3250
4446
|
if (path !== undefined)
|
|
3251
4447
|
return path;
|
|
3252
|
-
if (typeof args["oldText"] === "string" &&
|
|
4448
|
+
if (typeof args["oldText"] === "string" &&
|
|
4449
|
+
typeof args["newText"] === "string")
|
|
3253
4450
|
return "edit text replacement";
|
|
3254
4451
|
const edits = args["edits"];
|
|
3255
4452
|
if (Array.isArray(edits))
|
|
3256
4453
|
return `${String(edits.length)} edit${edits.length === 1 ? "" : "s"}`;
|
|
3257
|
-
const entries = Object.entries(args)
|
|
3258
|
-
|
|
4454
|
+
const entries = Object.entries(args)
|
|
4455
|
+
.filter(([, value]) => value != null)
|
|
4456
|
+
.slice(0, 3);
|
|
4457
|
+
return entries
|
|
4458
|
+
.map(([key, value]) => `${key}: ${shortToolValue(value)}`)
|
|
4459
|
+
.join(" · ");
|
|
3259
4460
|
}
|
|
3260
4461
|
function shortToolValue(value) {
|
|
3261
4462
|
if (typeof value === "string")
|
|
@@ -3288,11 +4489,16 @@ function stringifyToolResult(result) {
|
|
|
3288
4489
|
if (typeof result === "string")
|
|
3289
4490
|
return result;
|
|
3290
4491
|
if (Array.isArray(result))
|
|
3291
|
-
return result
|
|
4492
|
+
return result
|
|
4493
|
+
.map(stringifyToolResult)
|
|
4494
|
+
.filter((text) => text !== "")
|
|
4495
|
+
.join("\n");
|
|
3292
4496
|
if (isRecord(result)) {
|
|
3293
4497
|
if (getString(result, "type") === "image")
|
|
3294
4498
|
return "[image]";
|
|
3295
|
-
const text = getString(result, "text") ??
|
|
4499
|
+
const text = getString(result, "text") ??
|
|
4500
|
+
getString(result, "content") ??
|
|
4501
|
+
getString(result, "output");
|
|
3296
4502
|
if (text !== undefined)
|
|
3297
4503
|
return text;
|
|
3298
4504
|
const content = getProperty(result, "content");
|
|
@@ -3318,7 +4524,9 @@ function getBoolean(value, key) {
|
|
|
3318
4524
|
}
|
|
3319
4525
|
function providerOutputTokens(message) {
|
|
3320
4526
|
const output = getProperty(getProperty(message, "usage"), "output");
|
|
3321
|
-
return typeof output === "number" && Number.isFinite(output) && output > 0
|
|
4527
|
+
return typeof output === "number" && Number.isFinite(output) && output > 0
|
|
4528
|
+
? output
|
|
4529
|
+
: undefined;
|
|
3322
4530
|
}
|
|
3323
4531
|
/** Match Pi Web's 4-characters-per-token live approximation. */
|
|
3324
4532
|
function estimatedOutputTokens(message) {
|
|
@@ -3353,7 +4561,9 @@ function stringifyPrimitive(value) {
|
|
|
3353
4561
|
return "";
|
|
3354
4562
|
if (typeof value === "string")
|
|
3355
4563
|
return value;
|
|
3356
|
-
if (typeof value === "number" ||
|
|
4564
|
+
if (typeof value === "number" ||
|
|
4565
|
+
typeof value === "boolean" ||
|
|
4566
|
+
typeof value === "bigint")
|
|
3357
4567
|
return String(value);
|
|
3358
4568
|
return "";
|
|
3359
4569
|
}
|