@hyperdreamer/pi-webui 1.11.0-beta.6 → 1.11.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{CodeViewer-CH1qYiww.js → CodeViewer-DdYnGA-X.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-Dm-7IWHF.js → UnifiedDiffViewer-BSMTtVUW.js} +1 -1
- package/dist/client/assets/{index-CcRQiTO_.js → index-B5Kn_roi.js} +416 -414
- package/dist/client/index.html +1 -1
- package/dist/server/sessiond.js +6 -1
- 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/piSessionService.js +877 -290
- 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/starterModelPolicyPreferenceStore.js +164 -0
- package/dist/server/sessions/starterModelPolicyPreferenceStore.js.map +1 -0
- package/dist/shared/apiTypes.d.ts +23 -3
- package/dist/shared/apiTypes.js +1 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +3 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/docs/config.md +10 -4
- package/optional-skills/deterministic-subagent-driven-development/SKILL.md +15 -15
- package/optional-skills/deterministic-subagent-driven-development/pi-webui-skill.json +2 -2
- package/optional-skills/deterministic-subagent-driven-development/references/capability-contract.md +16 -16
- package/optional-skills/deterministic-subagent-driven-development/references/plan-contract.md +4 -4
- package/optional-skills/deterministic-subagent-driven-development/references/state-machine.md +3 -3
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/plan-policy.mjs +18 -9
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/prompt-renderer.mjs +3 -3
- package/optional-skills/deterministic-subagent-driven-development/scripts/lib/state-machine.mjs +6 -5
- package/optional-skills/deterministic-subagent-driven-development/scripts/sdd-state.mjs +1 -0
- package/package.json +1 -1
|
@@ -7,28 +7,34 @@ 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";
|
|
10
|
+
import { SessionCommandService, clientSessionFromRuntime, } from "./sessionCommandService.js";
|
|
11
|
+
import { projectSessionTree, } from "./sessionTreeProjection.js";
|
|
12
|
+
import { SessionArchiveStore, } from "./sessionArchiveStore.js";
|
|
13
13
|
import { SessionMetadataStore } from "./sessionMetadataStore.js";
|
|
14
|
-
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
15
|
-
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.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
21
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
22
|
-
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
23
|
-
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
22
|
+
import { createSpawnSessionToolDefinition, } from "./spawnSessionTool.js";
|
|
23
|
+
import { createSubsessionToolDefinitions, } from "./spawnSubsessionTool.js";
|
|
24
|
+
import { createModelPolicyToolDefinition, } from "./modelPolicyTool.js";
|
|
25
|
+
import { buildModelPolicyCapability, } from "./modelPolicyCapability.js";
|
|
24
26
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
25
|
-
import { planForceCleanup, planSessionCleanup, summarizeForceCleanupExecution, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
27
|
+
import { planForceCleanup, planSessionCleanup, summarizeForceCleanupExecution, summarizeSessionCleanupExecution, } from "./sessionCleanup.js";
|
|
26
28
|
import { SessionNotificationStore, } from "./sessionNotificationStore.js";
|
|
27
29
|
import { plainTextTheme } from "./plainTextTheme.js";
|
|
28
|
-
import { SessionUnreadStore } from "./sessionUnreadStore.js";
|
|
29
|
-
import { createModelTierRegistry, isModelTier, runtimeThinkingLevels } from "./modelTierRegistry.js";
|
|
30
|
+
import { SessionUnreadStore, } from "./sessionUnreadStore.js";
|
|
31
|
+
import { createModelTierRegistry, isModelTier, runtimeThinkingLevels, } from "./modelTierRegistry.js";
|
|
30
32
|
import { inspectSessionModelPolicy, planSessionModelPolicyUpdate, serializeSessionModelPolicy, SESSION_MODEL_POLICY_CUSTOM_TYPE, } from "./sessionModelPolicy.js";
|
|
31
|
-
const noopLogger = {
|
|
33
|
+
const noopLogger = {
|
|
34
|
+
info() {
|
|
35
|
+
/* no-op */
|
|
36
|
+
},
|
|
37
|
+
};
|
|
32
38
|
const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
|
|
33
39
|
const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
|
|
34
40
|
const MAX_PENDING_UNREAD_MUTATIONS = SESSION_UNREAD_LIMIT + 1;
|
|
@@ -90,13 +96,13 @@ function requirePromptText(value) {
|
|
|
90
96
|
throw new Error("Prompt text is required");
|
|
91
97
|
return value;
|
|
92
98
|
}
|
|
93
|
-
function
|
|
94
|
-
const match = /^(?:\uFEFF)
|
|
99
|
+
function leadingTierEcho(prompt) {
|
|
100
|
+
const match = /^(?:\uFEFF)?Model tier: ([^\s\r\n]+)(?:[ \t]*(?:\r?\n|$))/u.exec(prompt);
|
|
95
101
|
if (match === null)
|
|
96
102
|
return undefined;
|
|
97
103
|
const value = match[1];
|
|
98
104
|
if (value === undefined || !isModelTier(value))
|
|
99
|
-
throw new Error(`Unknown leading tier
|
|
105
|
+
throw new Error(`Unknown leading model tier label: ${value ?? ""}`);
|
|
100
106
|
return value;
|
|
101
107
|
}
|
|
102
108
|
function parsePromptStreamingBehavior(value) {
|
|
@@ -106,7 +112,10 @@ function parsePromptStreamingBehavior(value) {
|
|
|
106
112
|
return value;
|
|
107
113
|
throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
|
|
108
114
|
}
|
|
109
|
-
const CLEAR_RUNTIME_NOTIFICATIONS = {
|
|
115
|
+
const CLEAR_RUNTIME_NOTIFICATIONS = {
|
|
116
|
+
kind: "clear",
|
|
117
|
+
reason: "runtime-close",
|
|
118
|
+
};
|
|
110
119
|
const DEFER_RUNTIME_NOTIFICATIONS = { kind: "defer" };
|
|
111
120
|
function resourceDiagnosticToWarning(diagnostic, source) {
|
|
112
121
|
return {
|
|
@@ -117,7 +126,11 @@ function resourceDiagnosticToWarning(diagnostic, source) {
|
|
|
117
126
|
};
|
|
118
127
|
}
|
|
119
128
|
function runtimeDiagnosticToWarning(diagnostic) {
|
|
120
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
severity: diagnostic.type,
|
|
131
|
+
message: diagnostic.message,
|
|
132
|
+
source: "runtime",
|
|
133
|
+
};
|
|
121
134
|
}
|
|
122
135
|
/**
|
|
123
136
|
* Compute the live warnings for a runtime by re-reading its current resource
|
|
@@ -141,7 +154,12 @@ export function collectRuntimeWarnings(runtime) {
|
|
|
141
154
|
for (const diagnostic of resourceLoader.getThemes().diagnostics)
|
|
142
155
|
warnings.push(resourceDiagnosticToWarning(diagnostic, "theme"));
|
|
143
156
|
for (const error of resourceLoader.getExtensions().errors) {
|
|
144
|
-
warnings.push({
|
|
157
|
+
warnings.push({
|
|
158
|
+
severity: "error",
|
|
159
|
+
message: `${error.path}: ${error.error}`,
|
|
160
|
+
source: "extension",
|
|
161
|
+
path: error.path,
|
|
162
|
+
});
|
|
145
163
|
}
|
|
146
164
|
}
|
|
147
165
|
return warnings;
|
|
@@ -207,7 +225,10 @@ export function dismissSessionWarning(session, dismissId) {
|
|
|
207
225
|
if (dismissId !== ANTHROPIC_EXTRA_USAGE_DISMISS_ID) {
|
|
208
226
|
throw new Error(`Unknown session warning dismiss id: ${dismissId}`);
|
|
209
227
|
}
|
|
210
|
-
session.settingsManager.setWarnings({
|
|
228
|
+
session.settingsManager.setWarnings({
|
|
229
|
+
...session.settingsManager.getWarnings(),
|
|
230
|
+
anthropicExtraUsage: false,
|
|
231
|
+
});
|
|
211
232
|
}
|
|
212
233
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
213
234
|
if (!(options.sessionManager instanceof SessionManager))
|
|
@@ -235,31 +256,53 @@ function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, ini
|
|
|
235
256
|
return createRuntime({
|
|
236
257
|
...options,
|
|
237
258
|
...(model === undefined ? {} : { initialModel: model }),
|
|
238
|
-
...(thinkingLevel === undefined
|
|
239
|
-
|
|
259
|
+
...(thinkingLevel === undefined
|
|
260
|
+
? {}
|
|
261
|
+
: { initialThinkingLevel: thinkingLevel }),
|
|
262
|
+
...(toolsEnabled === undefined
|
|
263
|
+
? {}
|
|
264
|
+
: { delegationToolsEnabled: toolsEnabled }),
|
|
240
265
|
});
|
|
241
266
|
};
|
|
242
267
|
}
|
|
243
|
-
export function createPiWebUiCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions) {
|
|
268
|
+
export function createPiWebUiCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions, modelPolicy) {
|
|
244
269
|
return [
|
|
245
270
|
createPiWebUiEditToolDefinition(cwd),
|
|
246
|
-
...(delegationEnabled && spawn !== undefined
|
|
247
|
-
|
|
271
|
+
...(delegationEnabled && spawn !== undefined
|
|
272
|
+
? [createSpawnSessionToolDefinition(cwd, { spawn })]
|
|
273
|
+
: []),
|
|
274
|
+
...(delegationEnabled && subsessions !== undefined
|
|
275
|
+
? createSubsessionToolDefinitions(cwd, subsessions)
|
|
276
|
+
: []),
|
|
277
|
+
// Paired with the subsession tools: the policy view exists so a dispatching
|
|
278
|
+
// agent can verify tier dispatch before using it, so it is pointless without
|
|
279
|
+
// the dispatch capability and misleading if advertised separately.
|
|
280
|
+
...(delegationEnabled &&
|
|
281
|
+
subsessions !== undefined &&
|
|
282
|
+
modelPolicy !== undefined
|
|
283
|
+
? [createModelPolicyToolDefinition(modelPolicy)]
|
|
284
|
+
: []),
|
|
248
285
|
];
|
|
249
286
|
}
|
|
250
|
-
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions) {
|
|
251
|
-
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
252
|
-
const services = await createAgentSessionServices({
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
287
|
+
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, modelPolicy) {
|
|
288
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled, }) => {
|
|
289
|
+
const services = await createAgentSessionServices({
|
|
290
|
+
cwd,
|
|
291
|
+
agentDir,
|
|
292
|
+
modelRuntime,
|
|
293
|
+
});
|
|
294
|
+
const resolvedDelegationToolsEnabled = delegationToolsEnabled ??
|
|
295
|
+
(await sessionAllowsDelegationTools(sessionManager, sessionManagers));
|
|
296
|
+
const customTools = createPiWebUiCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions, modelPolicy);
|
|
256
297
|
const result = await createAgentSessionFromServices({
|
|
257
298
|
services,
|
|
258
299
|
sessionManager,
|
|
259
300
|
customTools,
|
|
260
301
|
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
261
302
|
...(initialModel === undefined ? {} : { model: initialModel }),
|
|
262
|
-
...(initialThinkingLevel === undefined
|
|
303
|
+
...(initialThinkingLevel === undefined
|
|
304
|
+
? {}
|
|
305
|
+
: { thinkingLevel: initialThinkingLevel }),
|
|
263
306
|
});
|
|
264
307
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
265
308
|
};
|
|
@@ -270,16 +313,29 @@ function createPiWebUiEditToolDefinition(cwd) {
|
|
|
270
313
|
name: editTool.name,
|
|
271
314
|
label: editTool.label,
|
|
272
315
|
description: editTool.description,
|
|
273
|
-
...(editTool.promptSnippet === undefined
|
|
274
|
-
|
|
316
|
+
...(editTool.promptSnippet === undefined
|
|
317
|
+
? {}
|
|
318
|
+
: { promptSnippet: editTool.promptSnippet }),
|
|
319
|
+
...(editTool.promptGuidelines === undefined
|
|
320
|
+
? {}
|
|
321
|
+
: { promptGuidelines: editTool.promptGuidelines }),
|
|
275
322
|
parameters: editTool.parameters,
|
|
276
|
-
...(editTool.renderShell === undefined
|
|
277
|
-
|
|
278
|
-
|
|
323
|
+
...(editTool.renderShell === undefined
|
|
324
|
+
? {}
|
|
325
|
+
: { renderShell: editTool.renderShell }),
|
|
326
|
+
...(editTool.prepareArguments === undefined
|
|
327
|
+
? {}
|
|
328
|
+
: { prepareArguments: editTool.prepareArguments }),
|
|
329
|
+
...(editTool.executionMode === undefined
|
|
330
|
+
? {}
|
|
331
|
+
: { executionMode: editTool.executionMode }),
|
|
279
332
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
280
333
|
const preview = await computeEditPreview(params.path, params.edits, cwd);
|
|
281
334
|
if (signal?.aborted !== true) {
|
|
282
|
-
onUpdate?.({
|
|
335
|
+
onUpdate?.({
|
|
336
|
+
content: [{ type: "text", text: "Edit preview computed." }],
|
|
337
|
+
details: { preview },
|
|
338
|
+
});
|
|
283
339
|
}
|
|
284
340
|
return editTool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
285
341
|
},
|
|
@@ -367,34 +423,56 @@ export class PiSessionService {
|
|
|
367
423
|
this.spawnTargets = deps.spawnTargets;
|
|
368
424
|
this.logger = deps.logger ?? noopLogger;
|
|
369
425
|
this.now = deps.now ?? (() => new Date());
|
|
370
|
-
this.notificationStore =
|
|
426
|
+
this.notificationStore =
|
|
427
|
+
deps.notificationStore ?? new SessionNotificationStore();
|
|
371
428
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
372
429
|
this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
|
|
373
430
|
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
374
431
|
// Subsessions are a beta capability gated behind their own flag, and they
|
|
375
432
|
// also require the spawn capability (they share its project-scope resolver).
|
|
376
433
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
377
|
-
this.createRuntime =
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
434
|
+
this.createRuntime =
|
|
435
|
+
deps.createRuntime ??
|
|
436
|
+
createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.spawnTargets === undefined
|
|
437
|
+
? undefined
|
|
438
|
+
: (input) => this.spawnSession(input), !subsessionsActive
|
|
439
|
+
? undefined
|
|
440
|
+
: {
|
|
441
|
+
spawn: (input) => this.spawnSubsession(input),
|
|
442
|
+
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
443
|
+
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
444
|
+
read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
|
|
445
|
+
}, !subsessionsActive
|
|
446
|
+
? undefined
|
|
447
|
+
: {
|
|
448
|
+
inspect: (sessionId) => this.modelPolicyCapability(sessionId),
|
|
449
|
+
});
|
|
450
|
+
this.createAgentRuntime =
|
|
451
|
+
deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
452
|
+
this.modelTierRegistry =
|
|
453
|
+
deps.modelTierRegistry ??
|
|
454
|
+
createModelTierRegistry({
|
|
455
|
+
loadConfig: () => {
|
|
456
|
+
const loaded = loadPiWebUiConfig();
|
|
457
|
+
return {
|
|
458
|
+
...(loaded.config.modelTiers === undefined
|
|
459
|
+
? {}
|
|
460
|
+
: { modelTiers: loaded.config.modelTiers }),
|
|
461
|
+
...(loaded.modelTiersError === undefined
|
|
462
|
+
? {}
|
|
463
|
+
: { modelTiersError: loaded.modelTiersError }),
|
|
464
|
+
};
|
|
465
|
+
},
|
|
466
|
+
models: () => this.modelRuntime.getAvailableSnapshot(),
|
|
467
|
+
supportedThinkingLevels: runtimeThinkingLevels,
|
|
468
|
+
});
|
|
395
469
|
this.workspaceActivity = deps.workspaceActivity;
|
|
396
|
-
this.heartbeat = setInterval(() => {
|
|
397
|
-
|
|
470
|
+
this.heartbeat = setInterval(() => {
|
|
471
|
+
this.publishHeartbeats();
|
|
472
|
+
}, deps.heartbeatIntervalMs ?? 2000);
|
|
473
|
+
this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, {
|
|
474
|
+
echoUserMessage: false,
|
|
475
|
+
}), events, {
|
|
398
476
|
onCompactionStart: (session) => {
|
|
399
477
|
this.beginSessionEntryMutation(session, "compact the session");
|
|
400
478
|
this.publishActivity(session, "compacting", "active");
|
|
@@ -407,7 +485,8 @@ export class PiSessionService {
|
|
|
407
485
|
},
|
|
408
486
|
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
409
487
|
getSessionTree: (session) => {
|
|
410
|
-
if (typeof session.sessionManager.getTree !== "function" ||
|
|
488
|
+
if (typeof session.sessionManager.getTree !== "function" ||
|
|
489
|
+
typeof session.navigateTree !== "function")
|
|
411
490
|
return undefined;
|
|
412
491
|
return projectSessionTree(session.sessionManager.getTree(), session.sessionManager.getLeafId());
|
|
413
492
|
},
|
|
@@ -452,7 +531,9 @@ export class PiSessionService {
|
|
|
452
531
|
}
|
|
453
532
|
async cleanup(request) {
|
|
454
533
|
const plan = await this.cleanupPlan(request);
|
|
455
|
-
if (plan.deleteRecords.length > 0 &&
|
|
534
|
+
if (plan.deleteRecords.length > 0 &&
|
|
535
|
+
this.archiveStore.deleteArchived === undefined &&
|
|
536
|
+
this.archiveStore.deleteArchivedMany === undefined)
|
|
456
537
|
throw new Error("Archive store does not support deletion");
|
|
457
538
|
const archiveInputs = [];
|
|
458
539
|
const readyArchiveInputs = [];
|
|
@@ -464,7 +545,10 @@ export class PiSessionService {
|
|
|
464
545
|
skippedBusySessionIds.add(input.sessionId);
|
|
465
546
|
continue;
|
|
466
547
|
}
|
|
467
|
-
await this.closeActive(input.sessionId, {
|
|
548
|
+
await this.closeActive(input.sessionId, {
|
|
549
|
+
kind: "clear",
|
|
550
|
+
reason: "archive",
|
|
551
|
+
});
|
|
468
552
|
readyArchiveInputs.push(input);
|
|
469
553
|
}
|
|
470
554
|
await this.archiveStoreArchiveMany(readyArchiveInputs);
|
|
@@ -475,7 +559,10 @@ export class PiSessionService {
|
|
|
475
559
|
skippedBusySessionIds.add(record.sessionId);
|
|
476
560
|
continue;
|
|
477
561
|
}
|
|
478
|
-
await this.closeActive(record.sessionId, {
|
|
562
|
+
await this.closeActive(record.sessionId, {
|
|
563
|
+
kind: "clear",
|
|
564
|
+
reason: "delete",
|
|
565
|
+
});
|
|
479
566
|
readyDeleteRecords.push(record);
|
|
480
567
|
}
|
|
481
568
|
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
@@ -491,7 +578,8 @@ export class PiSessionService {
|
|
|
491
578
|
});
|
|
492
579
|
}
|
|
493
580
|
async forceCleanup() {
|
|
494
|
-
if (this.archiveStore.deleteArchived === undefined &&
|
|
581
|
+
if (this.archiveStore.deleteArchived === undefined &&
|
|
582
|
+
this.archiveStore.deleteArchivedMany === undefined)
|
|
495
583
|
throw new Error("Archive store does not support deletion");
|
|
496
584
|
const archivedRecords = await this.archiveStore.list();
|
|
497
585
|
const plan = planForceCleanup({
|
|
@@ -506,7 +594,10 @@ export class PiSessionService {
|
|
|
506
594
|
skippedBusySessionIds.add(record.sessionId);
|
|
507
595
|
continue;
|
|
508
596
|
}
|
|
509
|
-
await this.closeActive(record.sessionId, {
|
|
597
|
+
await this.closeActive(record.sessionId, {
|
|
598
|
+
kind: "clear",
|
|
599
|
+
reason: "delete",
|
|
600
|
+
});
|
|
510
601
|
readyDeleteRecords.push(record);
|
|
511
602
|
}
|
|
512
603
|
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
@@ -587,14 +678,26 @@ export class PiSessionService {
|
|
|
587
678
|
}
|
|
588
679
|
async start(cwd, options) {
|
|
589
680
|
const modelPolicy = options?.modelPolicy;
|
|
590
|
-
return this.startSession(cwd, {
|
|
681
|
+
return this.startSession(cwd, {
|
|
682
|
+
initializeModelPolicy: modelPolicy ?? true,
|
|
683
|
+
});
|
|
591
684
|
}
|
|
592
685
|
async startSession(cwd, options) {
|
|
593
|
-
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
...(options.
|
|
597
|
-
|
|
686
|
+
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined
|
|
687
|
+
? undefined
|
|
688
|
+
: { parentSession: options.parentSession }), cwd, {
|
|
689
|
+
...(options.initialModel === undefined
|
|
690
|
+
? {}
|
|
691
|
+
: { initialModel: options.initialModel }),
|
|
692
|
+
...(options.initialThinkingLevel === undefined
|
|
693
|
+
? {}
|
|
694
|
+
: { initialThinkingLevel: options.initialThinkingLevel }),
|
|
695
|
+
...(options.creationProvenance === undefined
|
|
696
|
+
? {}
|
|
697
|
+
: { creationProvenance: options.creationProvenance }),
|
|
698
|
+
...(options.initializeModelPolicy === undefined
|
|
699
|
+
? {}
|
|
700
|
+
: { initializeModelPolicy: options.initializeModelPolicy }),
|
|
598
701
|
});
|
|
599
702
|
const { session } = active.runtime;
|
|
600
703
|
const created = {
|
|
@@ -608,7 +711,9 @@ export class PiSessionService {
|
|
|
608
711
|
firstMessage: "",
|
|
609
712
|
// Include the parent so listeners can nest the new session in the tree
|
|
610
713
|
// immediately, instead of showing it flat until the next reload.
|
|
611
|
-
...(options.parentSession === undefined
|
|
714
|
+
...(options.parentSession === undefined
|
|
715
|
+
? {}
|
|
716
|
+
: { parentSessionPath: options.parentSession }),
|
|
612
717
|
};
|
|
613
718
|
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
614
719
|
// session to their list without a manual reload.
|
|
@@ -628,7 +733,12 @@ export class PiSessionService {
|
|
|
628
733
|
throw spawnTargetError(decision);
|
|
629
734
|
const created = await this.startSession(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
|
|
630
735
|
await this.prompt(created.id, input.prompt);
|
|
631
|
-
this.logger.info({
|
|
736
|
+
this.logger.info({
|
|
737
|
+
spawningCwd: input.spawningCwd,
|
|
738
|
+
sessionId: created.id,
|
|
739
|
+
cwd: decision.cwd,
|
|
740
|
+
promptLength: input.prompt.length,
|
|
741
|
+
}, "spawn_session started a new session");
|
|
632
742
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
633
743
|
}
|
|
634
744
|
/**
|
|
@@ -646,9 +756,9 @@ export class PiSessionService {
|
|
|
646
756
|
let initialModel = input.model;
|
|
647
757
|
let initialThinkingLevel;
|
|
648
758
|
if (input.tier !== undefined) {
|
|
649
|
-
const echoedTier =
|
|
759
|
+
const echoedTier = leadingTierEcho(input.prompt);
|
|
650
760
|
if (echoedTier !== undefined && echoedTier !== input.tier) {
|
|
651
|
-
throw new Error(`Leading tier
|
|
761
|
+
throw new Error(`Leading Model tier: ${echoedTier} label disagrees with typed tier ${input.tier}`);
|
|
652
762
|
}
|
|
653
763
|
const resolved = this.modelTierRegistry.resolve(input.tier);
|
|
654
764
|
if (!isKnownThinkingLevel(resolved.thinkingLevel)) {
|
|
@@ -658,7 +768,9 @@ export class PiSessionService {
|
|
|
658
768
|
initialThinkingLevel = resolved.thinkingLevel;
|
|
659
769
|
}
|
|
660
770
|
const created = await this.startSession(decision.cwd, {
|
|
661
|
-
...(input.parentSessionFile === undefined
|
|
771
|
+
...(input.parentSessionFile === undefined
|
|
772
|
+
? {}
|
|
773
|
+
: { parentSession: input.parentSessionFile }),
|
|
662
774
|
...(initialModel === undefined ? {} : { initialModel }),
|
|
663
775
|
...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
|
|
664
776
|
creationProvenance: "tracked-subsession",
|
|
@@ -675,7 +787,12 @@ export class PiSessionService {
|
|
|
675
787
|
this.persistSubsessionLink(link);
|
|
676
788
|
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
677
789
|
await this.prompt(created.id, input.prompt);
|
|
678
|
-
this.logger.info({
|
|
790
|
+
this.logger.info({
|
|
791
|
+
parentSessionId: input.parentSessionId,
|
|
792
|
+
sessionId: created.id,
|
|
793
|
+
cwd: decision.cwd,
|
|
794
|
+
promptLength: input.prompt.length,
|
|
795
|
+
}, "spawn_subsession started a tracked child session");
|
|
679
796
|
return { sessionId: created.id, cwd: decision.cwd };
|
|
680
797
|
}
|
|
681
798
|
/** Summaries of the tracked subsessions spawned by `parentSessionId`. */
|
|
@@ -686,7 +803,10 @@ export class PiSessionService {
|
|
|
686
803
|
if (childIds === undefined)
|
|
687
804
|
return [];
|
|
688
805
|
const authorizedChildIds = [...childIds].filter((childId) => this.subsessionLinkBelongsToParent(parentSessionId, parentFile, childId));
|
|
689
|
-
return Promise.all(authorizedChildIds.map(async (childId) => ({
|
|
806
|
+
return Promise.all(authorizedChildIds.map(async (childId) => ({
|
|
807
|
+
sessionId: childId,
|
|
808
|
+
...(await this.subsessionSummaryFields(childId)),
|
|
809
|
+
})));
|
|
690
810
|
}
|
|
691
811
|
/** Status and final result of a subsession, scoped to the caller's children. */
|
|
692
812
|
async checkSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
@@ -715,7 +835,8 @@ export class PiSessionService {
|
|
|
715
835
|
async openSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
716
836
|
const parentFile = nonEmptyString(parentSessionFile);
|
|
717
837
|
await this.hydrateSubsessionsForParent(parentSessionId, parentFile);
|
|
718
|
-
if (this.subsessionParents.get(sessionId) !== parentSessionId ||
|
|
838
|
+
if (this.subsessionParents.get(sessionId) !== parentSessionId ||
|
|
839
|
+
!this.subsessionLinkBelongsToParent(parentSessionId, parentFile, sessionId)) {
|
|
719
840
|
throw new Error(`Session ${sessionId} is not one of your subsessions`);
|
|
720
841
|
}
|
|
721
842
|
return this.getOrOpenTrackedSubsession(sessionId);
|
|
@@ -724,19 +845,24 @@ export class PiSessionService {
|
|
|
724
845
|
const link = this.subsessionLinks.get(childSessionId);
|
|
725
846
|
if (link?.parentSessionId !== parentSessionId)
|
|
726
847
|
return false;
|
|
727
|
-
return parentSessionFile === undefined ||
|
|
848
|
+
return (parentSessionFile === undefined ||
|
|
849
|
+
trackedLinkParentFileMatches(link, parentSessionFile));
|
|
728
850
|
}
|
|
729
851
|
activeChildForSubsessionLink(link) {
|
|
730
852
|
const active = this.active.get(link.childSessionId);
|
|
731
853
|
if (active === undefined)
|
|
732
854
|
return undefined;
|
|
733
|
-
return activeSessionFileMatches(active, link.childSessionFile)
|
|
855
|
+
return activeSessionFileMatches(active, link.childSessionFile)
|
|
856
|
+
? active
|
|
857
|
+
: undefined;
|
|
734
858
|
}
|
|
735
859
|
activeParentForSubsessionLink(link) {
|
|
736
860
|
const active = this.active.get(link.parentSessionId);
|
|
737
861
|
if (active === undefined)
|
|
738
862
|
return undefined;
|
|
739
|
-
return activeSessionFileMatches(active, link.parentSessionFile)
|
|
863
|
+
return activeSessionFileMatches(active, link.parentSessionFile)
|
|
864
|
+
? active
|
|
865
|
+
: undefined;
|
|
740
866
|
}
|
|
741
867
|
subsessionLinkForActiveChild(session) {
|
|
742
868
|
const childId = session.sessionId;
|
|
@@ -744,12 +870,15 @@ export class PiSessionService {
|
|
|
744
870
|
const link = this.subsessionLinks.get(childId);
|
|
745
871
|
if (parentId === undefined || link?.parentSessionId !== parentId)
|
|
746
872
|
return undefined;
|
|
747
|
-
return sessionFileMatches(session, link.childSessionFile)
|
|
873
|
+
return sessionFileMatches(session, link.childSessionFile)
|
|
874
|
+
? link
|
|
875
|
+
: undefined;
|
|
748
876
|
}
|
|
749
877
|
async registerVerifiedSubsession(link) {
|
|
750
878
|
const { childSessionId, parentSessionId } = link;
|
|
751
879
|
const previousParentId = this.subsessionParents.get(childSessionId);
|
|
752
|
-
if (previousParentId !== undefined &&
|
|
880
|
+
if (previousParentId !== undefined &&
|
|
881
|
+
previousParentId !== parentSessionId) {
|
|
753
882
|
const previousChildren = this.subsessionChildren.get(previousParentId);
|
|
754
883
|
previousChildren?.delete(childSessionId);
|
|
755
884
|
if (previousChildren?.size === 0)
|
|
@@ -803,7 +932,11 @@ export class PiSessionService {
|
|
|
803
932
|
parent.sessionManager.appendCustomEntry(SUBSESSION_LINK_CUSTOM_TYPE, persistedParentSubsessionLinkData(link));
|
|
804
933
|
}
|
|
805
934
|
catch (error) {
|
|
806
|
-
this.logger.info({
|
|
935
|
+
this.logger.info({
|
|
936
|
+
parentSessionId: link.parentSessionId,
|
|
937
|
+
sessionId: link.childSessionId,
|
|
938
|
+
error: error instanceof Error ? error.message : String(error),
|
|
939
|
+
}, "failed to persist subsession link");
|
|
807
940
|
}
|
|
808
941
|
}
|
|
809
942
|
persistSubsessionChildMarker(parentSessionId, childSessionId) {
|
|
@@ -816,7 +949,11 @@ export class PiSessionService {
|
|
|
816
949
|
child.sessionManager.appendCustomEntry(SUBSESSION_CHILD_LINK_CUSTOM_TYPE, persistedChildSubsessionLinkData(parentSessionId, childSessionId));
|
|
817
950
|
}
|
|
818
951
|
catch (error) {
|
|
819
|
-
this.logger.info({
|
|
952
|
+
this.logger.info({
|
|
953
|
+
parentSessionId,
|
|
954
|
+
sessionId: childSessionId,
|
|
955
|
+
error: error instanceof Error ? error.message : String(error),
|
|
956
|
+
}, "failed to persist subsession child marker");
|
|
820
957
|
}
|
|
821
958
|
}
|
|
822
959
|
async hydrateSubsessionsForParent(parentSessionId, parentSessionFile) {
|
|
@@ -824,7 +961,9 @@ export class PiSessionService {
|
|
|
824
961
|
if (this.subsessionHydratedParents.has(hydrationKey))
|
|
825
962
|
return;
|
|
826
963
|
const activeParent = this.active.get(parentSessionId);
|
|
827
|
-
if (activeParent !== undefined &&
|
|
964
|
+
if (activeParent !== undefined &&
|
|
965
|
+
(parentSessionFile === undefined ||
|
|
966
|
+
activeSessionFileMatches(activeParent, parentSessionFile))) {
|
|
828
967
|
const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
|
|
829
968
|
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile);
|
|
830
969
|
if (complete)
|
|
@@ -833,7 +972,8 @@ export class PiSessionService {
|
|
|
833
972
|
}
|
|
834
973
|
if (parentSessionFile === undefined)
|
|
835
974
|
return;
|
|
836
|
-
if ((await readSessionHeaderSummary(parentSessionFile))?.id !==
|
|
975
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !==
|
|
976
|
+
parentSessionId)
|
|
837
977
|
return;
|
|
838
978
|
let parentManager;
|
|
839
979
|
try {
|
|
@@ -875,8 +1015,11 @@ export class PiSessionService {
|
|
|
875
1015
|
return trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessionFile);
|
|
876
1016
|
}
|
|
877
1017
|
async parentLinkHasValidChildTarget(parentSessionFile, link) {
|
|
878
|
-
return link.spawnedSessionFile !== undefined
|
|
879
|
-
|
|
1018
|
+
return (link.spawnedSessionFile !== undefined &&
|
|
1019
|
+
(await sessionFileHeaderMatches(link.spawnedSessionFile, {
|
|
1020
|
+
sessionId: link.spawnedSessionId,
|
|
1021
|
+
parentSessionFile,
|
|
1022
|
+
})));
|
|
880
1023
|
}
|
|
881
1024
|
async recoverSubsessionTrackingForOpenedSession(session) {
|
|
882
1025
|
const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
|
|
@@ -900,7 +1043,10 @@ export class PiSessionService {
|
|
|
900
1043
|
if (active !== undefined)
|
|
901
1044
|
return active.runtime.session;
|
|
902
1045
|
if (link.childSessionFile !== undefined) {
|
|
903
|
-
if (!(await sessionFileHeaderMatches(link.childSessionFile, {
|
|
1046
|
+
if (!(await sessionFileHeaderMatches(link.childSessionFile, {
|
|
1047
|
+
sessionId,
|
|
1048
|
+
parentSessionFile: link.parentSessionFile,
|
|
1049
|
+
})))
|
|
904
1050
|
throw new Error("Session not found");
|
|
905
1051
|
const sessionManager = this.sessionManager.open(link.childSessionFile);
|
|
906
1052
|
return (await this.create(sessionManager, link.cwd ?? sessionManager.getCwd())).runtime.session;
|
|
@@ -911,9 +1057,16 @@ export class PiSessionService {
|
|
|
911
1057
|
const link = this.subsessionLinks.get(childSessionId);
|
|
912
1058
|
const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
|
|
913
1059
|
if (active !== undefined) {
|
|
914
|
-
return {
|
|
1060
|
+
return {
|
|
1061
|
+
cwd: active.runtime.cwd,
|
|
1062
|
+
status: this.subsessionStatus(active.runtime.session),
|
|
1063
|
+
};
|
|
915
1064
|
}
|
|
916
|
-
if (link?.childSessionFile !== undefined &&
|
|
1065
|
+
if (link?.childSessionFile !== undefined &&
|
|
1066
|
+
(await sessionFileHeaderMatches(link.childSessionFile, {
|
|
1067
|
+
sessionId: childSessionId,
|
|
1068
|
+
parentSessionFile: link.parentSessionFile,
|
|
1069
|
+
}))) {
|
|
917
1070
|
return { cwd: link.cwd ?? "", status: "idle" };
|
|
918
1071
|
}
|
|
919
1072
|
if (link?.cwd !== undefined)
|
|
@@ -933,7 +1086,9 @@ export class PiSessionService {
|
|
|
933
1086
|
return [];
|
|
934
1087
|
return [...childIds].filter((childId) => {
|
|
935
1088
|
const link = this.subsessionLinks.get(childId);
|
|
936
|
-
const active = link === undefined
|
|
1089
|
+
const active = link === undefined
|
|
1090
|
+
? undefined
|
|
1091
|
+
: this.activeChildForSubsessionLink(link);
|
|
937
1092
|
return active !== undefined && this.hasActiveWork(active.runtime.session);
|
|
938
1093
|
});
|
|
939
1094
|
}
|
|
@@ -975,11 +1130,13 @@ export class PiSessionService {
|
|
|
975
1130
|
const parentSessionFile = link.parentSessionFile;
|
|
976
1131
|
if (parentSessionFile === undefined)
|
|
977
1132
|
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
978
|
-
if ((await readSessionHeaderSummary(parentSessionFile))?.id !==
|
|
1133
|
+
if ((await readSessionHeaderSummary(parentSessionFile))?.id !==
|
|
1134
|
+
parentSessionId) {
|
|
979
1135
|
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
980
1136
|
}
|
|
981
1137
|
const sessionManager = this.sessionManager.open(parentSessionFile);
|
|
982
|
-
return (await this.create(sessionManager, sessionManager.getCwd())).runtime
|
|
1138
|
+
return (await this.create(sessionManager, sessionManager.getCwd())).runtime
|
|
1139
|
+
.session;
|
|
983
1140
|
}
|
|
984
1141
|
/**
|
|
985
1142
|
* Deliver a subsession-completion notice to the parent as a system-authored
|
|
@@ -997,18 +1154,31 @@ export class PiSessionService {
|
|
|
997
1154
|
this.deferredSubsessionNotifications.set(session, pending);
|
|
998
1155
|
return;
|
|
999
1156
|
}
|
|
1000
|
-
await this.deliverSubsessionNotification(session, {
|
|
1157
|
+
await this.deliverSubsessionNotification(session, {
|
|
1158
|
+
parentId,
|
|
1159
|
+
childId,
|
|
1160
|
+
text,
|
|
1161
|
+
});
|
|
1001
1162
|
}
|
|
1002
1163
|
catch (error) {
|
|
1003
1164
|
this.logSubsessionNotificationFailure(parentId, childId, error);
|
|
1004
1165
|
}
|
|
1005
1166
|
}
|
|
1006
1167
|
async deliverSubsessionNotification(session, notification) {
|
|
1007
|
-
await this.runSessionEntryMutation(session, "deliver a subsession notification", () => session.sendCustomMessage({
|
|
1168
|
+
await this.runSessionEntryMutation(session, "deliver a subsession notification", () => session.sendCustomMessage({
|
|
1169
|
+
customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE,
|
|
1170
|
+
content: notification.text,
|
|
1171
|
+
display: true,
|
|
1172
|
+
details: { sessionId: notification.childId },
|
|
1173
|
+
}, { triggerTurn: true, deliverAs: "followUp" }));
|
|
1008
1174
|
this.publishStatus(session);
|
|
1009
1175
|
}
|
|
1010
1176
|
logSubsessionNotificationFailure(parentId, childId, error) {
|
|
1011
|
-
this.logger.info({
|
|
1177
|
+
this.logger.info({
|
|
1178
|
+
parentSessionId: parentId,
|
|
1179
|
+
sessionId: childId,
|
|
1180
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1181
|
+
}, "failed to notify parent of subsession completion");
|
|
1012
1182
|
}
|
|
1013
1183
|
async exportHistory(ref) {
|
|
1014
1184
|
return exportSessionHistoryHtml(await this.sessionFileForHistory(ref));
|
|
@@ -1048,7 +1218,9 @@ export class PiSessionService {
|
|
|
1048
1218
|
const current = inspection.kind === "invalid" ? inspection.fallback : inspection.policy;
|
|
1049
1219
|
const policy = await this.applySessionModelPolicy(session, current, update, {
|
|
1050
1220
|
action,
|
|
1051
|
-
onTransitionFailure: (reason) => {
|
|
1221
|
+
onTransitionFailure: (reason) => {
|
|
1222
|
+
this.publishModelPolicyTransitionFailure(session, reason);
|
|
1223
|
+
},
|
|
1052
1224
|
});
|
|
1053
1225
|
const resolved = this.exactSelectionFromSession(session);
|
|
1054
1226
|
this.publishActivity(session, `model policy: ${policy.mode}`, "idle", policy.mode === "tiered" ? policy.tier : resolved.model.id);
|
|
@@ -1095,8 +1267,7 @@ export class PiSessionService {
|
|
|
1095
1267
|
const candidates = session.scopedModels.length > 0
|
|
1096
1268
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
1097
1269
|
: session.modelRuntime.getAvailableSnapshot();
|
|
1098
|
-
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
|
|
1099
|
-
?? session.modelRuntime.getModel(provider, modelId);
|
|
1270
|
+
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId) ?? session.modelRuntime.getModel(provider, modelId);
|
|
1100
1271
|
if (model === undefined)
|
|
1101
1272
|
throw new Error(`Model not found: ${provider}/${modelId}`);
|
|
1102
1273
|
await this.runExactModelPolicyMutation(session, "change models", currentPolicy, () => session.setModel(model));
|
|
@@ -1111,7 +1282,9 @@ export class PiSessionService {
|
|
|
1111
1282
|
const result = await this.runExactModelPolicyMutation(session, "change models", currentPolicy, async () => {
|
|
1112
1283
|
const cycled = await session.cycleModel(direction);
|
|
1113
1284
|
if (cycled === undefined) {
|
|
1114
|
-
throw new Error(session.scopedModels.length > 0
|
|
1285
|
+
throw new Error(session.scopedModels.length > 0
|
|
1286
|
+
? "Only one model in scope"
|
|
1287
|
+
: "Only one model available");
|
|
1115
1288
|
}
|
|
1116
1289
|
return cycled;
|
|
1117
1290
|
});
|
|
@@ -1159,13 +1332,31 @@ export class PiSessionService {
|
|
|
1159
1332
|
const session = await this.getOrOpen(ref);
|
|
1160
1333
|
const commands = [...BUILTIN_COMMANDS];
|
|
1161
1334
|
for (const command of session.extensionRunner.getRegisteredCommands()) {
|
|
1162
|
-
commands.push({
|
|
1335
|
+
commands.push({
|
|
1336
|
+
name: command.invocationName,
|
|
1337
|
+
...(command.description === undefined
|
|
1338
|
+
? {}
|
|
1339
|
+
: { description: command.description }),
|
|
1340
|
+
source: "extension",
|
|
1341
|
+
});
|
|
1163
1342
|
}
|
|
1164
1343
|
for (const template of session.promptTemplates) {
|
|
1165
|
-
commands.push({
|
|
1344
|
+
commands.push({
|
|
1345
|
+
name: template.name,
|
|
1346
|
+
...(template.description === undefined
|
|
1347
|
+
? {}
|
|
1348
|
+
: { description: template.description }),
|
|
1349
|
+
source: "prompt",
|
|
1350
|
+
});
|
|
1166
1351
|
}
|
|
1167
1352
|
for (const skill of session.resourceLoader.getSkills().skills) {
|
|
1168
|
-
commands.push({
|
|
1353
|
+
commands.push({
|
|
1354
|
+
name: `skill:${skill.name}`,
|
|
1355
|
+
...(skill.description === undefined
|
|
1356
|
+
? {}
|
|
1357
|
+
: { description: skill.description }),
|
|
1358
|
+
source: "skill",
|
|
1359
|
+
});
|
|
1169
1360
|
}
|
|
1170
1361
|
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
1171
1362
|
}
|
|
@@ -1177,7 +1368,9 @@ export class PiSessionService {
|
|
|
1177
1368
|
// as a transient line that vanishes on reload.
|
|
1178
1369
|
const echoUserMessage = options?.echoUserMessage !== false;
|
|
1179
1370
|
const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
|
|
1180
|
-
const parsedAttachments = parsePromptAttachments(attachments, {
|
|
1371
|
+
const parsedAttachments = parsePromptAttachments(attachments, {
|
|
1372
|
+
enforceInlineSizeLimit: false,
|
|
1373
|
+
});
|
|
1181
1374
|
const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
|
|
1182
1375
|
await this.assertWritable(ref);
|
|
1183
1376
|
const session = await this.getOrOpen(ref);
|
|
@@ -1186,7 +1379,9 @@ export class PiSessionService {
|
|
|
1186
1379
|
this.maybeGenerateSessionName(session, promptText);
|
|
1187
1380
|
const isQueued = session.isStreaming || session.isCompacting;
|
|
1188
1381
|
const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
|
|
1189
|
-
if (isQueued &&
|
|
1382
|
+
if (isQueued &&
|
|
1383
|
+
images.length === 0 &&
|
|
1384
|
+
this.hasQueuedMessageText(session, promptText)) {
|
|
1190
1385
|
this.publishActivity(session, "duplicate queued message ignored", "active");
|
|
1191
1386
|
this.publishStatus(session);
|
|
1192
1387
|
return;
|
|
@@ -1217,24 +1412,42 @@ export class PiSessionService {
|
|
|
1217
1412
|
catch (error) {
|
|
1218
1413
|
const message = error instanceof Error ? error.message : String(error);
|
|
1219
1414
|
this.publishActivity(session, "error", "error", message);
|
|
1220
|
-
this.events.publish(session.sessionId, {
|
|
1415
|
+
this.events.publish(session.sessionId, {
|
|
1416
|
+
type: "session.error",
|
|
1417
|
+
message,
|
|
1418
|
+
});
|
|
1221
1419
|
return Promise.resolve();
|
|
1222
1420
|
}
|
|
1223
|
-
this.publishActivity(session, behavior === "steer"
|
|
1421
|
+
this.publishActivity(session, behavior === "steer"
|
|
1422
|
+
? "steering queued"
|
|
1423
|
+
: behavior === "followUp"
|
|
1424
|
+
? "message queued"
|
|
1425
|
+
: "prompt accepted", "active");
|
|
1224
1426
|
if (behavior === undefined && echoUserMessage)
|
|
1225
|
-
this.events.publish(session.sessionId, {
|
|
1427
|
+
this.events.publish(session.sessionId, {
|
|
1428
|
+
type: "message.append",
|
|
1429
|
+
message: userMessage(text, images),
|
|
1430
|
+
});
|
|
1226
1431
|
const promptOptions = buildPromptOptions(behavior, images);
|
|
1227
1432
|
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions)).catch((error) => {
|
|
1228
1433
|
const message = error instanceof Error ? error.message : String(error);
|
|
1229
1434
|
this.publishActivity(session, "error", "error", message);
|
|
1230
|
-
this.events.publish(session.sessionId, {
|
|
1435
|
+
this.events.publish(session.sessionId, {
|
|
1436
|
+
type: "session.error",
|
|
1437
|
+
message,
|
|
1438
|
+
});
|
|
1231
1439
|
});
|
|
1232
1440
|
void promptPromise;
|
|
1233
1441
|
return promptPromise;
|
|
1234
1442
|
}
|
|
1235
1443
|
enqueuePromptDuringCompaction(session, text, kind, images = [], echoUserMessage = true) {
|
|
1236
1444
|
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
1237
|
-
queue.push({
|
|
1445
|
+
queue.push({
|
|
1446
|
+
kind,
|
|
1447
|
+
text,
|
|
1448
|
+
...(images.length > 0 ? { images } : {}),
|
|
1449
|
+
...(echoUserMessage ? {} : { echoUserMessage: false }),
|
|
1450
|
+
});
|
|
1238
1451
|
this.compactionPromptQueues.set(session.sessionId, queue);
|
|
1239
1452
|
this.publishActivity(session, "message queued during compaction", "active");
|
|
1240
1453
|
this.publishStatus(session);
|
|
@@ -1247,7 +1460,12 @@ export class PiSessionService {
|
|
|
1247
1460
|
*/
|
|
1248
1461
|
retainPromptDuringModelPolicyMutation(session, text, kind, images = [], echoUserMessage = true, position = "back") {
|
|
1249
1462
|
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
1250
|
-
const queued = {
|
|
1463
|
+
const queued = {
|
|
1464
|
+
kind,
|
|
1465
|
+
text,
|
|
1466
|
+
...(images.length > 0 ? { images } : {}),
|
|
1467
|
+
...(echoUserMessage ? {} : { echoUserMessage: false }),
|
|
1468
|
+
};
|
|
1251
1469
|
if (position === "front")
|
|
1252
1470
|
queue.unshift(queued);
|
|
1253
1471
|
else
|
|
@@ -1257,7 +1475,10 @@ export class PiSessionService {
|
|
|
1257
1475
|
this.publishStatus(session);
|
|
1258
1476
|
}
|
|
1259
1477
|
async saveAttachments(ref, attachments, folder) {
|
|
1260
|
-
const parsed = parsePromptAttachments(attachments, {
|
|
1478
|
+
const parsed = parsePromptAttachments(attachments, {
|
|
1479
|
+
enforceInlineSizeLimit: false,
|
|
1480
|
+
allowFileAttachments: true,
|
|
1481
|
+
});
|
|
1261
1482
|
if (parsed.length === 0)
|
|
1262
1483
|
return [];
|
|
1263
1484
|
await this.assertWritable(ref);
|
|
@@ -1276,26 +1497,46 @@ export class PiSessionService {
|
|
|
1276
1497
|
if (session.isBashRunning)
|
|
1277
1498
|
throw new Error("A bash command is already running");
|
|
1278
1499
|
this.publishActivity(session, "running bash", "active", command);
|
|
1279
|
-
this.events.publish(session.sessionId, {
|
|
1500
|
+
this.events.publish(session.sessionId, {
|
|
1501
|
+
type: "shell.start",
|
|
1502
|
+
command,
|
|
1503
|
+
excludeFromContext: isExcluded,
|
|
1504
|
+
});
|
|
1280
1505
|
void this.runSessionEntryMutation(session, "run a shell command", () => session.executeBash(command, (chunk) => {
|
|
1281
|
-
this.events.publish(session.sessionId, {
|
|
1506
|
+
this.events.publish(session.sessionId, {
|
|
1507
|
+
type: "shell.chunk",
|
|
1508
|
+
chunk,
|
|
1509
|
+
});
|
|
1282
1510
|
this.publishActivity(session, "running bash", "active", command);
|
|
1283
1511
|
this.publishStatus(session);
|
|
1284
|
-
}, { excludeFromContext: isExcluded }))
|
|
1512
|
+
}, { excludeFromContext: isExcluded }))
|
|
1513
|
+
.then((result) => {
|
|
1285
1514
|
this.events.publish(session.sessionId, {
|
|
1286
1515
|
type: "shell.end",
|
|
1287
1516
|
output: result.output,
|
|
1288
|
-
...(result.exitCode === undefined
|
|
1517
|
+
...(result.exitCode === undefined
|
|
1518
|
+
? {}
|
|
1519
|
+
: { exitCode: result.exitCode }),
|
|
1289
1520
|
cancelled: result.cancelled,
|
|
1290
1521
|
truncated: result.truncated,
|
|
1291
|
-
...(result.fullOutputPath === undefined
|
|
1522
|
+
...(result.fullOutputPath === undefined
|
|
1523
|
+
? {}
|
|
1524
|
+
: { fullOutputPath: result.fullOutputPath }),
|
|
1292
1525
|
});
|
|
1293
1526
|
this.publishActivity(session, "bash complete", result.exitCode === 0 ? "idle" : "error", command);
|
|
1294
1527
|
this.publishStatus(session);
|
|
1295
|
-
})
|
|
1528
|
+
})
|
|
1529
|
+
.catch((error) => {
|
|
1296
1530
|
const message = error instanceof Error ? error.message : String(error);
|
|
1297
|
-
this.events.publish(session.sessionId, {
|
|
1298
|
-
|
|
1531
|
+
this.events.publish(session.sessionId, {
|
|
1532
|
+
type: "shell.end",
|
|
1533
|
+
output: message,
|
|
1534
|
+
isError: true,
|
|
1535
|
+
});
|
|
1536
|
+
this.events.publish(session.sessionId, {
|
|
1537
|
+
type: "session.error",
|
|
1538
|
+
message,
|
|
1539
|
+
});
|
|
1299
1540
|
this.publishActivity(session, "bash failed", "error", message);
|
|
1300
1541
|
this.publishStatus(session);
|
|
1301
1542
|
});
|
|
@@ -1327,7 +1568,13 @@ export class PiSessionService {
|
|
|
1327
1568
|
await this.assertWritable(ref);
|
|
1328
1569
|
const active = await this.getActive(ref);
|
|
1329
1570
|
const sourceSession = active.runtime.session;
|
|
1330
|
-
const result = await this.runTreeExclusiveOperation([
|
|
1571
|
+
const result = await this.runTreeExclusiveOperation([
|
|
1572
|
+
{
|
|
1573
|
+
sessionId: sourceSession.sessionId,
|
|
1574
|
+
session: sourceSession,
|
|
1575
|
+
runtime: active.runtime,
|
|
1576
|
+
},
|
|
1577
|
+
], "Stop current session activity before replacing the session", () => active.runtime.fork(entryId));
|
|
1331
1578
|
if (result.cancelled)
|
|
1332
1579
|
return { cancelled: true };
|
|
1333
1580
|
const session = clientSessionFromRuntime(active.runtime);
|
|
@@ -1350,7 +1597,8 @@ export class PiSessionService {
|
|
|
1350
1597
|
// may enter this runtime until Pi's potentially asynchronous summary settles.
|
|
1351
1598
|
this.treeNavigations.add(session);
|
|
1352
1599
|
try {
|
|
1353
|
-
if (expectedLeafId !== undefined &&
|
|
1600
|
+
if (expectedLeafId !== undefined &&
|
|
1601
|
+
session.sessionManager.getLeafId() !== expectedLeafId) {
|
|
1354
1602
|
throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
|
|
1355
1603
|
}
|
|
1356
1604
|
this.publishActivity(session, options.summarize ? "summarizing branch" : "navigating session tree", "active");
|
|
@@ -1358,19 +1606,32 @@ export class PiSessionService {
|
|
|
1358
1606
|
const result = await session.navigateTree(targetId, options);
|
|
1359
1607
|
if (result.cancelled) {
|
|
1360
1608
|
if (this.isCurrentActiveSession(session)) {
|
|
1361
|
-
this.publishActivity(session, result.aborted === true
|
|
1609
|
+
this.publishActivity(session, result.aborted === true
|
|
1610
|
+
? "branch summary aborted"
|
|
1611
|
+
: "tree navigation cancelled", "idle");
|
|
1362
1612
|
}
|
|
1363
|
-
return {
|
|
1613
|
+
return {
|
|
1614
|
+
cancelled: true,
|
|
1615
|
+
...(result.aborted === undefined ? {} : { aborted: result.aborted }),
|
|
1616
|
+
};
|
|
1364
1617
|
}
|
|
1365
1618
|
if (this.isCurrentActiveSession(session))
|
|
1366
1619
|
this.publishActivity(session, "session tree navigated", "idle");
|
|
1367
|
-
return {
|
|
1620
|
+
return {
|
|
1621
|
+
cancelled: false,
|
|
1622
|
+
...(result.editorText === undefined
|
|
1623
|
+
? {}
|
|
1624
|
+
: { editorText: result.editorText }),
|
|
1625
|
+
};
|
|
1368
1626
|
}
|
|
1369
1627
|
catch (error) {
|
|
1370
1628
|
const message = error instanceof Error ? error.message : String(error);
|
|
1371
1629
|
if (this.isCurrentActiveSession(session)) {
|
|
1372
1630
|
this.publishActivity(session, "tree navigation failed", "error", message);
|
|
1373
|
-
this.events.publish(session.sessionId, {
|
|
1631
|
+
this.events.publish(session.sessionId, {
|
|
1632
|
+
type: "session.error",
|
|
1633
|
+
message,
|
|
1634
|
+
});
|
|
1374
1635
|
}
|
|
1375
1636
|
throw error;
|
|
1376
1637
|
}
|
|
@@ -1394,13 +1655,16 @@ export class PiSessionService {
|
|
|
1394
1655
|
const priorGeneration = this.notificationGenerationBySession.get(session);
|
|
1395
1656
|
let candidateGeneration;
|
|
1396
1657
|
try {
|
|
1397
|
-
await session.reload(priorGeneration === undefined
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1658
|
+
await session.reload(priorGeneration === undefined
|
|
1659
|
+
? undefined
|
|
1660
|
+
: {
|
|
1661
|
+
beforeSessionStart: () => {
|
|
1662
|
+
candidateGeneration =
|
|
1663
|
+
this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
1664
|
+
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
1665
|
+
this.replaceSessionNotificationContext(session, candidateGeneration);
|
|
1666
|
+
},
|
|
1667
|
+
});
|
|
1404
1668
|
if (candidateGeneration !== undefined) {
|
|
1405
1669
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1406
1670
|
}
|
|
@@ -1414,7 +1678,10 @@ export class PiSessionService {
|
|
|
1414
1678
|
}
|
|
1415
1679
|
const message = error instanceof Error ? error.message : String(error);
|
|
1416
1680
|
this.publishActivity(session, "reload failed", "error", message);
|
|
1417
|
-
this.events.publish(session.sessionId, {
|
|
1681
|
+
this.events.publish(session.sessionId, {
|
|
1682
|
+
type: "session.error",
|
|
1683
|
+
message,
|
|
1684
|
+
});
|
|
1418
1685
|
this.publishStatus(session);
|
|
1419
1686
|
throw error;
|
|
1420
1687
|
}
|
|
@@ -1426,7 +1693,10 @@ export class PiSessionService {
|
|
|
1426
1693
|
throw new Error("Stop current session activity before archiving");
|
|
1427
1694
|
await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before archiving", async () => {
|
|
1428
1695
|
const archiveInput = await this.archiveInputForSession(session);
|
|
1429
|
-
await this.closeActive(session.sessionId, {
|
|
1696
|
+
await this.closeActive(session.sessionId, {
|
|
1697
|
+
kind: "clear",
|
|
1698
|
+
reason: "archive",
|
|
1699
|
+
});
|
|
1430
1700
|
await this.archiveStore.archive(archiveInput);
|
|
1431
1701
|
await this.forgetUnreadSessions([archiveInput]);
|
|
1432
1702
|
});
|
|
@@ -1453,7 +1723,10 @@ export class PiSessionService {
|
|
|
1453
1723
|
const listed = findListedSessionForBulkRef(sessionContext, ref);
|
|
1454
1724
|
const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
|
|
1455
1725
|
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1456
|
-
failures.push({
|
|
1726
|
+
failures.push({
|
|
1727
|
+
sessionId: resolvedSessionId,
|
|
1728
|
+
error: "Stop current session activity before archiving",
|
|
1729
|
+
});
|
|
1457
1730
|
continue;
|
|
1458
1731
|
}
|
|
1459
1732
|
try {
|
|
@@ -1461,21 +1734,32 @@ export class PiSessionService {
|
|
|
1461
1734
|
planItems.push({ input: archiveInputFromListEntry(listed) });
|
|
1462
1735
|
}
|
|
1463
1736
|
else if (active !== undefined) {
|
|
1464
|
-
planItems.push({
|
|
1737
|
+
planItems.push({
|
|
1738
|
+
input: archiveInputFromActiveSession(active.runtime.session),
|
|
1739
|
+
});
|
|
1465
1740
|
}
|
|
1466
1741
|
else {
|
|
1467
1742
|
failures.push({ sessionId: ref.id, error: "Session not found" });
|
|
1468
1743
|
}
|
|
1469
1744
|
}
|
|
1470
1745
|
catch (error) {
|
|
1471
|
-
failures.push({
|
|
1746
|
+
failures.push({
|
|
1747
|
+
sessionId: resolvedSessionId,
|
|
1748
|
+
error: errorMessage(error),
|
|
1749
|
+
});
|
|
1472
1750
|
}
|
|
1473
1751
|
}
|
|
1474
1752
|
const readyPlanItems = [];
|
|
1475
1753
|
for (const item of planItems) {
|
|
1476
|
-
const active = this.activeForLookup({
|
|
1754
|
+
const active = this.activeForLookup({
|
|
1755
|
+
id: item.input.sessionId,
|
|
1756
|
+
cwd: item.input.cwd,
|
|
1757
|
+
});
|
|
1477
1758
|
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1478
|
-
failures.push({
|
|
1759
|
+
failures.push({
|
|
1760
|
+
sessionId: item.input.sessionId,
|
|
1761
|
+
error: "Stop current session activity before archiving",
|
|
1762
|
+
});
|
|
1479
1763
|
continue;
|
|
1480
1764
|
}
|
|
1481
1765
|
readyPlanItems.push(active === undefined ? item : { ...item, active });
|
|
@@ -1484,15 +1768,23 @@ export class PiSessionService {
|
|
|
1484
1768
|
const archivedSessionIds = [...alreadyArchivedSessionIds];
|
|
1485
1769
|
await this.runTreeExclusiveOperation(readyPlanItems.map(({ input, active }) => ({
|
|
1486
1770
|
sessionId: input.sessionId,
|
|
1487
|
-
...(active === undefined
|
|
1771
|
+
...(active === undefined
|
|
1772
|
+
? {}
|
|
1773
|
+
: { session: active.runtime.session, runtime: active.runtime }),
|
|
1488
1774
|
})), "Stop current session activity before archiving", async () => {
|
|
1489
1775
|
for (const item of readyPlanItems) {
|
|
1490
1776
|
try {
|
|
1491
|
-
await this.closeActive(item.input.sessionId, {
|
|
1777
|
+
await this.closeActive(item.input.sessionId, {
|
|
1778
|
+
kind: "clear",
|
|
1779
|
+
reason: "archive",
|
|
1780
|
+
});
|
|
1492
1781
|
readyInputs.push(item.input);
|
|
1493
1782
|
}
|
|
1494
1783
|
catch (error) {
|
|
1495
|
-
failures.push({
|
|
1784
|
+
failures.push({
|
|
1785
|
+
sessionId: item.input.sessionId,
|
|
1786
|
+
error: errorMessage(error),
|
|
1787
|
+
});
|
|
1496
1788
|
}
|
|
1497
1789
|
}
|
|
1498
1790
|
try {
|
|
@@ -1502,7 +1794,10 @@ export class PiSessionService {
|
|
|
1502
1794
|
}
|
|
1503
1795
|
catch (error) {
|
|
1504
1796
|
for (const input of readyInputs)
|
|
1505
|
-
failures.push({
|
|
1797
|
+
failures.push({
|
|
1798
|
+
sessionId: input.sessionId,
|
|
1799
|
+
error: errorMessage(error),
|
|
1800
|
+
});
|
|
1506
1801
|
}
|
|
1507
1802
|
});
|
|
1508
1803
|
await this.forgetUnreadSessions(unreadArchivedIdentities);
|
|
@@ -1516,22 +1811,30 @@ export class PiSessionService {
|
|
|
1516
1811
|
async archiveTree(ref) {
|
|
1517
1812
|
const session = await this.getOrOpen(ref);
|
|
1518
1813
|
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
1519
|
-
const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ??
|
|
1814
|
+
const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ??
|
|
1815
|
+
archiveCandidateFromActiveSession(session, false);
|
|
1520
1816
|
const plan = planSessionArchiveTree(root, catalog);
|
|
1521
|
-
const busy = plan.targets
|
|
1817
|
+
const busy = plan.targets
|
|
1818
|
+
.map((target) => target.activeSession)
|
|
1819
|
+
.find((target) => target !== undefined && this.hasActiveWork(target));
|
|
1522
1820
|
if (busy !== undefined)
|
|
1523
1821
|
throw new Error(`Stop current session activity before archiving ${sessionDisplayName(busy)}`);
|
|
1524
1822
|
const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
|
|
1525
1823
|
await this.runTreeExclusiveOperation(plan.unarchivedTargets.map((target) => ({
|
|
1526
1824
|
sessionId: target.id,
|
|
1527
|
-
...(target.activeSession === undefined
|
|
1825
|
+
...(target.activeSession === undefined
|
|
1826
|
+
? {}
|
|
1827
|
+
: { session: target.activeSession }),
|
|
1528
1828
|
})), `Stop current session activity before archiving ${sessionDisplayName(session)}`, async () => {
|
|
1529
1829
|
for (const target of plan.targets) {
|
|
1530
1830
|
if (target.archived)
|
|
1531
1831
|
this.publishNotificationMutations(this.notificationStore.clearSession(target.id, "archive"));
|
|
1532
1832
|
}
|
|
1533
1833
|
for (const input of archiveInputs)
|
|
1534
|
-
await this.closeActive(input.sessionId, {
|
|
1834
|
+
await this.closeActive(input.sessionId, {
|
|
1835
|
+
kind: "clear",
|
|
1836
|
+
reason: "archive",
|
|
1837
|
+
});
|
|
1535
1838
|
await this.archiveStoreArchiveMany(archiveInputs);
|
|
1536
1839
|
});
|
|
1537
1840
|
await this.forgetUnreadSessions(plan.targets.map((target) => ({ sessionId: target.id, cwd: target.cwd })));
|
|
@@ -1546,7 +1849,10 @@ export class PiSessionService {
|
|
|
1546
1849
|
const archived = await this.getArchived(ref);
|
|
1547
1850
|
if (archived === undefined)
|
|
1548
1851
|
throw new Error("Session not found");
|
|
1549
|
-
await this.closeActive(archived.sessionId, {
|
|
1852
|
+
await this.closeActive(archived.sessionId, {
|
|
1853
|
+
kind: "clear",
|
|
1854
|
+
reason: "restore",
|
|
1855
|
+
});
|
|
1550
1856
|
await this.archiveStore.restore(archived.sessionId);
|
|
1551
1857
|
await this.forgetUnreadSessions([archived]);
|
|
1552
1858
|
}
|
|
@@ -1556,14 +1862,18 @@ export class PiSessionService {
|
|
|
1556
1862
|
throw new Error("Archived session not found");
|
|
1557
1863
|
if (this.archiveStore.deleteArchived === undefined)
|
|
1558
1864
|
throw new Error("Archive store does not support deletion");
|
|
1559
|
-
await this.closeActive(record.sessionId, {
|
|
1865
|
+
await this.closeActive(record.sessionId, {
|
|
1866
|
+
kind: "clear",
|
|
1867
|
+
reason: "delete",
|
|
1868
|
+
});
|
|
1560
1869
|
if (record.archivePath === undefined)
|
|
1561
1870
|
await this.ensureArchivedRecordMoved(record);
|
|
1562
1871
|
await this.archiveStore.deleteArchived(record.sessionId);
|
|
1563
1872
|
await this.forgetUnreadSessions([record]);
|
|
1564
1873
|
}
|
|
1565
1874
|
async deleteArchivedMany(refs) {
|
|
1566
|
-
if (this.archiveStore.deleteArchived === undefined &&
|
|
1875
|
+
if (this.archiveStore.deleteArchived === undefined &&
|
|
1876
|
+
this.archiveStore.deleteArchivedMany === undefined)
|
|
1567
1877
|
throw new Error("Archive store does not support deletion");
|
|
1568
1878
|
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
1569
1879
|
const archivedRecords = await this.archiveStore.list();
|
|
@@ -1572,12 +1882,21 @@ export class PiSessionService {
|
|
|
1572
1882
|
for (const ref of uniqueRefs) {
|
|
1573
1883
|
const record = findArchivedRecordForBulkRef(archivedRecords, ref);
|
|
1574
1884
|
if (record === undefined) {
|
|
1575
|
-
failures.push({
|
|
1885
|
+
failures.push({
|
|
1886
|
+
sessionId: ref.id,
|
|
1887
|
+
error: "Archived session not found",
|
|
1888
|
+
});
|
|
1576
1889
|
continue;
|
|
1577
1890
|
}
|
|
1578
|
-
const active = this.activeForLookup({
|
|
1891
|
+
const active = this.activeForLookup({
|
|
1892
|
+
id: record.sessionId,
|
|
1893
|
+
cwd: record.cwd,
|
|
1894
|
+
});
|
|
1579
1895
|
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1580
|
-
failures.push({
|
|
1896
|
+
failures.push({
|
|
1897
|
+
sessionId: record.sessionId,
|
|
1898
|
+
error: "Stop current session activity before deleting archived session",
|
|
1899
|
+
});
|
|
1581
1900
|
continue;
|
|
1582
1901
|
}
|
|
1583
1902
|
planItems.push({ record });
|
|
@@ -1585,11 +1904,17 @@ export class PiSessionService {
|
|
|
1585
1904
|
const readyRecords = [];
|
|
1586
1905
|
for (const item of planItems) {
|
|
1587
1906
|
try {
|
|
1588
|
-
await this.closeActive(item.record.sessionId, {
|
|
1907
|
+
await this.closeActive(item.record.sessionId, {
|
|
1908
|
+
kind: "clear",
|
|
1909
|
+
reason: "delete",
|
|
1910
|
+
});
|
|
1589
1911
|
readyRecords.push(item.record);
|
|
1590
1912
|
}
|
|
1591
1913
|
catch (error) {
|
|
1592
|
-
failures.push({
|
|
1914
|
+
failures.push({
|
|
1915
|
+
sessionId: item.record.sessionId,
|
|
1916
|
+
error: errorMessage(error),
|
|
1917
|
+
});
|
|
1593
1918
|
}
|
|
1594
1919
|
}
|
|
1595
1920
|
const moveFailures = await this.moveLegacyArchivedRecordsForDelete(readyRecords);
|
|
@@ -1625,11 +1950,19 @@ export class PiSessionService {
|
|
|
1625
1950
|
const { sessionId, cwd } = notificationIdentityForSession(session);
|
|
1626
1951
|
let candidateGeneration;
|
|
1627
1952
|
try {
|
|
1628
|
-
await this.closeActive(sessionId, priorGeneration === undefined
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1953
|
+
await this.closeActive(sessionId, priorGeneration === undefined
|
|
1954
|
+
? CLEAR_RUNTIME_NOTIFICATIONS
|
|
1955
|
+
: DEFER_RUNTIME_NOTIFICATIONS);
|
|
1956
|
+
candidateGeneration =
|
|
1957
|
+
priorGeneration === undefined
|
|
1958
|
+
? undefined
|
|
1959
|
+
: this.notificationStore.beginReplacement(priorGeneration, {
|
|
1960
|
+
sessionId,
|
|
1961
|
+
cwd,
|
|
1962
|
+
});
|
|
1963
|
+
const reopened = await this.getActive(ref, candidateGeneration === undefined
|
|
1964
|
+
? {}
|
|
1965
|
+
: { notificationGeneration: candidateGeneration });
|
|
1633
1966
|
if (candidateGeneration !== undefined) {
|
|
1634
1967
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
1635
1968
|
}
|
|
@@ -1652,7 +1985,9 @@ export class PiSessionService {
|
|
|
1652
1985
|
await clearParentSession(sessionFile);
|
|
1653
1986
|
clearParentSessionHeader(session.sessionManager);
|
|
1654
1987
|
this.unregisterSubsession(session.sessionId);
|
|
1655
|
-
await this.forgetUnreadSessions([
|
|
1988
|
+
await this.forgetUnreadSessions([
|
|
1989
|
+
{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() },
|
|
1990
|
+
]);
|
|
1656
1991
|
}
|
|
1657
1992
|
async pin(ref) {
|
|
1658
1993
|
const session = await this.getOrOpen(ref);
|
|
@@ -1681,7 +2016,9 @@ export class PiSessionService {
|
|
|
1681
2016
|
modified: new Date().toISOString(),
|
|
1682
2017
|
messageCount: session.messages.length,
|
|
1683
2018
|
firstMessage: "",
|
|
1684
|
-
...(session.sessionName === undefined
|
|
2019
|
+
...(session.sessionName === undefined
|
|
2020
|
+
? {}
|
|
2021
|
+
: { name: session.sessionName }),
|
|
1685
2022
|
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
1686
2023
|
...overrides,
|
|
1687
2024
|
};
|
|
@@ -1743,9 +2080,13 @@ export class PiSessionService {
|
|
|
1743
2080
|
}
|
|
1744
2081
|
const [sessionsByCwd, allSessions] = await Promise.all([
|
|
1745
2082
|
this.listSessionsByCwd([...cwdSet]),
|
|
1746
|
-
needsAllSessions
|
|
2083
|
+
needsAllSessions
|
|
2084
|
+
? this.sessionManager.listAll?.() ?? Promise.resolve([])
|
|
2085
|
+
: Promise.resolve(undefined),
|
|
1747
2086
|
]);
|
|
1748
|
-
return allSessions === undefined
|
|
2087
|
+
return allSessions === undefined
|
|
2088
|
+
? { sessionsByCwd }
|
|
2089
|
+
: { sessionsByCwd, allSessions };
|
|
1749
2090
|
}
|
|
1750
2091
|
async listSessionsByCwd(cwds) {
|
|
1751
2092
|
const uniqueCwds = uniqueStrings(cwds);
|
|
@@ -1782,7 +2123,10 @@ export class PiSessionService {
|
|
|
1782
2123
|
sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1783
2124
|
}
|
|
1784
2125
|
catch (error) {
|
|
1785
|
-
return legacyRecords.map((record) => ({
|
|
2126
|
+
return legacyRecords.map((record) => ({
|
|
2127
|
+
sessionId: record.sessionId,
|
|
2128
|
+
error: errorMessage(error),
|
|
2129
|
+
}));
|
|
1786
2130
|
}
|
|
1787
2131
|
const moveInputs = legacyRecords
|
|
1788
2132
|
.map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
|
|
@@ -1798,17 +2142,25 @@ export class PiSessionService {
|
|
|
1798
2142
|
const failedIds = new Set(moveInputs.map((input) => input.sessionId));
|
|
1799
2143
|
return legacyRecords
|
|
1800
2144
|
.filter((record) => failedIds.has(record.sessionId))
|
|
1801
|
-
.map((record) => ({
|
|
2145
|
+
.map((record) => ({
|
|
2146
|
+
sessionId: record.sessionId,
|
|
2147
|
+
error: errorMessage(error),
|
|
2148
|
+
}));
|
|
1802
2149
|
}
|
|
1803
2150
|
}
|
|
1804
2151
|
async cleanupPlan(request) {
|
|
1805
|
-
const [sessions, archivedRecords] = await Promise.all([
|
|
2152
|
+
const [sessions, archivedRecords] = await Promise.all([
|
|
2153
|
+
this.sessionManager.listAll?.() ?? [],
|
|
2154
|
+
this.archiveStore.list(),
|
|
2155
|
+
]);
|
|
1806
2156
|
return planSessionCleanup({
|
|
1807
2157
|
sessions,
|
|
1808
2158
|
archivedRecords,
|
|
1809
2159
|
activeSessions: this.cleanupActiveSessionStatuses(),
|
|
1810
2160
|
thresholds: request.thresholds,
|
|
1811
|
-
...(request.projectCwds === undefined
|
|
2161
|
+
...(request.projectCwds === undefined
|
|
2162
|
+
? {}
|
|
2163
|
+
: { projectCwds: request.projectCwds }),
|
|
1812
2164
|
now: this.now(),
|
|
1813
2165
|
});
|
|
1814
2166
|
}
|
|
@@ -1826,7 +2178,8 @@ export class PiSessionService {
|
|
|
1826
2178
|
const sessionIds = new Set(listedSessionIds);
|
|
1827
2179
|
for (const active of new Set(this.active.values())) {
|
|
1828
2180
|
const session = active.runtime.session;
|
|
1829
|
-
if (session.sessionManager.getCwd() === cwd &&
|
|
2181
|
+
if (session.sessionManager.getCwd() === cwd &&
|
|
2182
|
+
!archivedById.has(session.sessionId))
|
|
1830
2183
|
sessionIds.add(session.sessionId);
|
|
1831
2184
|
}
|
|
1832
2185
|
return [...sessionIds];
|
|
@@ -1845,7 +2198,9 @@ export class PiSessionService {
|
|
|
1845
2198
|
const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
|
|
1846
2199
|
if (session === undefined)
|
|
1847
2200
|
return record;
|
|
1848
|
-
const [moved] = await this.archiveStoreArchiveMany([
|
|
2201
|
+
const [moved] = await this.archiveStoreArchiveMany([
|
|
2202
|
+
archiveInputFromListEntry(session),
|
|
2203
|
+
]);
|
|
1849
2204
|
return moved ?? record;
|
|
1850
2205
|
}
|
|
1851
2206
|
async ensureArchivedRecordsMoved(records) {
|
|
@@ -1854,7 +2209,9 @@ export class PiSessionService {
|
|
|
1854
2209
|
return;
|
|
1855
2210
|
const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1856
2211
|
const moveInputs = legacyRecords
|
|
1857
|
-
.map((record) => sessionsByCwd
|
|
2212
|
+
.map((record) => sessionsByCwd
|
|
2213
|
+
.get(record.cwd)
|
|
2214
|
+
?.find((candidate) => candidate.id === record.sessionId))
|
|
1858
2215
|
.filter(isDefined)
|
|
1859
2216
|
.map(archiveInputFromListEntry);
|
|
1860
2217
|
await this.archiveStoreArchiveMany(moveInputs);
|
|
@@ -1870,7 +2227,10 @@ export class PiSessionService {
|
|
|
1870
2227
|
return archiveInputFromActiveSession(session);
|
|
1871
2228
|
}
|
|
1872
2229
|
async workspaceArchiveCandidates(cwd) {
|
|
1873
|
-
const [sessions, archivedRecords] = await Promise.all([
|
|
2230
|
+
const [sessions, archivedRecords] = await Promise.all([
|
|
2231
|
+
this.sessionManager.list(cwd),
|
|
2232
|
+
this.archiveStore.list(),
|
|
2233
|
+
]);
|
|
1874
2234
|
const candidates = new Map();
|
|
1875
2235
|
const archivedById = new Map();
|
|
1876
2236
|
for (const record of archivedRecords) {
|
|
@@ -1896,15 +2256,22 @@ export class PiSessionService {
|
|
|
1896
2256
|
}
|
|
1897
2257
|
for (const active of new Set(this.active.values())) {
|
|
1898
2258
|
const session = active.runtime.session;
|
|
1899
|
-
if (session.sessionManager.getCwd() !== cwd ||
|
|
2259
|
+
if (session.sessionManager.getCwd() !== cwd ||
|
|
2260
|
+
archivedById.has(session.sessionId))
|
|
1900
2261
|
continue;
|
|
1901
2262
|
const existing = candidates.get(session.sessionId);
|
|
1902
|
-
candidates.set(session.sessionId, {
|
|
2263
|
+
candidates.set(session.sessionId, {
|
|
2264
|
+
...(existing ?? archiveCandidateFromActiveSession(session, false)),
|
|
2265
|
+
activeSession: session,
|
|
2266
|
+
});
|
|
1903
2267
|
}
|
|
1904
2268
|
return [...candidates.values()];
|
|
1905
2269
|
}
|
|
1906
2270
|
async listSessionNames(cwd) {
|
|
1907
|
-
const [sessions, archivedRecords] = await Promise.all([
|
|
2271
|
+
const [sessions, archivedRecords] = await Promise.all([
|
|
2272
|
+
this.sessionManager.list(cwd),
|
|
2273
|
+
this.archiveStore.list(),
|
|
2274
|
+
]);
|
|
1908
2275
|
const names = new Set();
|
|
1909
2276
|
for (const session of sessions)
|
|
1910
2277
|
addSessionName(names, session.name);
|
|
@@ -1925,7 +2292,9 @@ export class PiSessionService {
|
|
|
1925
2292
|
await Promise.allSettled(pendingOpens);
|
|
1926
2293
|
const active = this.active.get(sessionId);
|
|
1927
2294
|
if (notificationPolicy.kind === "clear") {
|
|
1928
|
-
const generation = active === undefined
|
|
2295
|
+
const generation = active === undefined
|
|
2296
|
+
? undefined
|
|
2297
|
+
: this.notificationGenerationBySession.get(active.runtime.session);
|
|
1929
2298
|
const mutations = generation === undefined
|
|
1930
2299
|
? this.notificationStore.clearSession(sessionId, notificationPolicy.reason)
|
|
1931
2300
|
: this.notificationStore.clearGeneration(generation, notificationPolicy.reason);
|
|
@@ -1977,7 +2346,7 @@ export class PiSessionService {
|
|
|
1977
2346
|
throw branchSummaryAbortError;
|
|
1978
2347
|
}
|
|
1979
2348
|
async assertWritable(ref) {
|
|
1980
|
-
if (await this.getArchived(ref) !== undefined)
|
|
2349
|
+
if ((await this.getArchived(ref)) !== undefined)
|
|
1981
2350
|
throw new Error("Archived sessions are read-only. Restore the session to continue.");
|
|
1982
2351
|
}
|
|
1983
2352
|
async getOrOpen(ref) {
|
|
@@ -1996,7 +2365,7 @@ export class PiSessionService {
|
|
|
1996
2365
|
return archived.archivePath;
|
|
1997
2366
|
const match = isPiSessionRef(ref)
|
|
1998
2367
|
? (await this.sessionManager.list(ref.cwd)).find((session) => session.id === ref.id || session.id.startsWith(ref.id))
|
|
1999
|
-
: (await this.sessionManager.listAll?.() ?? []).find((session) => session.id === ref || session.id.startsWith(ref));
|
|
2368
|
+
: ((await this.sessionManager.listAll?.()) ?? []).find((session) => session.id === ref || session.id.startsWith(ref));
|
|
2000
2369
|
if (match === undefined || match.path === "")
|
|
2001
2370
|
throw new Error("Session not found");
|
|
2002
2371
|
return match.path;
|
|
@@ -2012,7 +2381,7 @@ export class PiSessionService {
|
|
|
2012
2381
|
}
|
|
2013
2382
|
const match = isPiSessionRef(ref)
|
|
2014
2383
|
? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
|
|
2015
|
-
: (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
2384
|
+
: ((await this.sessionManager.listAll?.()) ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
|
|
2016
2385
|
if (!match)
|
|
2017
2386
|
throw new Error("Session not found");
|
|
2018
2387
|
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
@@ -2058,23 +2427,31 @@ export class PiSessionService {
|
|
|
2058
2427
|
if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
|
|
2059
2428
|
return exact;
|
|
2060
2429
|
for (const [candidateId, active] of this.active.entries()) {
|
|
2061
|
-
if (candidateId.startsWith(sessionId) &&
|
|
2430
|
+
if (candidateId.startsWith(sessionId) &&
|
|
2431
|
+
lookupMatchesActiveSession(ref, active))
|
|
2062
2432
|
return active;
|
|
2063
2433
|
}
|
|
2064
2434
|
return undefined;
|
|
2065
2435
|
}
|
|
2066
2436
|
async create(sessionManager, cwd, options = {}) {
|
|
2067
|
-
const delegationToolsEnabled = options.creationProvenance !== "tracked-subsession"
|
|
2068
|
-
|
|
2437
|
+
const delegationToolsEnabled = options.creationProvenance !== "tracked-subsession" &&
|
|
2438
|
+
(await sessionAllowsDelegationTools(sessionManager, this.sessionManager));
|
|
2069
2439
|
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
2070
2440
|
cwd,
|
|
2071
2441
|
agentDir: this.agentDir,
|
|
2072
2442
|
sessionManager,
|
|
2073
2443
|
delegationToolsEnabled,
|
|
2074
|
-
...(options.initialModel === undefined
|
|
2075
|
-
|
|
2444
|
+
...(options.initialModel === undefined
|
|
2445
|
+
? {}
|
|
2446
|
+
: { initialModel: options.initialModel }),
|
|
2447
|
+
...(options.initialThinkingLevel === undefined
|
|
2448
|
+
? {}
|
|
2449
|
+
: { initialThinkingLevel: options.initialThinkingLevel }),
|
|
2076
2450
|
});
|
|
2077
|
-
const active = {
|
|
2451
|
+
const active = {
|
|
2452
|
+
runtime,
|
|
2453
|
+
unsubscribe: noop,
|
|
2454
|
+
};
|
|
2078
2455
|
let boundSession = runtime.session;
|
|
2079
2456
|
let notificationGeneration = options.notificationGeneration;
|
|
2080
2457
|
let notificationOwnership = options.notifications === "disabled"
|
|
@@ -2139,7 +2516,8 @@ export class PiSessionService {
|
|
|
2139
2516
|
if (options.initializeModelPolicy !== undefined) {
|
|
2140
2517
|
await this.initializeSessionModelPolicy(runtime.session, options.initializeModelPolicy);
|
|
2141
2518
|
}
|
|
2142
|
-
if (notificationOwnership === "replacement" &&
|
|
2519
|
+
if (notificationOwnership === "replacement" &&
|
|
2520
|
+
notificationGeneration !== undefined) {
|
|
2143
2521
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
|
|
2144
2522
|
notificationOwnership = "external";
|
|
2145
2523
|
}
|
|
@@ -2187,7 +2565,10 @@ export class PiSessionService {
|
|
|
2187
2565
|
onError: (error) => {
|
|
2188
2566
|
const message = `${error.extensionPath}: ${error.error}`;
|
|
2189
2567
|
this.publishActivity(session, "extension error", "error", message);
|
|
2190
|
-
this.events.publish(session.sessionId, {
|
|
2568
|
+
this.events.publish(session.sessionId, {
|
|
2569
|
+
type: "session.error",
|
|
2570
|
+
message,
|
|
2571
|
+
});
|
|
2191
2572
|
},
|
|
2192
2573
|
});
|
|
2193
2574
|
}
|
|
@@ -2240,7 +2621,8 @@ export class PiSessionService {
|
|
|
2240
2621
|
const previousCwd = canonicalizeStoredCwd(previous.sessionManager.getCwd());
|
|
2241
2622
|
this.unreadStore.forgetActivity(previous.sessionId, previousCwd);
|
|
2242
2623
|
const nextCwd = canonicalizeStoredCwd(next.sessionManager.getCwd());
|
|
2243
|
-
if (previous.sessionId === next.sessionId &&
|
|
2624
|
+
if (previous.sessionId === next.sessionId &&
|
|
2625
|
+
cwdPathsEqual(previousCwd, nextCwd))
|
|
2244
2626
|
return;
|
|
2245
2627
|
await this.publishUnreadMutations(this.unreadStore.forgetSession(previous.sessionId, previousCwd));
|
|
2246
2628
|
}
|
|
@@ -2263,11 +2645,14 @@ export class PiSessionService {
|
|
|
2263
2645
|
publishUnreadMutations(mutations) {
|
|
2264
2646
|
this.enqueueUnreadMutations(mutations);
|
|
2265
2647
|
this.unreadPublicationFlushRequested = true;
|
|
2266
|
-
if (this.unreadPublication === undefined &&
|
|
2648
|
+
if (this.unreadPublication === undefined &&
|
|
2649
|
+
this.unreadPublicationRetryTimer !== undefined) {
|
|
2267
2650
|
const failure = this.unreadPublicationFailure;
|
|
2268
2651
|
return Promise.reject(failure instanceof Error
|
|
2269
2652
|
? failure
|
|
2270
|
-
: new Error("Session unread publication is awaiting retry", {
|
|
2653
|
+
: new Error("Session unread publication is awaiting retry", {
|
|
2654
|
+
cause: failure,
|
|
2655
|
+
}));
|
|
2271
2656
|
}
|
|
2272
2657
|
return this.ensureUnreadPublication();
|
|
2273
2658
|
}
|
|
@@ -2290,7 +2675,8 @@ export class PiSessionService {
|
|
|
2290
2675
|
return publication;
|
|
2291
2676
|
}
|
|
2292
2677
|
async drainUnreadPublication() {
|
|
2293
|
-
while (this.unreadPublicationFlushRequested ||
|
|
2678
|
+
while (this.unreadPublicationFlushRequested ||
|
|
2679
|
+
this.pendingUnreadMutations.length > 0) {
|
|
2294
2680
|
this.unreadPublicationFlushRequested = false;
|
|
2295
2681
|
const batch = this.pendingUnreadMutations.splice(0);
|
|
2296
2682
|
let publishedCount = 0;
|
|
@@ -2324,7 +2710,8 @@ export class PiSessionService {
|
|
|
2324
2710
|
this.pendingUnreadMutations.splice(0, excess);
|
|
2325
2711
|
}
|
|
2326
2712
|
scheduleUnreadPublicationRetry() {
|
|
2327
|
-
if (this.unreadPublicationStopped ||
|
|
2713
|
+
if (this.unreadPublicationStopped ||
|
|
2714
|
+
this.unreadPublicationRetryTimer !== undefined)
|
|
2328
2715
|
return;
|
|
2329
2716
|
const delay = this.unreadPublicationRetryDelayMs;
|
|
2330
2717
|
this.unreadPublicationRetryDelayMs = Math.min(Math.max(delay * 2, this.unreadPublicationRetryInitialMs), Math.max(MAX_UNREAD_PUBLICATION_RETRY_MS, this.unreadPublicationRetryInitialMs));
|
|
@@ -2393,8 +2780,11 @@ export class PiSessionService {
|
|
|
2393
2780
|
recordGenerationProgress(session, message) {
|
|
2394
2781
|
const current = this.generationMetrics.get(session);
|
|
2395
2782
|
const providerTokens = providerOutputTokens(message);
|
|
2396
|
-
const estimatedTokens = providerTokens === undefined && current?.estimated !== false
|
|
2397
|
-
|
|
2783
|
+
const estimatedTokens = providerTokens === undefined && current?.estimated !== false
|
|
2784
|
+
? estimatedOutputTokens(message)
|
|
2785
|
+
: undefined;
|
|
2786
|
+
const outputTokens = providerTokens ??
|
|
2787
|
+
(current?.estimated === false ? current.outputTokens : estimatedTokens);
|
|
2398
2788
|
if (outputTokens === undefined)
|
|
2399
2789
|
return;
|
|
2400
2790
|
this.generationMetrics.set(session, {
|
|
@@ -2408,15 +2798,20 @@ export class PiSessionService {
|
|
|
2408
2798
|
if (metrics === undefined)
|
|
2409
2799
|
return undefined;
|
|
2410
2800
|
const elapsedMs = this.now().getTime() - metrics.startedAt;
|
|
2411
|
-
const tokensPerSecond = elapsedMs >= MIN_GENERATION_RATE_ELAPSED_MS
|
|
2801
|
+
const tokensPerSecond = elapsedMs >= MIN_GENERATION_RATE_ELAPSED_MS
|
|
2802
|
+
? (metrics.outputTokens * 1_000) / elapsedMs
|
|
2803
|
+
: undefined;
|
|
2412
2804
|
return {
|
|
2413
2805
|
outputTokens: metrics.outputTokens,
|
|
2414
|
-
...(tokensPerSecond === undefined || !Number.isFinite(tokensPerSecond)
|
|
2806
|
+
...(tokensPerSecond === undefined || !Number.isFinite(tokensPerSecond)
|
|
2807
|
+
? {}
|
|
2808
|
+
: { tokensPerSecond }),
|
|
2415
2809
|
...(metrics.estimated ? { estimated: true } : {}),
|
|
2416
2810
|
};
|
|
2417
2811
|
}
|
|
2418
2812
|
scheduleCompactionQueueDrain(sessionId, delayMs = 0) {
|
|
2419
|
-
if (!this.compactionPromptQueues.has(sessionId) ||
|
|
2813
|
+
if (!this.compactionPromptQueues.has(sessionId) ||
|
|
2814
|
+
this.compactionDrainTimers.has(sessionId))
|
|
2420
2815
|
return;
|
|
2421
2816
|
const timer = setTimeout(() => {
|
|
2422
2817
|
this.compactionDrainTimers.delete(sessionId);
|
|
@@ -2447,7 +2842,9 @@ export class PiSessionService {
|
|
|
2447
2842
|
return;
|
|
2448
2843
|
this.publishStatus(session);
|
|
2449
2844
|
const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images, prompt.echoUserMessage ?? true);
|
|
2450
|
-
void submitted.finally(() => {
|
|
2845
|
+
void submitted.finally(() => {
|
|
2846
|
+
this.scheduleCompactionQueueDrain(sessionId);
|
|
2847
|
+
});
|
|
2451
2848
|
}
|
|
2452
2849
|
takeCompactionPromptQueue(sessionId) {
|
|
2453
2850
|
const queued = this.compactionPromptQueues.get(sessionId) ?? [];
|
|
@@ -2475,7 +2872,10 @@ export class PiSessionService {
|
|
|
2475
2872
|
this.compactionDrainTimers.clear();
|
|
2476
2873
|
}
|
|
2477
2874
|
maybeGenerateSessionName(session, firstMessage) {
|
|
2478
|
-
if (session.sessionName !== undefined ||
|
|
2875
|
+
if (session.sessionName !== undefined ||
|
|
2876
|
+
session.messages.length !== 0 ||
|
|
2877
|
+
session.isStreaming ||
|
|
2878
|
+
session.isCompacting)
|
|
2479
2879
|
return;
|
|
2480
2880
|
const deterministicName = deterministicSessionName(firstMessage);
|
|
2481
2881
|
if (deterministicName !== undefined) {
|
|
@@ -2485,9 +2885,11 @@ export class PiSessionService {
|
|
|
2485
2885
|
const model = session.model;
|
|
2486
2886
|
if (model === undefined)
|
|
2487
2887
|
return;
|
|
2488
|
-
void generateShortSessionName(session.agent.streamFunction, model, firstMessage)
|
|
2888
|
+
void generateShortSessionName(session.agent.streamFunction, model, firstMessage)
|
|
2889
|
+
.then((name) => {
|
|
2489
2890
|
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
2490
|
-
})
|
|
2891
|
+
})
|
|
2892
|
+
.catch(() => {
|
|
2491
2893
|
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
|
|
2492
2894
|
});
|
|
2493
2895
|
}
|
|
@@ -2509,7 +2911,10 @@ export class PiSessionService {
|
|
|
2509
2911
|
this.applyGeneratedSessionName(session, generatedName);
|
|
2510
2912
|
}
|
|
2511
2913
|
catch (error) {
|
|
2512
|
-
this.logger.info({
|
|
2914
|
+
this.logger.info({
|
|
2915
|
+
sessionId: session.sessionId,
|
|
2916
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2917
|
+
}, "failed to apply deferred session name");
|
|
2513
2918
|
}
|
|
2514
2919
|
}
|
|
2515
2920
|
const notifications = this.deferredSubsessionNotifications.get(session) ?? [];
|
|
@@ -2544,7 +2949,9 @@ export class PiSessionService {
|
|
|
2544
2949
|
this.authLossWarnings.delete(warningKey);
|
|
2545
2950
|
return;
|
|
2546
2951
|
}
|
|
2547
|
-
if (removedProviderId === undefined ||
|
|
2952
|
+
if (removedProviderId === undefined ||
|
|
2953
|
+
model.provider !== removedProviderId ||
|
|
2954
|
+
this.authLossWarnings.has(warningKey))
|
|
2548
2955
|
return;
|
|
2549
2956
|
this.authLossWarnings.add(warningKey);
|
|
2550
2957
|
this.events.publish(session.sessionId, {
|
|
@@ -2563,7 +2970,11 @@ export class PiSessionService {
|
|
|
2563
2970
|
publishSessionName(session) {
|
|
2564
2971
|
const event = session.sessionName === undefined
|
|
2565
2972
|
? { type: "session.name", sessionId: session.sessionId }
|
|
2566
|
-
: {
|
|
2973
|
+
: {
|
|
2974
|
+
type: "session.name",
|
|
2975
|
+
sessionId: session.sessionId,
|
|
2976
|
+
name: session.sessionName,
|
|
2977
|
+
};
|
|
2567
2978
|
this.events.publish(session.sessionId, event);
|
|
2568
2979
|
this.events.publishGlobal(event);
|
|
2569
2980
|
}
|
|
@@ -2603,17 +3014,20 @@ export class PiSessionService {
|
|
|
2603
3014
|
return "active";
|
|
2604
3015
|
}
|
|
2605
3016
|
hasActiveWork(session) {
|
|
2606
|
-
return this.treeNavigations.has(session)
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
3017
|
+
return (this.treeNavigations.has(session) ||
|
|
3018
|
+
this.isSessionEntryMutationActive(session) ||
|
|
3019
|
+
this.isTreeExclusiveOperationActive(session) ||
|
|
3020
|
+
sessionHasActiveWork(session, this.compactionQueuedMessages(session.sessionId).length));
|
|
2610
3021
|
}
|
|
2611
3022
|
async runTreeExclusiveOperation(targets, activeError, operation) {
|
|
2612
3023
|
const sessionIds = new Set();
|
|
2613
3024
|
const runtimes = new Set();
|
|
2614
3025
|
const sessions = new Set();
|
|
2615
3026
|
for (const target of targets) {
|
|
2616
|
-
const runtime = target.runtime ??
|
|
3027
|
+
const runtime = target.runtime ??
|
|
3028
|
+
(target.session === undefined
|
|
3029
|
+
? undefined
|
|
3030
|
+
: this.activeRuntimeForSession(target.session));
|
|
2617
3031
|
const session = target.session ?? runtime?.session;
|
|
2618
3032
|
if (session !== undefined && this.hasActiveWork(session))
|
|
2619
3033
|
throw new Error(activeError);
|
|
@@ -2652,7 +3066,8 @@ export class PiSessionService {
|
|
|
2652
3066
|
if (this.isTreeExclusiveSessionIdentityActive(session.sessionId))
|
|
2653
3067
|
return true;
|
|
2654
3068
|
const runtime = this.activeRuntimeForSession(session);
|
|
2655
|
-
return runtime !== undefined &&
|
|
3069
|
+
return (runtime !== undefined &&
|
|
3070
|
+
(this.treeExclusiveRuntimeOperationCounts.get(runtime) ?? 0) > 0);
|
|
2656
3071
|
}
|
|
2657
3072
|
activeRuntimeForSession(session) {
|
|
2658
3073
|
for (const active of new Set(this.active.values())) {
|
|
@@ -2744,11 +3159,18 @@ export class PiSessionService {
|
|
|
2744
3159
|
}
|
|
2745
3160
|
publishActivity(session, label, phase, detail) {
|
|
2746
3161
|
const at = new Date().toISOString();
|
|
2747
|
-
const stored = detail === undefined
|
|
3162
|
+
const stored = detail === undefined
|
|
3163
|
+
? { phase, label, at }
|
|
3164
|
+
: { phase, label, detail, at };
|
|
2748
3165
|
this.activities.set(session.sessionId, stored);
|
|
2749
|
-
const activity = detail === undefined
|
|
3166
|
+
const activity = detail === undefined
|
|
3167
|
+
? { sessionId: session.sessionId, phase, label, at }
|
|
3168
|
+
: { sessionId: session.sessionId, phase, label, detail, at };
|
|
2750
3169
|
this.workspaceActivity?.applySessionActivity(session.sessionManager.getCwd(), activity);
|
|
2751
|
-
this.events.publish(session.sessionId, {
|
|
3170
|
+
this.events.publish(session.sessionId, {
|
|
3171
|
+
type: "activity.update",
|
|
3172
|
+
activity,
|
|
3173
|
+
});
|
|
2752
3174
|
this.events.publishGlobal({ type: "activity.update", activity });
|
|
2753
3175
|
this.observeUnreadActivityState(session);
|
|
2754
3176
|
}
|
|
@@ -2768,19 +3190,49 @@ export class PiSessionService {
|
|
|
2768
3190
|
const stored = { phase: "idle", label: "idle", at };
|
|
2769
3191
|
this.activities.set(session.sessionId, stored);
|
|
2770
3192
|
const activity = { sessionId: session.sessionId, ...stored };
|
|
2771
|
-
this.events.publish(session.sessionId, {
|
|
3193
|
+
this.events.publish(session.sessionId, {
|
|
3194
|
+
type: "activity.update",
|
|
3195
|
+
activity,
|
|
3196
|
+
});
|
|
2772
3197
|
this.events.publishGlobal({ type: "activity.update", activity });
|
|
2773
3198
|
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Read-only capability view for `get_model_policy`, built from the session's
|
|
3201
|
+
* own cached inspection so it cannot disagree with what the HTTP route reports.
|
|
3202
|
+
*/
|
|
3203
|
+
modelPolicyCapability(sessionId) {
|
|
3204
|
+
// The tool only ever asks about its own session, which is live by definition
|
|
3205
|
+
// while a tool call is executing. A miss means the runtime was torn down
|
|
3206
|
+
// mid-call, which is a terminal read failure rather than a policy answer.
|
|
3207
|
+
const session = this.active.get(sessionId)?.runtime.session;
|
|
3208
|
+
if (session === undefined)
|
|
3209
|
+
throw new Error(`Session ${sessionId} is not active`);
|
|
3210
|
+
const inspection = this.inspectAndCacheSessionModelPolicy(session);
|
|
3211
|
+
return buildModelPolicyCapability({
|
|
3212
|
+
policy: inspection.kind === "invalid" ? undefined : inspection.policy,
|
|
3213
|
+
currentRuntime: this.exactSelectionFromSession(session),
|
|
3214
|
+
ladder: this.modelTierRegistry.validate(),
|
|
3215
|
+
resolveTier: (tier) => {
|
|
3216
|
+
const resolved = this.modelTierRegistry.resolve(tier);
|
|
3217
|
+
return {
|
|
3218
|
+
model: { provider: resolved.model.provider, id: resolved.model.id },
|
|
3219
|
+
thinkingLevel: resolved.thinkingLevel,
|
|
3220
|
+
};
|
|
3221
|
+
},
|
|
3222
|
+
blockedReason: this.modelPolicyBlockedReason(session),
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
2774
3225
|
exactSelectionFromSession(session) {
|
|
2775
3226
|
const model = session.model;
|
|
2776
|
-
if (model === undefined
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
3227
|
+
if (model === undefined ||
|
|
3228
|
+
typeof model.provider !== "string" ||
|
|
3229
|
+
model.provider.trim() === "" ||
|
|
3230
|
+
typeof model.id !== "string" ||
|
|
3231
|
+
model.id.trim() === "") {
|
|
2781
3232
|
throw new Error("Session model policy requires a resolved runtime model provider and id");
|
|
2782
3233
|
}
|
|
2783
|
-
if (typeof session.thinkingLevel !== "string" ||
|
|
3234
|
+
if (typeof session.thinkingLevel !== "string" ||
|
|
3235
|
+
session.thinkingLevel.trim() === "") {
|
|
2784
3236
|
throw new Error("Session model policy requires a resolved runtime thinking level");
|
|
2785
3237
|
}
|
|
2786
3238
|
return {
|
|
@@ -2789,7 +3241,8 @@ export class PiSessionService {
|
|
|
2789
3241
|
};
|
|
2790
3242
|
}
|
|
2791
3243
|
policyEntries(session) {
|
|
2792
|
-
return session.sessionManager.getEntries?.() ??
|
|
3244
|
+
return (session.sessionManager.getEntries?.() ??
|
|
3245
|
+
session.sessionManager.getBranch());
|
|
2793
3246
|
}
|
|
2794
3247
|
inspectAndCacheSessionModelPolicy(session) {
|
|
2795
3248
|
const inspection = inspectSessionModelPolicy(this.policyEntries(session), this.exactSelectionFromSession(session));
|
|
@@ -2804,7 +3257,8 @@ export class PiSessionService {
|
|
|
2804
3257
|
}
|
|
2805
3258
|
/** Effective block: an unproven runtime restoration outranks an entry defect. */
|
|
2806
3259
|
modelPolicyBlockedReason(session) {
|
|
2807
|
-
return this.modelPolicyRuntimeBlocks.get(session.sessionId) ??
|
|
3260
|
+
return (this.modelPolicyRuntimeBlocks.get(session.sessionId) ??
|
|
3261
|
+
this.modelPolicyEntryReasons.get(session));
|
|
2808
3262
|
}
|
|
2809
3263
|
/**
|
|
2810
3264
|
* Root-session policy initialization. `true` records the runtime's own tuple;
|
|
@@ -2812,7 +3266,10 @@ export class PiSessionService {
|
|
|
2812
3266
|
* of `start()` sees the session, so no prompt can precede the policy.
|
|
2813
3267
|
*/
|
|
2814
3268
|
async initializeSessionModelPolicy(session, initializer) {
|
|
2815
|
-
const current = {
|
|
3269
|
+
const current = {
|
|
3270
|
+
mode: "exact",
|
|
3271
|
+
exact: this.exactSelectionFromSession(session),
|
|
3272
|
+
};
|
|
2816
3273
|
if (initializer === true) {
|
|
2817
3274
|
this.appendSessionModelPolicy(session, current);
|
|
2818
3275
|
this.inspectAndCacheSessionModelPolicy(session);
|
|
@@ -2820,7 +3277,9 @@ export class PiSessionService {
|
|
|
2820
3277
|
}
|
|
2821
3278
|
// Root creation reports failure through its own abort/dispose cleanup rather
|
|
2822
3279
|
// than publishing a failed transition for a session no client has seen.
|
|
2823
|
-
await this.applySessionModelPolicy(session, current, initializer, {
|
|
3280
|
+
await this.applySessionModelPolicy(session, current, initializer, {
|
|
3281
|
+
action: "initialize the session model policy",
|
|
3282
|
+
});
|
|
2824
3283
|
}
|
|
2825
3284
|
/**
|
|
2826
3285
|
* The single policy transition path. Validates the complete target before any
|
|
@@ -2831,7 +3290,10 @@ export class PiSessionService {
|
|
|
2831
3290
|
async applySessionModelPolicy(session, current, update, options) {
|
|
2832
3291
|
const plan = planSessionModelPolicyUpdate(current, update, (tier) => {
|
|
2833
3292
|
const resolved = this.modelTierRegistry.resolve(tier);
|
|
2834
|
-
return {
|
|
3293
|
+
return {
|
|
3294
|
+
model: { provider: resolved.model.provider, id: resolved.model.id },
|
|
3295
|
+
thinkingLevel: resolved.thinkingLevel,
|
|
3296
|
+
};
|
|
2835
3297
|
});
|
|
2836
3298
|
// Validation happens before the mutation opens, so a rejected target leaves
|
|
2837
3299
|
// the runtime and the persisted policy untouched.
|
|
@@ -2849,7 +3311,7 @@ export class PiSessionService {
|
|
|
2849
3311
|
}
|
|
2850
3312
|
catch (error) {
|
|
2851
3313
|
const reason = error instanceof Error ? error.message : String(error);
|
|
2852
|
-
if (!await this.restoreExactSelection(session, previous)) {
|
|
3314
|
+
if (!(await this.restoreExactSelection(session, previous))) {
|
|
2853
3315
|
this.modelPolicyRuntimeBlocks.set(session.sessionId, `MODEL_POLICY_BLOCKED: ${reason}; the previous model and thinking pair could not be restored`);
|
|
2854
3316
|
}
|
|
2855
3317
|
this.inspectAndCacheSessionModelPolicy(session);
|
|
@@ -2956,14 +3418,16 @@ export class PiSessionService {
|
|
|
2956
3418
|
const policy = {
|
|
2957
3419
|
mode: "exact",
|
|
2958
3420
|
exact: this.exactSelectionFromSession(session),
|
|
2959
|
-
...(currentPolicy.tier === undefined
|
|
3421
|
+
...(currentPolicy.tier === undefined
|
|
3422
|
+
? {}
|
|
3423
|
+
: { tier: currentPolicy.tier }),
|
|
2960
3424
|
};
|
|
2961
3425
|
try {
|
|
2962
3426
|
this.appendSessionModelPolicy(session, policy);
|
|
2963
3427
|
}
|
|
2964
3428
|
catch (error) {
|
|
2965
3429
|
const reason = error instanceof Error ? error.message : String(error);
|
|
2966
|
-
if (!await this.restoreExactSelection(session, previous)) {
|
|
3430
|
+
if (!(await this.restoreExactSelection(session, previous))) {
|
|
2967
3431
|
this.modelPolicyRuntimeBlocks.set(session.sessionId, `MODEL_POLICY_BLOCKED: ${reason}; the previous model and thinking pair could not be restored`);
|
|
2968
3432
|
}
|
|
2969
3433
|
this.inspectAndCacheSessionModelPolicy(session);
|
|
@@ -3009,7 +3473,8 @@ export class PiSessionService {
|
|
|
3009
3473
|
const candidates = session.scopedModels.length > 0
|
|
3010
3474
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
3011
3475
|
: session.modelRuntime.getAvailableSnapshot();
|
|
3012
|
-
const model = candidates.find((candidate) => candidate.provider === selection.model.provider &&
|
|
3476
|
+
const model = candidates.find((candidate) => candidate.provider === selection.model.provider &&
|
|
3477
|
+
candidate.id === selection.model.id);
|
|
3013
3478
|
const described = `${selection.model.provider}/${selection.model.id}`;
|
|
3014
3479
|
if (model === undefined)
|
|
3015
3480
|
throw new Error(`Model not found: ${described}`);
|
|
@@ -3019,7 +3484,13 @@ export class PiSessionService {
|
|
|
3019
3484
|
if (!runtimeThinkingLevels(model).includes(selection.thinkingLevel)) {
|
|
3020
3485
|
throw new Error(`Thinking level ${selection.thinkingLevel} is unsupported by ${described}`);
|
|
3021
3486
|
}
|
|
3022
|
-
return {
|
|
3487
|
+
return {
|
|
3488
|
+
model,
|
|
3489
|
+
selection: {
|
|
3490
|
+
model: { provider: model.provider, id: model.id },
|
|
3491
|
+
thinkingLevel: selection.thinkingLevel,
|
|
3492
|
+
},
|
|
3493
|
+
};
|
|
3023
3494
|
}
|
|
3024
3495
|
/**
|
|
3025
3496
|
* Model first, then thinking: `setThinkingLevel` clamps against the currently
|
|
@@ -3034,11 +3505,11 @@ export class PiSessionService {
|
|
|
3034
3505
|
await session.setModel(target.model);
|
|
3035
3506
|
session.setThinkingLevel(level);
|
|
3036
3507
|
const effective = this.exactSelectionFromSession(session);
|
|
3037
|
-
if (effective.model.provider !== target.selection.model.provider
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
throw new Error(`Session model policy could not activate ${target.selection.model.provider}/${target.selection.model.id} at thinking level ${level}`
|
|
3041
|
-
|
|
3508
|
+
if (effective.model.provider !== target.selection.model.provider ||
|
|
3509
|
+
effective.model.id !== target.selection.model.id ||
|
|
3510
|
+
effective.thinkingLevel !== level) {
|
|
3511
|
+
throw new Error(`Session model policy could not activate ${target.selection.model.provider}/${target.selection.model.id} at thinking level ${level}` +
|
|
3512
|
+
`; the runtime reports ${effective.model.provider}/${effective.model.id} at thinking level ${effective.thinkingLevel}`);
|
|
3042
3513
|
}
|
|
3043
3514
|
}
|
|
3044
3515
|
/**
|
|
@@ -3049,9 +3520,9 @@ export class PiSessionService {
|
|
|
3049
3520
|
async restoreExactSelection(session, previous) {
|
|
3050
3521
|
try {
|
|
3051
3522
|
const current = this.exactSelectionFromSession(session);
|
|
3052
|
-
if (current.model.provider === previous.model.provider
|
|
3053
|
-
|
|
3054
|
-
|
|
3523
|
+
if (current.model.provider === previous.model.provider &&
|
|
3524
|
+
current.model.id === previous.model.id &&
|
|
3525
|
+
current.thinkingLevel === previous.thinkingLevel) {
|
|
3055
3526
|
return true;
|
|
3056
3527
|
}
|
|
3057
3528
|
const target = await this.resolveAvailableExactSelection(session, previous);
|
|
@@ -3093,7 +3564,9 @@ export class PiSessionService {
|
|
|
3093
3564
|
}
|
|
3094
3565
|
return {
|
|
3095
3566
|
mode: inspection.policy.mode,
|
|
3096
|
-
...(inspection.policy.tier === undefined
|
|
3567
|
+
...(inspection.policy.tier === undefined
|
|
3568
|
+
? {}
|
|
3569
|
+
: { tier: inspection.policy.tier }),
|
|
3097
3570
|
resolved,
|
|
3098
3571
|
ladderValid: ladderValidation.valid,
|
|
3099
3572
|
...(blockedReason === undefined ? {} : { blockedReason }),
|
|
@@ -3101,7 +3574,9 @@ export class PiSessionService {
|
|
|
3101
3574
|
}
|
|
3102
3575
|
statusFromSession(session) {
|
|
3103
3576
|
const stats = session.getSessionStats();
|
|
3104
|
-
const model = session.model === undefined
|
|
3577
|
+
const model = session.model === undefined
|
|
3578
|
+
? undefined
|
|
3579
|
+
: modelToClientModel(session.model);
|
|
3105
3580
|
const generation = this.generationForStatus(session);
|
|
3106
3581
|
const contextUsage = session.getContextUsage();
|
|
3107
3582
|
const warnings = this.warningsForSession(session);
|
|
@@ -3139,7 +3614,8 @@ export class PiSessionService {
|
|
|
3139
3614
|
return warnings;
|
|
3140
3615
|
}
|
|
3141
3616
|
pendingMessageCount(session) {
|
|
3142
|
-
return session.pendingMessageCount +
|
|
3617
|
+
return (session.pendingMessageCount +
|
|
3618
|
+
this.compactionQueuedMessages(session.sessionId).length);
|
|
3143
3619
|
}
|
|
3144
3620
|
compactionQueuedMessages(sessionId) {
|
|
3145
3621
|
return this.compactionPromptQueues.get(sessionId) ?? [];
|
|
@@ -3154,7 +3630,9 @@ function previewResponseFromPlan(plan) {
|
|
|
3154
3630
|
thresholds: plan.thresholds,
|
|
3155
3631
|
projects: plan.projects,
|
|
3156
3632
|
totals: plan.totals,
|
|
3157
|
-
...(plan.skippedBusySessionIds.length === 0
|
|
3633
|
+
...(plan.skippedBusySessionIds.length === 0
|
|
3634
|
+
? {}
|
|
3635
|
+
: { skippedBusySessionIds: plan.skippedBusySessionIds }),
|
|
3158
3636
|
};
|
|
3159
3637
|
}
|
|
3160
3638
|
function uniqueBulkSessionRefs(refs) {
|
|
@@ -3173,15 +3651,19 @@ function bulkRefToLookup(ref) {
|
|
|
3173
3651
|
return ref.cwd === undefined ? ref.id : { id: ref.id, cwd: ref.cwd };
|
|
3174
3652
|
}
|
|
3175
3653
|
function findArchivedRecordForBulkRef(records, ref) {
|
|
3176
|
-
return records.find((record) => (ref.cwd === undefined || record.cwd === ref.cwd) &&
|
|
3654
|
+
return records.find((record) => (ref.cwd === undefined || record.cwd === ref.cwd) &&
|
|
3655
|
+
(record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
|
|
3177
3656
|
}
|
|
3178
3657
|
function findListedSessionForBulkRef(context, ref) {
|
|
3179
3658
|
if (ref.cwd !== undefined)
|
|
3180
3659
|
return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
3181
|
-
return context.allSessions === undefined
|
|
3660
|
+
return context.allSessions === undefined
|
|
3661
|
+
? undefined
|
|
3662
|
+
: findSessionByIdOrPrefix(context.allSessions, ref.id);
|
|
3182
3663
|
}
|
|
3183
3664
|
function findSessionByIdOrPrefix(sessions, sessionId) {
|
|
3184
|
-
return sessions.find((session) => session.id === sessionId) ??
|
|
3665
|
+
return (sessions.find((session) => session.id === sessionId) ??
|
|
3666
|
+
sessions.find((session) => session.id.startsWith(sessionId)));
|
|
3185
3667
|
}
|
|
3186
3668
|
function uniqueStrings(values) {
|
|
3187
3669
|
return [...new Set(values)];
|
|
@@ -3219,7 +3701,9 @@ function clientSessionFromListEntry(session) {
|
|
|
3219
3701
|
modified: session.modified.toISOString(),
|
|
3220
3702
|
messageCount: session.messageCount,
|
|
3221
3703
|
firstMessage: session.firstMessage,
|
|
3222
|
-
...(session.parentSessionPath === undefined
|
|
3704
|
+
...(session.parentSessionPath === undefined
|
|
3705
|
+
? {}
|
|
3706
|
+
: { parentSessionPath: session.parentSessionPath }),
|
|
3223
3707
|
};
|
|
3224
3708
|
}
|
|
3225
3709
|
function archiveInputFromListEntry(session) {
|
|
@@ -3232,7 +3716,9 @@ function archiveInputFromListEntry(session) {
|
|
|
3232
3716
|
messageCount: session.messageCount,
|
|
3233
3717
|
firstMessage: session.firstMessage,
|
|
3234
3718
|
...(session.name === undefined ? {} : { name: session.name }),
|
|
3235
|
-
...(session.parentSessionPath === undefined
|
|
3719
|
+
...(session.parentSessionPath === undefined
|
|
3720
|
+
? {}
|
|
3721
|
+
: { parentSessionPath: session.parentSessionPath }),
|
|
3236
3722
|
};
|
|
3237
3723
|
}
|
|
3238
3724
|
function archiveInputFromActiveSession(session) {
|
|
@@ -3259,7 +3745,9 @@ function archiveCandidateFromListEntry(session) {
|
|
|
3259
3745
|
cwd: session.cwd,
|
|
3260
3746
|
archived: false,
|
|
3261
3747
|
listEntry: session,
|
|
3262
|
-
...(session.parentSessionPath === undefined
|
|
3748
|
+
...(session.parentSessionPath === undefined
|
|
3749
|
+
? {}
|
|
3750
|
+
: { parentSessionPath: session.parentSessionPath }),
|
|
3263
3751
|
};
|
|
3264
3752
|
}
|
|
3265
3753
|
function archiveCandidateFromArchivedRecord(record, fallback) {
|
|
@@ -3298,7 +3786,10 @@ function archiveInputFromCandidate(candidate) {
|
|
|
3298
3786
|
throw new Error(`Session is not available for archiving: ${candidate.id}`);
|
|
3299
3787
|
}
|
|
3300
3788
|
function sessionHasActiveWork(session, extraQueuedMessageCount = 0) {
|
|
3301
|
-
return session.isStreaming ||
|
|
3789
|
+
return (session.isStreaming ||
|
|
3790
|
+
session.isCompacting ||
|
|
3791
|
+
session.isBashRunning ||
|
|
3792
|
+
session.pendingMessageCount + extraQueuedMessageCount > 0);
|
|
3302
3793
|
}
|
|
3303
3794
|
function sessionDisplayName(session) {
|
|
3304
3795
|
return session.sessionName ?? session.sessionId;
|
|
@@ -3309,7 +3800,11 @@ function clientSessionFromArchivedRecord(record, fallback) {
|
|
|
3309
3800
|
const modified = record.modified ?? fallback?.modified.toISOString();
|
|
3310
3801
|
const messageCount = record.messageCount ?? fallback?.messageCount;
|
|
3311
3802
|
const firstMessage = record.firstMessage ?? fallback?.firstMessage;
|
|
3312
|
-
if (path === undefined ||
|
|
3803
|
+
if (path === undefined ||
|
|
3804
|
+
created === undefined ||
|
|
3805
|
+
modified === undefined ||
|
|
3806
|
+
messageCount === undefined ||
|
|
3807
|
+
firstMessage === undefined)
|
|
3313
3808
|
return undefined;
|
|
3314
3809
|
const name = record.name ?? fallback?.name;
|
|
3315
3810
|
const parentSessionPath = record.parentSessionPath ?? fallback?.parentSessionPath;
|
|
@@ -3409,9 +3904,11 @@ function findReciprocalParentSubsessionLink(managers, parentSessionFile, parentS
|
|
|
3409
3904
|
const link = parsePersistedParentSubsessionLink(entry);
|
|
3410
3905
|
if (link === undefined)
|
|
3411
3906
|
continue;
|
|
3412
|
-
if (link.spawnedBySessionId !== parentSessionId ||
|
|
3907
|
+
if (link.spawnedBySessionId !== parentSessionId ||
|
|
3908
|
+
link.spawnedSessionId !== childSessionId)
|
|
3413
3909
|
continue;
|
|
3414
|
-
if (link.spawnedSessionFile === undefined ||
|
|
3910
|
+
if (link.spawnedSessionFile === undefined ||
|
|
3911
|
+
!sessionPathsEqual(link.spawnedSessionFile, childSessionFile))
|
|
3415
3912
|
continue;
|
|
3416
3913
|
return link;
|
|
3417
3914
|
}
|
|
@@ -3421,7 +3918,9 @@ function trackedSubsessionLinkFromParentLink(parentSessionId, link, parentSessio
|
|
|
3421
3918
|
return {
|
|
3422
3919
|
parentSessionId,
|
|
3423
3920
|
childSessionId: link.spawnedSessionId,
|
|
3424
|
-
...(link.spawnedSessionFile === undefined
|
|
3921
|
+
...(link.spawnedSessionFile === undefined
|
|
3922
|
+
? {}
|
|
3923
|
+
: { childSessionFile: link.spawnedSessionFile }),
|
|
3425
3924
|
parentSessionFile,
|
|
3426
3925
|
...(link.cwd === undefined ? {} : { cwd: link.cwd }),
|
|
3427
3926
|
};
|
|
@@ -3431,7 +3930,9 @@ function persistedParentSubsessionLinkData(link) {
|
|
|
3431
3930
|
version: 1,
|
|
3432
3931
|
spawnedBySessionId: link.parentSessionId,
|
|
3433
3932
|
spawnedSessionId: link.childSessionId,
|
|
3434
|
-
...(link.childSessionFile === undefined
|
|
3933
|
+
...(link.childSessionFile === undefined
|
|
3934
|
+
? {}
|
|
3935
|
+
: { spawnedSessionFile: link.childSessionFile }),
|
|
3435
3936
|
...(link.cwd === undefined ? {} : { cwd: link.cwd }),
|
|
3436
3937
|
};
|
|
3437
3938
|
}
|
|
@@ -3443,33 +3944,45 @@ function persistedChildSubsessionLinkData(parentSessionId, childSessionId) {
|
|
|
3443
3944
|
};
|
|
3444
3945
|
}
|
|
3445
3946
|
function parsePersistedParentSubsessionLink(entry) {
|
|
3446
|
-
if (!isRecord(entry) ||
|
|
3947
|
+
if (!isRecord(entry) ||
|
|
3948
|
+
entry["type"] !== "custom" ||
|
|
3949
|
+
entry["customType"] !== SUBSESSION_LINK_CUSTOM_TYPE)
|
|
3447
3950
|
return undefined;
|
|
3448
3951
|
const data = entry["data"];
|
|
3449
3952
|
if (!isRecord(data))
|
|
3450
3953
|
return undefined;
|
|
3451
3954
|
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
3452
3955
|
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
3453
|
-
if (spawnedBySessionId === undefined ||
|
|
3956
|
+
if (spawnedBySessionId === undefined ||
|
|
3957
|
+
spawnedBySessionId === "" ||
|
|
3958
|
+
spawnedSessionId === undefined ||
|
|
3959
|
+
spawnedSessionId === "")
|
|
3454
3960
|
return undefined;
|
|
3455
3961
|
const spawnedSessionFile = getString(data, "spawnedSessionFile");
|
|
3456
3962
|
const cwd = getString(data, "cwd");
|
|
3457
3963
|
return {
|
|
3458
3964
|
spawnedBySessionId,
|
|
3459
3965
|
spawnedSessionId,
|
|
3460
|
-
...(spawnedSessionFile === undefined || spawnedSessionFile === ""
|
|
3966
|
+
...(spawnedSessionFile === undefined || spawnedSessionFile === ""
|
|
3967
|
+
? {}
|
|
3968
|
+
: { spawnedSessionFile }),
|
|
3461
3969
|
...(cwd === undefined || cwd === "" ? {} : { cwd }),
|
|
3462
3970
|
};
|
|
3463
3971
|
}
|
|
3464
3972
|
function parsePersistedChildSubsessionLink(entry) {
|
|
3465
|
-
if (!isRecord(entry) ||
|
|
3973
|
+
if (!isRecord(entry) ||
|
|
3974
|
+
entry["type"] !== "custom" ||
|
|
3975
|
+
entry["customType"] !== SUBSESSION_CHILD_LINK_CUSTOM_TYPE)
|
|
3466
3976
|
return undefined;
|
|
3467
3977
|
const data = entry["data"];
|
|
3468
3978
|
if (!isRecord(data))
|
|
3469
3979
|
return undefined;
|
|
3470
3980
|
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
3471
3981
|
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
3472
|
-
if (spawnedBySessionId === undefined ||
|
|
3982
|
+
if (spawnedBySessionId === undefined ||
|
|
3983
|
+
spawnedBySessionId === "" ||
|
|
3984
|
+
spawnedSessionId === undefined ||
|
|
3985
|
+
spawnedSessionId === "")
|
|
3473
3986
|
return undefined;
|
|
3474
3987
|
return { spawnedBySessionId, spawnedSessionId };
|
|
3475
3988
|
}
|
|
@@ -3494,13 +4007,16 @@ function sessionFileExists(sessionFile) {
|
|
|
3494
4007
|
}
|
|
3495
4008
|
function sessionFileMatches(session, expectedSessionFile) {
|
|
3496
4009
|
const sessionFile = nonEmptyString(session.sessionFile);
|
|
3497
|
-
return sessionFile !== undefined &&
|
|
4010
|
+
return (sessionFile !== undefined &&
|
|
4011
|
+
expectedSessionFile !== undefined &&
|
|
4012
|
+
sessionPathsEqual(sessionFile, expectedSessionFile));
|
|
3498
4013
|
}
|
|
3499
4014
|
function activeSessionFileMatches(active, expectedSessionFile) {
|
|
3500
4015
|
return sessionFileMatches(active.runtime.session, expectedSessionFile);
|
|
3501
4016
|
}
|
|
3502
4017
|
function trackedLinkParentFileMatches(link, parentSessionFile) {
|
|
3503
|
-
return link.parentSessionFile !== undefined &&
|
|
4018
|
+
return (link.parentSessionFile !== undefined &&
|
|
4019
|
+
sessionPathsEqual(link.parentSessionFile, parentSessionFile));
|
|
3504
4020
|
}
|
|
3505
4021
|
async function readSessionHeaderSummary(sessionFile) {
|
|
3506
4022
|
let file;
|
|
@@ -3512,10 +4028,15 @@ async function readSessionHeaderSummary(sessionFile) {
|
|
|
3512
4028
|
if (firstLine === undefined || firstLine === "")
|
|
3513
4029
|
return undefined;
|
|
3514
4030
|
const header = JSON.parse(firstLine);
|
|
3515
|
-
if (!isRecord(header) ||
|
|
4031
|
+
if (!isRecord(header) ||
|
|
4032
|
+
header["type"] !== "session" ||
|
|
4033
|
+
typeof header["id"] !== "string")
|
|
3516
4034
|
return undefined;
|
|
3517
4035
|
const parentSession = getString(header, "parentSession");
|
|
3518
|
-
return {
|
|
4036
|
+
return {
|
|
4037
|
+
id: header["id"],
|
|
4038
|
+
...(parentSession === undefined ? {} : { parentSession }),
|
|
4039
|
+
};
|
|
3519
4040
|
}
|
|
3520
4041
|
catch {
|
|
3521
4042
|
return undefined;
|
|
@@ -3530,7 +4051,8 @@ async function sessionFileHeaderMatches(sessionFile, expected) {
|
|
|
3530
4051
|
return false;
|
|
3531
4052
|
if (expected.parentSessionFile === undefined)
|
|
3532
4053
|
return true;
|
|
3533
|
-
return header.parentSession !== undefined &&
|
|
4054
|
+
return (header.parentSession !== undefined &&
|
|
4055
|
+
sessionPathsEqual(header.parentSession, expected.parentSessionFile));
|
|
3534
4056
|
}
|
|
3535
4057
|
async function clearParentSession(sessionFile) {
|
|
3536
4058
|
const content = await readFile(sessionFile, "utf8");
|
|
@@ -3555,8 +4077,12 @@ function clearSessionQueue(session) {
|
|
|
3555
4077
|
}
|
|
3556
4078
|
function queuedMessagesFromSession(session, extraQueuedMessages = []) {
|
|
3557
4079
|
return [
|
|
3558
|
-
...session
|
|
3559
|
-
|
|
4080
|
+
...session
|
|
4081
|
+
.getSteeringMessages()
|
|
4082
|
+
.map((text) => ({ kind: "steer", text })),
|
|
4083
|
+
...session
|
|
4084
|
+
.getFollowUpMessages()
|
|
4085
|
+
.map((text) => ({ kind: "followUp", text })),
|
|
3560
4086
|
...extraQueuedMessages,
|
|
3561
4087
|
];
|
|
3562
4088
|
}
|
|
@@ -3608,7 +4134,8 @@ function historyMessages(session) {
|
|
|
3608
4134
|
messages.push({
|
|
3609
4135
|
...historyMessage,
|
|
3610
4136
|
entryId,
|
|
3611
|
-
...(previousMessage?.role === "assistant" &&
|
|
4137
|
+
...(previousMessage?.role === "assistant" &&
|
|
4138
|
+
previousMessage.entryId !== undefined
|
|
3612
4139
|
? { previousAssistantEntryId: previousMessage.entryId }
|
|
3613
4140
|
: {}),
|
|
3614
4141
|
...(messages.length > 0 ? { canFork: true } : {}),
|
|
@@ -3621,17 +4148,30 @@ function historyMessages(session) {
|
|
|
3621
4148
|
continue;
|
|
3622
4149
|
}
|
|
3623
4150
|
if (entry["type"] === "custom_message" && entry["display"] === true) {
|
|
3624
|
-
messages.push({
|
|
4151
|
+
messages.push({
|
|
4152
|
+
role: "custom",
|
|
4153
|
+
content: entry["content"],
|
|
4154
|
+
customType: entry["customType"],
|
|
4155
|
+
details: entry["details"],
|
|
4156
|
+
});
|
|
3625
4157
|
previousMessage = { role: "custom", entryId: undefined };
|
|
3626
4158
|
continue;
|
|
3627
4159
|
}
|
|
3628
4160
|
if (entry["type"] === "compaction") {
|
|
3629
|
-
messages.push({
|
|
4161
|
+
messages.push({
|
|
4162
|
+
role: "system",
|
|
4163
|
+
source: "compaction",
|
|
4164
|
+
content: `Compacted history:\n\n${stringValue(entry["summary"])}`,
|
|
4165
|
+
});
|
|
3630
4166
|
previousMessage = { role: "system", entryId: undefined };
|
|
3631
4167
|
continue;
|
|
3632
4168
|
}
|
|
3633
4169
|
if (entry["type"] === "branch_summary") {
|
|
3634
|
-
messages.push({
|
|
4170
|
+
messages.push({
|
|
4171
|
+
role: "system",
|
|
4172
|
+
source: "branch_summary",
|
|
4173
|
+
content: `Branch summary:\n\n${stringValue(entry["summary"])}`,
|
|
4174
|
+
});
|
|
3635
4175
|
previousMessage = { role: "system", entryId: undefined };
|
|
3636
4176
|
}
|
|
3637
4177
|
}
|
|
@@ -3664,7 +4204,9 @@ function finalAssistantText(messages) {
|
|
|
3664
4204
|
continue;
|
|
3665
4205
|
const texts = [];
|
|
3666
4206
|
for (const part of content) {
|
|
3667
|
-
if (isRecord(part) &&
|
|
4207
|
+
if (isRecord(part) &&
|
|
4208
|
+
part["type"] === "text" &&
|
|
4209
|
+
typeof part["text"] === "string")
|
|
3668
4210
|
texts.push(part["text"]);
|
|
3669
4211
|
}
|
|
3670
4212
|
if (texts.length > 0)
|
|
@@ -3675,23 +4217,52 @@ function finalAssistantText(messages) {
|
|
|
3675
4217
|
function toClientEvent(event) {
|
|
3676
4218
|
const eventType = getString(event, "type");
|
|
3677
4219
|
const assistantMessageEvent = getProperty(event, "assistantMessageEvent");
|
|
3678
|
-
if (eventType === "message_update" &&
|
|
3679
|
-
|
|
4220
|
+
if (eventType === "message_update" &&
|
|
4221
|
+
getString(assistantMessageEvent, "type") === "text_delta") {
|
|
4222
|
+
return {
|
|
4223
|
+
type: "assistant.delta",
|
|
4224
|
+
text: getString(assistantMessageEvent, "delta") ?? "",
|
|
4225
|
+
};
|
|
3680
4226
|
}
|
|
3681
|
-
if (eventType === "message_update" &&
|
|
3682
|
-
|
|
4227
|
+
if (eventType === "message_update" &&
|
|
4228
|
+
getString(assistantMessageEvent, "type") === "thinking_delta") {
|
|
4229
|
+
return {
|
|
4230
|
+
type: "assistant.thinking.delta",
|
|
4231
|
+
text: getString(assistantMessageEvent, "delta") ?? "",
|
|
4232
|
+
};
|
|
3683
4233
|
}
|
|
3684
4234
|
if (eventType === "tool_execution_start") {
|
|
3685
4235
|
const args = getProperty(event, "args");
|
|
3686
|
-
return {
|
|
4236
|
+
return {
|
|
4237
|
+
type: "tool.start",
|
|
4238
|
+
toolName: getString(event, "toolName") ?? "",
|
|
4239
|
+
toolCallId: getString(event, "toolCallId") ?? "",
|
|
4240
|
+
summary: summarizeToolArgs(args),
|
|
4241
|
+
args,
|
|
4242
|
+
};
|
|
3687
4243
|
}
|
|
3688
4244
|
if (eventType === "tool_execution_update") {
|
|
3689
4245
|
const partialResult = getProperty(event, "partialResult");
|
|
3690
|
-
return {
|
|
4246
|
+
return {
|
|
4247
|
+
type: "tool.update",
|
|
4248
|
+
toolName: getString(event, "toolName") ?? "",
|
|
4249
|
+
toolCallId: getString(event, "toolCallId") ?? "",
|
|
4250
|
+
text: stringifyToolResult(partialResult),
|
|
4251
|
+
content: toolResultContent(partialResult),
|
|
4252
|
+
details: toolResultDetails(partialResult),
|
|
4253
|
+
};
|
|
3691
4254
|
}
|
|
3692
4255
|
if (eventType === "tool_execution_end") {
|
|
3693
4256
|
const result = getProperty(event, "result");
|
|
3694
|
-
return {
|
|
4257
|
+
return {
|
|
4258
|
+
type: "tool.end",
|
|
4259
|
+
toolName: getString(event, "toolName") ?? "",
|
|
4260
|
+
toolCallId: getString(event, "toolCallId") ?? "",
|
|
4261
|
+
text: stringifyToolResult(result),
|
|
4262
|
+
content: toolResultContent(result),
|
|
4263
|
+
details: toolResultDetails(result),
|
|
4264
|
+
isError: getBoolean(event, "isError") === true,
|
|
4265
|
+
};
|
|
3695
4266
|
}
|
|
3696
4267
|
if (eventType === "agent_start")
|
|
3697
4268
|
return { type: "agent.start" };
|
|
@@ -3699,7 +4270,9 @@ function toClientEvent(event) {
|
|
|
3699
4270
|
return { type: "agent.end" };
|
|
3700
4271
|
if (eventType === "message_end") {
|
|
3701
4272
|
const message = getProperty(event, "message");
|
|
3702
|
-
return message === undefined
|
|
4273
|
+
return message === undefined
|
|
4274
|
+
? { type: "message.end" }
|
|
4275
|
+
: { type: "message.end", message };
|
|
3703
4276
|
}
|
|
3704
4277
|
return { type: "pi.event", eventType: eventType ?? "unknown" };
|
|
3705
4278
|
}
|
|
@@ -3712,13 +4285,18 @@ function summarizeToolArgs(args) {
|
|
|
3712
4285
|
const path = getString(args, "path");
|
|
3713
4286
|
if (path !== undefined)
|
|
3714
4287
|
return path;
|
|
3715
|
-
if (typeof args["oldText"] === "string" &&
|
|
4288
|
+
if (typeof args["oldText"] === "string" &&
|
|
4289
|
+
typeof args["newText"] === "string")
|
|
3716
4290
|
return "edit text replacement";
|
|
3717
4291
|
const edits = args["edits"];
|
|
3718
4292
|
if (Array.isArray(edits))
|
|
3719
4293
|
return `${String(edits.length)} edit${edits.length === 1 ? "" : "s"}`;
|
|
3720
|
-
const entries = Object.entries(args)
|
|
3721
|
-
|
|
4294
|
+
const entries = Object.entries(args)
|
|
4295
|
+
.filter(([, value]) => value != null)
|
|
4296
|
+
.slice(0, 3);
|
|
4297
|
+
return entries
|
|
4298
|
+
.map(([key, value]) => `${key}: ${shortToolValue(value)}`)
|
|
4299
|
+
.join(" · ");
|
|
3722
4300
|
}
|
|
3723
4301
|
function shortToolValue(value) {
|
|
3724
4302
|
if (typeof value === "string")
|
|
@@ -3751,11 +4329,16 @@ function stringifyToolResult(result) {
|
|
|
3751
4329
|
if (typeof result === "string")
|
|
3752
4330
|
return result;
|
|
3753
4331
|
if (Array.isArray(result))
|
|
3754
|
-
return result
|
|
4332
|
+
return result
|
|
4333
|
+
.map(stringifyToolResult)
|
|
4334
|
+
.filter((text) => text !== "")
|
|
4335
|
+
.join("\n");
|
|
3755
4336
|
if (isRecord(result)) {
|
|
3756
4337
|
if (getString(result, "type") === "image")
|
|
3757
4338
|
return "[image]";
|
|
3758
|
-
const text = getString(result, "text") ??
|
|
4339
|
+
const text = getString(result, "text") ??
|
|
4340
|
+
getString(result, "content") ??
|
|
4341
|
+
getString(result, "output");
|
|
3759
4342
|
if (text !== undefined)
|
|
3760
4343
|
return text;
|
|
3761
4344
|
const content = getProperty(result, "content");
|
|
@@ -3781,7 +4364,9 @@ function getBoolean(value, key) {
|
|
|
3781
4364
|
}
|
|
3782
4365
|
function providerOutputTokens(message) {
|
|
3783
4366
|
const output = getProperty(getProperty(message, "usage"), "output");
|
|
3784
|
-
return typeof output === "number" && Number.isFinite(output) && output > 0
|
|
4367
|
+
return typeof output === "number" && Number.isFinite(output) && output > 0
|
|
4368
|
+
? output
|
|
4369
|
+
: undefined;
|
|
3785
4370
|
}
|
|
3786
4371
|
/** Match Pi Web's 4-characters-per-token live approximation. */
|
|
3787
4372
|
function estimatedOutputTokens(message) {
|
|
@@ -3816,7 +4401,9 @@ function stringifyPrimitive(value) {
|
|
|
3816
4401
|
return "";
|
|
3817
4402
|
if (typeof value === "string")
|
|
3818
4403
|
return value;
|
|
3819
|
-
if (typeof value === "number" ||
|
|
4404
|
+
if (typeof value === "number" ||
|
|
4405
|
+
typeof value === "boolean" ||
|
|
4406
|
+
typeof value === "bigint")
|
|
3820
4407
|
return String(value);
|
|
3821
4408
|
return "";
|
|
3822
4409
|
}
|