@jmfederico/pi-web 1.202607.3 → 1.202608.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/client/assets/{CodeViewer-CDVbMiN9.js → CodeViewer-D91Sp61M.js} +1 -1
- package/dist/client/assets/{TerminalPanel-CTS1CgqF.js → TerminalPanel-fYrKc3jh.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-Cqke12ZX.js → UnifiedDiffViewer-BafLWGtF.js} +1 -1
- package/dist/client/assets/{index-LME0LfPb.js → index-CA8q9_o7.js} +310 -353
- package/dist/client/index.html +1 -1
- package/dist/pi-web-plugins/relays/relayDiscovery.js +159 -27
- package/dist/pi-web-plugins/relays/relaysPanelElement.js +173 -16
- package/dist/server/app.js +0 -22
- package/dist/server/app.js.map +1 -1
- package/dist/server/browserMessageProjection.js +0 -2
- package/dist/server/browserMessageProjection.js.map +1 -1
- package/dist/server/configRoutes.js +4 -13
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +2 -13
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/piWebStatus.js +12 -38
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/sessiond/agentHttpDispatcher.js +108 -0
- package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -0
- package/dist/server/sessiond/agentProcessEnvironment.js +62 -0
- package/dist/server/sessiond/agentProcessEnvironment.js.map +1 -0
- package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
- package/dist/server/sessiond.js +26 -5
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/askUserTool.js +0 -3
- package/dist/server/sessions/askUserTool.js.map +1 -1
- package/dist/server/sessions/authRoutes.js +0 -10
- package/dist/server/sessions/authRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +0 -24
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/messagePaging.js +2 -2
- package/dist/server/sessions/messagePaging.js.map +1 -1
- package/dist/server/sessions/oauthLoginFlowService.js +2 -5
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/pendingAskStore.js +0 -3
- package/dist/server/sessions/pendingAskStore.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +182 -2
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +244 -186
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionFileHeader.js +92 -20
- package/dist/server/sessions/sessionFileHeader.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +74 -56
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionSummaryScanner.js +619 -0
- package/dist/server/sessions/sessionSummaryScanner.js.map +1 -0
- package/dist/server/sessions/spawnSessionTool.js +8 -1
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +7 -1
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/workspaces/effectivePathAccess.js +0 -19
- package/dist/server/workspaces/effectivePathAccess.js.map +1 -1
- package/dist/server/workspaces/workspaceDeletionRoutes.js +30 -4
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
- package/dist/server/workspaces/workspaceService.js.map +1 -1
- package/dist/sessiond/config.js +2 -2
- package/dist/sessiond/config.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +12 -29
- package/dist/shared/apiTypes.js +7 -16
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +10 -44
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +0 -3
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/docs/config.md +59 -4
- package/docs/plugins.md +5 -3
- package/package.json +10 -9
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
6
6
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
@@ -56,20 +56,36 @@ function spawnTargetError(decision) {
|
|
|
56
56
|
return new Error("Spawning session is not in a registered project");
|
|
57
57
|
return new Error(`cwd must be a workspace of this project. Allowed: ${decision.allowedCwds.join(", ")}`);
|
|
58
58
|
}
|
|
59
|
-
function
|
|
60
|
-
return `${
|
|
59
|
+
function modelSpecOf(model) {
|
|
60
|
+
return `${model.provider}/${model.id}`;
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Parse a strict `provider/model-id` spec: split on the first `/` (model ids
|
|
64
|
+
* may themselves contain `/`) and require both parts to be non-empty.
|
|
65
|
+
*/
|
|
66
|
+
function parseModelSpec(spec) {
|
|
67
|
+
const slash = spec.indexOf("/");
|
|
68
|
+
if (slash <= 0 || slash === spec.length - 1)
|
|
69
|
+
return undefined;
|
|
70
|
+
return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
|
|
64
71
|
}
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Error for a spawn-tool model spec that matched nothing. States the facts —
|
|
74
|
+
* the bad spec and the required format — with deliberately no model list
|
|
75
|
+
* (a list would invite guesses). The agent loop turns the throw into an
|
|
76
|
+
* error tool result; how to recover is the agent's call.
|
|
77
|
+
*/
|
|
78
|
+
function unknownSpawnModelError(modelSpec) {
|
|
79
|
+
return new Error(`Unknown model "${modelSpec}". Pass an exact "provider/model-id".`);
|
|
67
80
|
}
|
|
68
|
-
function
|
|
69
|
-
return
|
|
81
|
+
function authLossWarningKey(sessionId, provider, modelId) {
|
|
82
|
+
return `${sessionId}:${provider}/${modelId}`;
|
|
70
83
|
}
|
|
71
|
-
function
|
|
72
|
-
return
|
|
84
|
+
function refMatchesActiveSession(ref, active) {
|
|
85
|
+
return cwdPathsEqual(active.runtime.cwd, ref.cwd);
|
|
86
|
+
}
|
|
87
|
+
function refMatchesStartupSession(ref, session) {
|
|
88
|
+
return cwdPathsEqual(session.sessionManager.getCwd(), ref.cwd);
|
|
73
89
|
}
|
|
74
90
|
function sessionTreeNavigationOptions(request) {
|
|
75
91
|
switch (request.summary.mode) {
|
|
@@ -220,26 +236,31 @@ export function dismissSessionWarning(session, dismissId) {
|
|
|
220
236
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
221
237
|
if (!(options.sessionManager instanceof SessionManager))
|
|
222
238
|
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
223
|
-
const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.delegationToolsEnabled);
|
|
239
|
+
const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.initialThinkingLevel, options.delegationToolsEnabled);
|
|
224
240
|
return createAgentSessionRuntime(runtimeFactory, {
|
|
225
241
|
cwd: options.cwd,
|
|
226
242
|
agentDir: options.agentDir,
|
|
227
243
|
sessionManager: options.sessionManager,
|
|
228
244
|
});
|
|
229
245
|
}
|
|
230
|
-
function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, delegationToolsEnabled) {
|
|
246
|
+
function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, initialThinkingLevel, delegationToolsEnabled) {
|
|
231
247
|
// These inputs belong only to the session being opened. A later runtime
|
|
232
|
-
// replacement resolves its own model and delegation capability
|
|
248
|
+
// replacement resolves its own model and delegation capability, and restores
|
|
249
|
+
// the thinking level from the existing session file.
|
|
233
250
|
let pendingInitialModel = initialModel;
|
|
251
|
+
let pendingInitialThinkingLevel = initialThinkingLevel;
|
|
234
252
|
let pendingDelegationToolsEnabled = delegationToolsEnabled;
|
|
235
253
|
return async (options) => {
|
|
236
254
|
const model = pendingInitialModel;
|
|
255
|
+
const thinkingLevel = pendingInitialThinkingLevel;
|
|
237
256
|
const toolsEnabled = pendingDelegationToolsEnabled;
|
|
238
257
|
pendingInitialModel = undefined;
|
|
258
|
+
pendingInitialThinkingLevel = undefined;
|
|
239
259
|
pendingDelegationToolsEnabled = undefined;
|
|
240
260
|
return createRuntime({
|
|
241
261
|
...options,
|
|
242
262
|
...(model === undefined ? {} : { initialModel: model }),
|
|
263
|
+
...(thinkingLevel === undefined ? {} : { initialThinkingLevel: thinkingLevel }),
|
|
243
264
|
...(toolsEnabled === undefined ? {} : { delegationToolsEnabled: toolsEnabled }),
|
|
244
265
|
});
|
|
245
266
|
};
|
|
@@ -255,7 +276,7 @@ export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn,
|
|
|
255
276
|
];
|
|
256
277
|
}
|
|
257
278
|
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, askUser) {
|
|
258
|
-
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => {
|
|
279
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
259
280
|
const services = await createAgentSessionServices({ cwd, agentDir, modelRuntime });
|
|
260
281
|
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
261
282
|
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
@@ -266,6 +287,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
266
287
|
customTools,
|
|
267
288
|
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
268
289
|
...(initialModel === undefined ? {} : { model: initialModel }),
|
|
290
|
+
...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
|
|
269
291
|
});
|
|
270
292
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
271
293
|
};
|
|
@@ -325,8 +347,13 @@ export class PiSessionService {
|
|
|
325
347
|
this.subsessionLinks = new Map();
|
|
326
348
|
/** Parent id/file identities whose persisted links have already been loaded. */
|
|
327
349
|
this.subsessionHydratedParents = new Set();
|
|
328
|
-
/**
|
|
350
|
+
/**
|
|
351
|
+
* Session file path -> its parsed header and the file identity at read
|
|
352
|
+
* time. Headers are written once, so identity-verified reads are cached.
|
|
353
|
+
*/
|
|
329
354
|
this.sessionHeaderCache = new Map();
|
|
355
|
+
/** Session file path -> its in-flight header read, so concurrent scans share one read per file. */
|
|
356
|
+
this.pendingSessionHeaderReads = new Map();
|
|
330
357
|
/**
|
|
331
358
|
* Tracked subsession id -> whether a completion notification is armed.
|
|
332
359
|
* Armed when the child starts working; firing on completion disarms it so a
|
|
@@ -340,18 +367,27 @@ export class PiSessionService {
|
|
|
340
367
|
this.unreadPublicationFlushRequested = false;
|
|
341
368
|
this.unreadPublicationStopped = false;
|
|
342
369
|
/**
|
|
343
|
-
* Read a session file header, memoized per path
|
|
344
|
-
*
|
|
345
|
-
*
|
|
370
|
+
* Read a session file header, memoized per path, with one in-flight read
|
|
371
|
+
* per path: concurrent scans of a shared session directory share the read
|
|
372
|
+
* instead of each opening the file. Pi writes the header once at session
|
|
373
|
+
* creation, so a successful read stays valid as long as the path still
|
|
374
|
+
* holds the same file: every hit re-checks the file's identity (dev/ino),
|
|
375
|
+
* and a replacement landed at a cached path is re-read instead of serving
|
|
376
|
+
* the previous file's header — the summary scanner detects such
|
|
377
|
+
* replacements, so the listing and the header reads must agree. The one
|
|
378
|
+
* thing identity cannot detect is an in-place rewrite that keeps the
|
|
379
|
+
* inode; detach, the only such writer, deletes the entry itself. Failures
|
|
380
|
+
* are not cached so a session file that appears later is picked up.
|
|
346
381
|
*/
|
|
347
|
-
this.readCachedSessionHeader =
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
350
|
-
return
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
382
|
+
this.readCachedSessionHeader = (sessionFile) => {
|
|
383
|
+
const pending = this.pendingSessionHeaderReads.get(sessionFile);
|
|
384
|
+
if (pending !== undefined)
|
|
385
|
+
return pending;
|
|
386
|
+
const read = this.loadSessionHeader(sessionFile).finally(() => {
|
|
387
|
+
this.pendingSessionHeaderReads.delete(sessionFile);
|
|
388
|
+
});
|
|
389
|
+
this.pendingSessionHeaderReads.set(sessionFile, read);
|
|
390
|
+
return read;
|
|
355
391
|
};
|
|
356
392
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
357
393
|
this.agentDir = deps.agentDir;
|
|
@@ -381,7 +417,7 @@ export class PiSessionService {
|
|
|
381
417
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
382
418
|
this.workspaceActivity = deps.workspaceActivity;
|
|
383
419
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
384
|
-
this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, { echoUserMessage: false }), events, {
|
|
420
|
+
this.commandService = new SessionCommandService((sessionId) => this.getActive(this.activeSessionRef(sessionId)), (sessionId, text) => this.prompt(this.activeSessionRef(sessionId), text, undefined, undefined, { echoUserMessage: false }), events, {
|
|
385
421
|
onCompactionStart: (session) => {
|
|
386
422
|
this.beginSessionEntryMutation(session, "compact the session");
|
|
387
423
|
this.publishActivity(session, "compacting", "active");
|
|
@@ -465,7 +501,6 @@ export class PiSessionService {
|
|
|
465
501
|
await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
|
|
466
502
|
readyDeleteRecords.push(record);
|
|
467
503
|
}
|
|
468
|
-
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
469
504
|
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
470
505
|
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
471
506
|
await this.forgetUnreadSessions(deleteRecords);
|
|
@@ -507,6 +542,7 @@ export class PiSessionService {
|
|
|
507
542
|
this.subsessionHydratedParents.clear();
|
|
508
543
|
this.subsessionNotifyArmed.clear();
|
|
509
544
|
this.sessionHeaderCache.clear();
|
|
545
|
+
this.pendingSessionHeaderReads.clear();
|
|
510
546
|
this.notificationStore.clearAll("service-dispose");
|
|
511
547
|
await Promise.all(activeSessions.map(async (active) => {
|
|
512
548
|
active.unsubscribe();
|
|
@@ -524,9 +560,7 @@ export class PiSessionService {
|
|
|
524
560
|
async list(cwd) {
|
|
525
561
|
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
|
|
526
562
|
const sessionsById = new Map(sessions.map((session) => [session.id, session]));
|
|
527
|
-
const archivedForCwd =
|
|
528
|
-
.filter((record) => record.cwd === cwd)
|
|
529
|
-
.map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
|
|
563
|
+
const archivedForCwd = archivedRecords.filter((record) => record.cwd === cwd);
|
|
530
564
|
const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
|
|
531
565
|
for (const record of archivedForCwd) {
|
|
532
566
|
this.publishNotificationMutations(this.notificationStore.clearSession(record.sessionId, "archive-reconcile"));
|
|
@@ -576,8 +610,10 @@ export class PiSessionService {
|
|
|
576
610
|
*
|
|
577
611
|
* Only sibling workspaces are scanned: agents may only spawn into workspaces
|
|
578
612
|
* of the spawning session's own project, so that bounds where a child can be.
|
|
579
|
-
*
|
|
580
|
-
*
|
|
613
|
+
* Each sibling scan reads only the header line of every session file (the
|
|
614
|
+
* only place `parentSessionPath` is recorded), never the transcripts. Listing
|
|
615
|
+
* is skipped entirely when no project-workspace locator is configured or the
|
|
616
|
+
* cwd belongs to no registered project.
|
|
581
617
|
*/
|
|
582
618
|
async countChildrenInSiblingWorkspaces(sessions, cwd) {
|
|
583
619
|
if (this.projectWorkspaces === undefined || sessions.length === 0)
|
|
@@ -586,10 +622,7 @@ export class PiSessionService {
|
|
|
586
622
|
const siblingCwds = await siblingWorkspaceCwds(this.projectWorkspaces, cwd);
|
|
587
623
|
if (siblingCwds.length === 0)
|
|
588
624
|
return new Map();
|
|
589
|
-
const listings = await Promise.all(siblingCwds.map(
|
|
590
|
-
const entries = await this.sessionManager.list(siblingCwd);
|
|
591
|
-
return entries.flatMap((entry) => entry.parentSessionPath === undefined ? [] : [entry.parentSessionPath]);
|
|
592
|
-
}));
|
|
625
|
+
const listings = await Promise.all(siblingCwds.map((siblingCwd) => this.sessionManager.listParentSessionPaths(siblingCwd, this.readCachedSessionHeader)));
|
|
593
626
|
return countOutOfListingChildren(sessions, listings.flat());
|
|
594
627
|
}
|
|
595
628
|
catch (error) {
|
|
@@ -597,6 +630,27 @@ export class PiSessionService {
|
|
|
597
630
|
return new Map();
|
|
598
631
|
}
|
|
599
632
|
}
|
|
633
|
+
async loadSessionHeader(sessionFile) {
|
|
634
|
+
let stats;
|
|
635
|
+
try {
|
|
636
|
+
stats = await stat(sessionFile);
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// Gone or unreadable: report no header and forget any cached entry.
|
|
640
|
+
this.sessionHeaderCache.delete(sessionFile);
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
const cached = this.sessionHeaderCache.get(sessionFile);
|
|
644
|
+
if (cached?.dev === stats.dev && cached.ino === stats.ino)
|
|
645
|
+
return cached.header;
|
|
646
|
+
const header = await readSessionHeaderSummary(sessionFile);
|
|
647
|
+
if (header === undefined) {
|
|
648
|
+
this.sessionHeaderCache.delete(sessionFile);
|
|
649
|
+
return undefined;
|
|
650
|
+
}
|
|
651
|
+
this.sessionHeaderCache.set(sessionFile, { header, dev: stats.dev, ino: stats.ino });
|
|
652
|
+
return header;
|
|
653
|
+
}
|
|
600
654
|
async start(cwd, options = {}) {
|
|
601
655
|
return this.startSession(cwd, options);
|
|
602
656
|
}
|
|
@@ -605,6 +659,7 @@ export class PiSessionService {
|
|
|
605
659
|
startupIntent: "create",
|
|
606
660
|
...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
|
|
607
661
|
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
662
|
+
...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
|
|
608
663
|
...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
|
|
609
664
|
});
|
|
610
665
|
const { session } = active.runtime;
|
|
@@ -637,10 +692,23 @@ export class PiSessionService {
|
|
|
637
692
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
638
693
|
if (!decision.allowed)
|
|
639
694
|
throw spawnTargetError(decision);
|
|
640
|
-
|
|
641
|
-
|
|
695
|
+
// A model spec overrides the inherited model. Only a spec resolves
|
|
696
|
+
// against the spawning session; the default path must not depend on it.
|
|
697
|
+
const model = input.modelSpec === undefined
|
|
698
|
+
? input.model
|
|
699
|
+
: await this.resolveSpawnModel({ id: input.spawningSessionId, cwd: input.spawningCwd }, input.modelSpec);
|
|
700
|
+
const created = await this.start(decision.cwd, {
|
|
701
|
+
...(model === undefined ? {} : { initialModel: model }),
|
|
702
|
+
...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
|
|
703
|
+
});
|
|
704
|
+
const modelUsed = this.active.get(created.id)?.runtime.session.model;
|
|
705
|
+
await this.prompt(created, input.prompt);
|
|
642
706
|
this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
|
|
643
|
-
return {
|
|
707
|
+
return {
|
|
708
|
+
sessionId: created.id,
|
|
709
|
+
cwd: decision.cwd,
|
|
710
|
+
...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
|
|
711
|
+
};
|
|
644
712
|
}
|
|
645
713
|
/**
|
|
646
714
|
* Start a *tracked* child session on behalf of a LLM. Identical to
|
|
@@ -654,11 +722,18 @@ export class PiSessionService {
|
|
|
654
722
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
655
723
|
if (!decision.allowed)
|
|
656
724
|
throw spawnTargetError(decision);
|
|
725
|
+
// A model spec overrides the inherited model and is resolved against the
|
|
726
|
+
// parent's model runtime; only a spec resolves against the parent.
|
|
727
|
+
const model = input.modelSpec === undefined
|
|
728
|
+
? input.model
|
|
729
|
+
: await this.resolveSpawnModel({ id: input.parentSessionId, cwd: input.spawningCwd }, input.modelSpec);
|
|
657
730
|
const created = await this.startSession(decision.cwd, {
|
|
658
731
|
...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
|
|
659
|
-
...(
|
|
732
|
+
...(model === undefined ? {} : { initialModel: model }),
|
|
733
|
+
...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
|
|
660
734
|
creationProvenance: "tracked-subsession",
|
|
661
735
|
});
|
|
736
|
+
const modelUsed = this.active.get(created.id)?.runtime.session.model;
|
|
662
737
|
const parentSessionFile = nonEmptyString(input.parentSessionFile);
|
|
663
738
|
const link = {
|
|
664
739
|
parentSessionId: input.parentSessionId,
|
|
@@ -670,9 +745,43 @@ export class PiSessionService {
|
|
|
670
745
|
await this.registerVerifiedSubsession(link);
|
|
671
746
|
this.persistSubsessionLink(link);
|
|
672
747
|
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
673
|
-
await this.prompt(created
|
|
748
|
+
await this.prompt(created, input.prompt);
|
|
674
749
|
this.logger.info({ parentSessionId: input.parentSessionId, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_subsession started a tracked child session");
|
|
675
|
-
return {
|
|
750
|
+
return {
|
|
751
|
+
sessionId: created.id,
|
|
752
|
+
cwd: decision.cwd,
|
|
753
|
+
...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* The models a session may pick from: its scoped set when model-scoped,
|
|
758
|
+
* otherwise the runtime's available snapshot. Refreshes the runtime catalog
|
|
759
|
+
* first so callers see newly configured providers and models.
|
|
760
|
+
*/
|
|
761
|
+
async sessionModelCandidates(session) {
|
|
762
|
+
await session.modelRuntime.refresh();
|
|
763
|
+
return session.scopedModels.length > 0
|
|
764
|
+
? session.scopedModels.map((scoped) => scoped.model)
|
|
765
|
+
: session.modelRuntime.getAvailableSnapshot();
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Resolve a strict `provider/model-id` spec from a spawn tool against the
|
|
769
|
+
* *spawning* session's model runtime, using the same candidates
|
|
770
|
+
* {@link setModel} offers plus a direct runtime lookup as fallback. Unknown
|
|
771
|
+
* or malformed specs throw; the agent loop turns that into an error tool
|
|
772
|
+
* result the spawning agent can retry from.
|
|
773
|
+
*/
|
|
774
|
+
async resolveSpawnModel(spawningRef, modelSpec) {
|
|
775
|
+
const session = await this.getOrOpen(spawningRef);
|
|
776
|
+
const parsed = parseModelSpec(modelSpec);
|
|
777
|
+
const candidates = await this.sessionModelCandidates(session);
|
|
778
|
+
const model = parsed === undefined
|
|
779
|
+
? undefined
|
|
780
|
+
: candidates.find((candidate) => candidate.provider === parsed.provider && candidate.id === parsed.modelId)
|
|
781
|
+
?? session.modelRuntime.getModel(parsed.provider, parsed.modelId);
|
|
782
|
+
if (model === undefined)
|
|
783
|
+
throw unknownSpawnModelError(modelSpec);
|
|
784
|
+
return model;
|
|
676
785
|
}
|
|
677
786
|
/**
|
|
678
787
|
* Register the question set an agent wants the user to answer as the session's
|
|
@@ -1240,26 +1349,20 @@ export class PiSessionService {
|
|
|
1240
1349
|
const streamingMessage = session.state.streamingMessage;
|
|
1241
1350
|
const partial = streamingMessage === undefined || streamingMessage === null
|
|
1242
1351
|
? null
|
|
1243
|
-
: projectBrowserMessage(streamingMessage);
|
|
1352
|
+
: annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
|
|
1244
1353
|
return { seq, partial };
|
|
1245
1354
|
}
|
|
1246
1355
|
async availableModels(ref) {
|
|
1247
1356
|
const session = await this.getOrOpen(ref);
|
|
1248
|
-
await
|
|
1249
|
-
const models = session.scopedModels.length > 0
|
|
1250
|
-
? session.scopedModels.map((scoped) => scoped.model)
|
|
1251
|
-
: session.modelRuntime.getAvailableSnapshot();
|
|
1357
|
+
const models = await this.sessionModelCandidates(session);
|
|
1252
1358
|
return models.map(modelToClientModel);
|
|
1253
1359
|
}
|
|
1254
1360
|
async setModel(ref, provider, modelId) {
|
|
1255
1361
|
await this.assertWritable(ref);
|
|
1256
1362
|
const session = await this.getOrOpen(ref);
|
|
1257
1363
|
this.assertTreeNavigationInactive(session, "change models");
|
|
1258
|
-
await
|
|
1364
|
+
const candidates = await this.sessionModelCandidates(session);
|
|
1259
1365
|
this.assertTreeNavigationInactive(session, "change models");
|
|
1260
|
-
const candidates = session.scopedModels.length > 0
|
|
1261
|
-
? session.scopedModels.map((scoped) => scoped.model)
|
|
1262
|
-
: session.modelRuntime.getAvailableSnapshot();
|
|
1263
1366
|
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
|
|
1264
1367
|
?? session.modelRuntime.getModel(provider, modelId);
|
|
1265
1368
|
if (model === undefined)
|
|
@@ -1432,7 +1535,7 @@ export class PiSessionService {
|
|
|
1432
1535
|
async navigateTree(ref, request) {
|
|
1433
1536
|
if (request.targetId.trim() === "")
|
|
1434
1537
|
throw new Error("Session tree target is required");
|
|
1435
|
-
if (this.isTreeExclusiveSessionIdentityActive(
|
|
1538
|
+
if (this.isTreeExclusiveSessionIdentityActive(ref.id)) {
|
|
1436
1539
|
throw new Error("Stop current session activity before navigating the session tree");
|
|
1437
1540
|
}
|
|
1438
1541
|
await this.assertWritable(ref);
|
|
@@ -1531,7 +1634,7 @@ export class PiSessionService {
|
|
|
1531
1634
|
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
1532
1635
|
const [archivedRecords, sessionContext] = await Promise.all([
|
|
1533
1636
|
this.archiveStore.list(),
|
|
1534
|
-
this.
|
|
1637
|
+
this.bulkSessionRefContext(uniqueRefs),
|
|
1535
1638
|
]);
|
|
1536
1639
|
const failures = [];
|
|
1537
1640
|
const alreadyArchivedSessionIds = [];
|
|
@@ -1545,7 +1648,7 @@ export class PiSessionService {
|
|
|
1545
1648
|
unreadArchivedIdentities.push(archived);
|
|
1546
1649
|
continue;
|
|
1547
1650
|
}
|
|
1548
|
-
const active = this.
|
|
1651
|
+
const active = this.activeForRef(bulkRefToSessionRef(ref));
|
|
1549
1652
|
const listed = findListedSessionForBulkRef(sessionContext, ref);
|
|
1550
1653
|
const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
|
|
1551
1654
|
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
@@ -1569,7 +1672,7 @@ export class PiSessionService {
|
|
|
1569
1672
|
}
|
|
1570
1673
|
const readyPlanItems = [];
|
|
1571
1674
|
for (const item of planItems) {
|
|
1572
|
-
const active = this.
|
|
1675
|
+
const active = this.activeForRef({ id: item.input.sessionId, cwd: item.input.cwd });
|
|
1573
1676
|
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1574
1677
|
failures.push({ sessionId: item.input.sessionId, error: "Stop current session activity before archiving" });
|
|
1575
1678
|
continue;
|
|
@@ -1646,18 +1749,6 @@ export class PiSessionService {
|
|
|
1646
1749
|
await this.archiveStore.restore(archived.sessionId);
|
|
1647
1750
|
await this.forgetUnreadSessions([archived]);
|
|
1648
1751
|
}
|
|
1649
|
-
async deleteArchived(ref) {
|
|
1650
|
-
const record = await this.getArchived(ref);
|
|
1651
|
-
if (record === undefined)
|
|
1652
|
-
throw new Error("Archived session not found");
|
|
1653
|
-
if (this.archiveStore.deleteArchived === undefined)
|
|
1654
|
-
throw new Error("Archive store does not support deletion");
|
|
1655
|
-
await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
|
|
1656
|
-
if (record.archivePath === undefined)
|
|
1657
|
-
await this.ensureArchivedRecordMoved(record);
|
|
1658
|
-
await this.archiveStore.deleteArchived(record.sessionId);
|
|
1659
|
-
await this.forgetUnreadSessions([record]);
|
|
1660
|
-
}
|
|
1661
1752
|
async deleteArchivedMany(refs) {
|
|
1662
1753
|
if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
1663
1754
|
throw new Error("Archive store does not support deletion");
|
|
@@ -1671,7 +1762,7 @@ export class PiSessionService {
|
|
|
1671
1762
|
failures.push({ sessionId: ref.id, error: "Archived session not found" });
|
|
1672
1763
|
continue;
|
|
1673
1764
|
}
|
|
1674
|
-
const active = this.
|
|
1765
|
+
const active = this.activeForRef({ id: record.sessionId, cwd: record.cwd });
|
|
1675
1766
|
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1676
1767
|
failures.push({ sessionId: record.sessionId, error: "Stop current session activity before deleting archived session" });
|
|
1677
1768
|
continue;
|
|
@@ -1688,12 +1779,7 @@ export class PiSessionService {
|
|
|
1688
1779
|
failures.push({ sessionId: item.record.sessionId, error: errorMessage(error) });
|
|
1689
1780
|
}
|
|
1690
1781
|
}
|
|
1691
|
-
const
|
|
1692
|
-
failures.push(...moveFailures);
|
|
1693
|
-
const moveFailureIds = new Set(moveFailures.map((failure) => failure.sessionId));
|
|
1694
|
-
const deleteIds = readyRecords
|
|
1695
|
-
.map((record) => record.sessionId)
|
|
1696
|
-
.filter((sessionId) => !moveFailureIds.has(sessionId));
|
|
1782
|
+
const deleteIds = readyRecords.map((record) => record.sessionId);
|
|
1697
1783
|
let deletedSessionIds = [];
|
|
1698
1784
|
try {
|
|
1699
1785
|
deletedSessionIds = await this.archiveStoreDeleteArchivedMany(deleteIds);
|
|
@@ -1746,6 +1832,14 @@ export class PiSessionService {
|
|
|
1746
1832
|
if (sessionFile === undefined || sessionFile === "")
|
|
1747
1833
|
throw new Error("Session is not persisted");
|
|
1748
1834
|
await clearParentSession(sessionFile);
|
|
1835
|
+
// The header cache memoizes the first successful read, and detach is the
|
|
1836
|
+
// only flow that rewrites a header; drop the stale entry so sibling child
|
|
1837
|
+
// counts and parent locations observe the cleared parent immediately.
|
|
1838
|
+
this.sessionHeaderCache.delete(sessionFile);
|
|
1839
|
+
// The same in-place rewrite is invisible to the gateway's summary memo
|
|
1840
|
+
// (same inode, possibly grown file), which would otherwise keep listing
|
|
1841
|
+
// the old parent link and a stale message count until restart.
|
|
1842
|
+
this.sessionManager.invalidateSessionFile(sessionFile);
|
|
1749
1843
|
clearParentSessionHeader(session.sessionManager);
|
|
1750
1844
|
this.unregisterSubsession(session.sessionId);
|
|
1751
1845
|
await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }]);
|
|
@@ -1765,7 +1859,7 @@ export class PiSessionService {
|
|
|
1765
1859
|
return this.statusFromSession(session);
|
|
1766
1860
|
}
|
|
1767
1861
|
async abort(ref) {
|
|
1768
|
-
const active = this.
|
|
1862
|
+
const active = this.activeForRef(ref);
|
|
1769
1863
|
if (active === undefined)
|
|
1770
1864
|
return;
|
|
1771
1865
|
const sessionId = active.runtime.session.sessionId;
|
|
@@ -1791,31 +1885,26 @@ export class PiSessionService {
|
|
|
1791
1885
|
}
|
|
1792
1886
|
}
|
|
1793
1887
|
async stop(ref) {
|
|
1794
|
-
const active = this.
|
|
1888
|
+
const active = this.activeForRef(ref);
|
|
1795
1889
|
if (active !== undefined) {
|
|
1796
1890
|
await this.closeActive(active.runtime.session.sessionId);
|
|
1797
1891
|
return;
|
|
1798
1892
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1893
|
+
// A session whose open is parked (e.g. on a session_start dialog) is not
|
|
1894
|
+
// active yet; close it through the same path so stopping cannot block
|
|
1895
|
+
// behind the dialog timeout.
|
|
1896
|
+
const startup = this.startupSessionForRef(ref);
|
|
1897
|
+
if (startup !== undefined) {
|
|
1898
|
+
await this.closeActive(startup.sessionId);
|
|
1801
1899
|
return;
|
|
1802
1900
|
}
|
|
1803
|
-
|
|
1901
|
+
this.publishNotificationMutations(this.notificationStore.clearSessionIdentity(ref.id, canonicalizeStoredCwd(ref.cwd), "runtime-close"));
|
|
1804
1902
|
}
|
|
1805
|
-
async
|
|
1903
|
+
async bulkSessionRefContext(refs) {
|
|
1806
1904
|
const cwdSet = new Set();
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
needsAllSessions = true;
|
|
1811
|
-
else
|
|
1812
|
-
cwdSet.add(ref.cwd);
|
|
1813
|
-
}
|
|
1814
|
-
const [sessionsByCwd, allSessions] = await Promise.all([
|
|
1815
|
-
this.listSessionsByCwd([...cwdSet]),
|
|
1816
|
-
needsAllSessions ? this.sessionManager.listAll?.() ?? Promise.resolve([]) : Promise.resolve(undefined),
|
|
1817
|
-
]);
|
|
1818
|
-
return allSessions === undefined ? { sessionsByCwd } : { sessionsByCwd, allSessions };
|
|
1905
|
+
for (const ref of refs)
|
|
1906
|
+
cwdSet.add(ref.cwd);
|
|
1907
|
+
return { sessionsByCwd: await this.listSessionsByCwd([...cwdSet]) };
|
|
1819
1908
|
}
|
|
1820
1909
|
async listSessionsByCwd(cwds) {
|
|
1821
1910
|
const uniqueCwds = uniqueStrings(cwds);
|
|
@@ -1843,36 +1932,8 @@ export class PiSessionService {
|
|
|
1843
1932
|
await this.archiveStore.deleteArchived(sessionId);
|
|
1844
1933
|
return [...sessionIds];
|
|
1845
1934
|
}
|
|
1846
|
-
async moveLegacyArchivedRecordsForDelete(records) {
|
|
1847
|
-
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
1848
|
-
if (legacyRecords.length === 0)
|
|
1849
|
-
return [];
|
|
1850
|
-
let sessionsByCwd;
|
|
1851
|
-
try {
|
|
1852
|
-
sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1853
|
-
}
|
|
1854
|
-
catch (error) {
|
|
1855
|
-
return legacyRecords.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
1856
|
-
}
|
|
1857
|
-
const moveInputs = legacyRecords
|
|
1858
|
-
.map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
|
|
1859
|
-
.filter(isDefined)
|
|
1860
|
-
.map(archiveInputFromListEntry);
|
|
1861
|
-
if (moveInputs.length === 0)
|
|
1862
|
-
return [];
|
|
1863
|
-
try {
|
|
1864
|
-
await this.archiveStoreArchiveMany(moveInputs);
|
|
1865
|
-
return [];
|
|
1866
|
-
}
|
|
1867
|
-
catch (error) {
|
|
1868
|
-
const failedIds = new Set(moveInputs.map((input) => input.sessionId));
|
|
1869
|
-
return legacyRecords
|
|
1870
|
-
.filter((record) => failedIds.has(record.sessionId))
|
|
1871
|
-
.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
1935
|
async cleanupPlan(request) {
|
|
1875
|
-
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll
|
|
1936
|
+
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll(), this.archiveStore.list()]);
|
|
1876
1937
|
return planSessionCleanup({
|
|
1877
1938
|
sessions,
|
|
1878
1939
|
archivedRecords,
|
|
@@ -1901,34 +1962,6 @@ export class PiSessionService {
|
|
|
1901
1962
|
}
|
|
1902
1963
|
return [...sessionIds];
|
|
1903
1964
|
}
|
|
1904
|
-
async ensureArchivedSessionMoved(record, session) {
|
|
1905
|
-
if (session === undefined || this.active.has(record.sessionId))
|
|
1906
|
-
return record;
|
|
1907
|
-
try {
|
|
1908
|
-
return await this.archiveStore.archive(archiveInputFromListEntry(session));
|
|
1909
|
-
}
|
|
1910
|
-
catch {
|
|
1911
|
-
return record;
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
async ensureArchivedRecordMoved(record) {
|
|
1915
|
-
const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
|
|
1916
|
-
if (session === undefined)
|
|
1917
|
-
return record;
|
|
1918
|
-
const [moved] = await this.archiveStoreArchiveMany([archiveInputFromListEntry(session)]);
|
|
1919
|
-
return moved ?? record;
|
|
1920
|
-
}
|
|
1921
|
-
async ensureArchivedRecordsMoved(records) {
|
|
1922
|
-
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
1923
|
-
if (legacyRecords.length === 0)
|
|
1924
|
-
return;
|
|
1925
|
-
const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1926
|
-
const moveInputs = legacyRecords
|
|
1927
|
-
.map((record) => sessionsByCwd.get(record.cwd)?.find((candidate) => candidate.id === record.sessionId))
|
|
1928
|
-
.filter(isDefined)
|
|
1929
|
-
.map(archiveInputFromListEntry);
|
|
1930
|
-
await this.archiveStoreArchiveMany(moveInputs);
|
|
1931
|
-
}
|
|
1932
1965
|
async archiveInputForSession(session) {
|
|
1933
1966
|
const cwd = session.sessionManager.getCwd();
|
|
1934
1967
|
const sessionFile = session.sessionFile;
|
|
@@ -2065,7 +2098,7 @@ export class PiSessionService {
|
|
|
2065
2098
|
return (await this.getActive(ref)).runtime.session;
|
|
2066
2099
|
}
|
|
2067
2100
|
async getActive(ref, options = {}) {
|
|
2068
|
-
const active = this.
|
|
2101
|
+
const active = this.activeForRef(ref);
|
|
2069
2102
|
if (active !== undefined)
|
|
2070
2103
|
return active;
|
|
2071
2104
|
const archived = await this.getArchived(ref);
|
|
@@ -2073,15 +2106,15 @@ export class PiSessionService {
|
|
|
2073
2106
|
const { archivePath } = archived;
|
|
2074
2107
|
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath), { notifications: "disabled" });
|
|
2075
2108
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2109
|
+
// Resolve the session file directly by id: opening one inactive session
|
|
2110
|
+
// must not pay for a full transcript listing of its whole workspace.
|
|
2111
|
+
const match = await this.sessionManager.resolveSessionFile(ref.cwd, ref.id, this.readCachedSessionHeader);
|
|
2079
2112
|
if (!match)
|
|
2080
2113
|
throw new Error("Session not found");
|
|
2081
2114
|
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
2082
2115
|
}
|
|
2083
2116
|
openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
|
|
2084
|
-
const active = this.
|
|
2117
|
+
const active = this.activeForRef({ id: sessionId, cwd });
|
|
2085
2118
|
if (active !== undefined)
|
|
2086
2119
|
return Promise.resolve(active);
|
|
2087
2120
|
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
|
|
@@ -2105,34 +2138,45 @@ export class PiSessionService {
|
|
|
2105
2138
|
.map((pending) => pending.promise);
|
|
2106
2139
|
}
|
|
2107
2140
|
async getArchived(ref) {
|
|
2108
|
-
const archived = await this.archiveStore.get(
|
|
2141
|
+
const archived = await this.archiveStore.get(ref.id);
|
|
2109
2142
|
if (archived === undefined)
|
|
2110
2143
|
return undefined;
|
|
2111
|
-
if (
|
|
2144
|
+
if (archived.cwd !== ref.cwd)
|
|
2112
2145
|
return undefined;
|
|
2113
2146
|
return archived;
|
|
2114
2147
|
}
|
|
2115
2148
|
isCurrentActiveSession(session) {
|
|
2116
2149
|
return this.active.get(session.sessionId)?.runtime.session === session;
|
|
2117
2150
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2151
|
+
/**
|
|
2152
|
+
* The command service tracks sessions by id alone; its callbacks only ever
|
|
2153
|
+
* run against a session the caller just resolved as active, so the cwd
|
|
2154
|
+
* needed for a full ref comes from that active runtime.
|
|
2155
|
+
*/
|
|
2156
|
+
activeSessionRef(sessionId) {
|
|
2157
|
+
const active = this.active.get(sessionId);
|
|
2158
|
+
if (active === undefined)
|
|
2159
|
+
throw new Error("Session not found");
|
|
2160
|
+
return { id: sessionId, cwd: active.runtime.cwd };
|
|
2161
|
+
}
|
|
2162
|
+
activeForRef(ref) {
|
|
2163
|
+
const sessionId = ref.id;
|
|
2120
2164
|
const exact = this.active.get(sessionId);
|
|
2121
|
-
if (exact !== undefined &&
|
|
2165
|
+
if (exact !== undefined && refMatchesActiveSession(ref, exact))
|
|
2122
2166
|
return exact;
|
|
2123
2167
|
for (const [candidateId, active] of this.active.entries()) {
|
|
2124
|
-
if (candidateId.startsWith(sessionId) &&
|
|
2168
|
+
if (candidateId.startsWith(sessionId) && refMatchesActiveSession(ref, active))
|
|
2125
2169
|
return active;
|
|
2126
2170
|
}
|
|
2127
2171
|
return undefined;
|
|
2128
2172
|
}
|
|
2129
|
-
|
|
2130
|
-
const sessionId =
|
|
2173
|
+
startupSessionForRef(ref) {
|
|
2174
|
+
const sessionId = ref.id;
|
|
2131
2175
|
const exact = this.startupSessions.get(sessionId);
|
|
2132
|
-
if (exact !== undefined &&
|
|
2176
|
+
if (exact !== undefined && refMatchesStartupSession(ref, exact))
|
|
2133
2177
|
return exact;
|
|
2134
2178
|
for (const [candidateId, session] of this.startupSessions.entries()) {
|
|
2135
|
-
if (candidateId.startsWith(sessionId) &&
|
|
2179
|
+
if (candidateId.startsWith(sessionId) && refMatchesStartupSession(ref, session))
|
|
2136
2180
|
return session;
|
|
2137
2181
|
}
|
|
2138
2182
|
return undefined;
|
|
@@ -2144,7 +2188,7 @@ export class PiSessionService {
|
|
|
2144
2188
|
* its status projection).
|
|
2145
2189
|
*/
|
|
2146
2190
|
async sessionForStatusOrDialogClose(ref) {
|
|
2147
|
-
const reachable = this.
|
|
2191
|
+
const reachable = this.activeForRef(ref)?.runtime.session ?? this.startupSessionForRef(ref);
|
|
2148
2192
|
if (reachable !== undefined)
|
|
2149
2193
|
return reachable;
|
|
2150
2194
|
return this.getOrOpen(ref);
|
|
@@ -2175,6 +2219,7 @@ export class PiSessionService {
|
|
|
2175
2219
|
sessionManager,
|
|
2176
2220
|
delegationToolsEnabled,
|
|
2177
2221
|
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
2222
|
+
...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
|
|
2178
2223
|
});
|
|
2179
2224
|
const active = { runtime, unsubscribe: noop };
|
|
2180
2225
|
let boundSession = runtime.session;
|
|
@@ -2322,14 +2367,6 @@ export class PiSessionService {
|
|
|
2322
2367
|
}
|
|
2323
2368
|
const added = this.notificationStore.addNotification(generation, message, type);
|
|
2324
2369
|
this.publishNotificationMutations(added.mutations);
|
|
2325
|
-
if (added.notification === undefined)
|
|
2326
|
-
return;
|
|
2327
|
-
this.events.publish(session.sessionId, {
|
|
2328
|
-
type: "command.output",
|
|
2329
|
-
level: type === "error" ? "error" : "info",
|
|
2330
|
-
message,
|
|
2331
|
-
notificationId: added.notification.id,
|
|
2332
|
-
});
|
|
2333
2370
|
};
|
|
2334
2371
|
// PI WEB owns the browser-facing dialog, notification, and text-formatting
|
|
2335
2372
|
// boundaries: the three dialog primitives park daemon-held Promises that
|
|
@@ -2476,7 +2513,7 @@ export class PiSessionService {
|
|
|
2476
2513
|
}
|
|
2477
2514
|
}
|
|
2478
2515
|
active.unsubscribe = session.subscribe((event) => {
|
|
2479
|
-
this.events.publish(session.sessionId, toClientEvent(event));
|
|
2516
|
+
this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel));
|
|
2480
2517
|
this.publishActivityForEvent(session, event);
|
|
2481
2518
|
const eventType = getString(event, "type");
|
|
2482
2519
|
if (eventType === "agent_end")
|
|
@@ -2957,7 +2994,7 @@ function uniqueBulkSessionRefs(refs) {
|
|
|
2957
2994
|
const seen = new Set();
|
|
2958
2995
|
const unique = [];
|
|
2959
2996
|
for (const ref of refs) {
|
|
2960
|
-
const key = `${ref.cwd
|
|
2997
|
+
const key = `${ref.cwd}\0${ref.id}`;
|
|
2961
2998
|
if (seen.has(key))
|
|
2962
2999
|
continue;
|
|
2963
3000
|
seen.add(key);
|
|
@@ -2965,16 +3002,14 @@ function uniqueBulkSessionRefs(refs) {
|
|
|
2965
3002
|
}
|
|
2966
3003
|
return unique;
|
|
2967
3004
|
}
|
|
2968
|
-
function
|
|
2969
|
-
return
|
|
3005
|
+
function bulkRefToSessionRef(ref) {
|
|
3006
|
+
return { id: ref.id, cwd: ref.cwd };
|
|
2970
3007
|
}
|
|
2971
3008
|
function findArchivedRecordForBulkRef(records, ref) {
|
|
2972
|
-
return records.find((record) =>
|
|
3009
|
+
return records.find((record) => record.cwd === ref.cwd && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
|
|
2973
3010
|
}
|
|
2974
3011
|
function findListedSessionForBulkRef(context, ref) {
|
|
2975
|
-
|
|
2976
|
-
return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
2977
|
-
return context.allSessions === undefined ? undefined : findSessionByIdOrPrefix(context.allSessions, ref.id);
|
|
3012
|
+
return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
2978
3013
|
}
|
|
2979
3014
|
function findSessionByIdOrPrefix(sessions, sessionId) {
|
|
2980
3015
|
return sessions.find((session) => session.id === sessionId) ?? sessions.find((session) => session.id.startsWith(sessionId));
|
|
@@ -3357,13 +3392,34 @@ function buildPromptOptions(behavior, images) {
|
|
|
3357
3392
|
function stringValue(value) {
|
|
3358
3393
|
return typeof value === "string" ? value : "";
|
|
3359
3394
|
}
|
|
3395
|
+
/**
|
|
3396
|
+
* Attach the thinking level in effect when an assistant message was generated,
|
|
3397
|
+
* so chat bubbles can show it next to the model. Non-assistant messages pass
|
|
3398
|
+
* through by reference; assistant messages are copied only when a level is set.
|
|
3399
|
+
* "off" is the absence of thinking, not a level worth labeling on every bubble.
|
|
3400
|
+
*/
|
|
3401
|
+
function annotateAssistantThinkingLevel(message, thinkingLevel) {
|
|
3402
|
+
if (thinkingLevel === undefined || thinkingLevel === "" || thinkingLevel === "off")
|
|
3403
|
+
return message;
|
|
3404
|
+
if (!isRecord(message) || message["role"] !== "assistant")
|
|
3405
|
+
return message;
|
|
3406
|
+
return { ...message, thinkingLevel };
|
|
3407
|
+
}
|
|
3360
3408
|
function historyMessages(session) {
|
|
3361
3409
|
const messages = [];
|
|
3410
|
+
// Pi records the initial level at session creation and every later change, so
|
|
3411
|
+
// walking the branch yields the level in effect for each assistant message.
|
|
3412
|
+
let thinkingLevel;
|
|
3362
3413
|
for (const entry of session.sessionManager.getBranch()) {
|
|
3363
3414
|
if (!isRecord(entry))
|
|
3364
3415
|
continue;
|
|
3365
3416
|
if (entry["type"] === "message")
|
|
3366
|
-
messages.push(entry["message"]);
|
|
3417
|
+
messages.push(annotateAssistantThinkingLevel(entry["message"], thinkingLevel));
|
|
3418
|
+
else if (entry["type"] === "thinking_level_change") {
|
|
3419
|
+
const level = getString(entry, "thinkingLevel");
|
|
3420
|
+
if (level !== undefined)
|
|
3421
|
+
thinkingLevel = level;
|
|
3422
|
+
}
|
|
3367
3423
|
else if (entry["type"] === "custom_message" && entry["display"] === true)
|
|
3368
3424
|
messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
|
|
3369
3425
|
else if (entry["type"] === "compaction")
|
|
@@ -3408,7 +3464,7 @@ function finalAssistantText(messages) {
|
|
|
3408
3464
|
}
|
|
3409
3465
|
return "";
|
|
3410
3466
|
}
|
|
3411
|
-
function toClientEvent(event) {
|
|
3467
|
+
function toClientEvent(event, thinkingLevel) {
|
|
3412
3468
|
const eventType = getString(event, "type");
|
|
3413
3469
|
const assistantMessageEvent = getProperty(event, "assistantMessageEvent");
|
|
3414
3470
|
if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "text_delta") {
|
|
@@ -3435,7 +3491,9 @@ function toClientEvent(event) {
|
|
|
3435
3491
|
return { type: "agent.end" };
|
|
3436
3492
|
if (eventType === "message_end") {
|
|
3437
3493
|
const message = getProperty(event, "message");
|
|
3438
|
-
|
|
3494
|
+
if (message === undefined)
|
|
3495
|
+
return { type: "message.end" };
|
|
3496
|
+
return { type: "message.end", message: annotateAssistantThinkingLevel(message, thinkingLevel) };
|
|
3439
3497
|
}
|
|
3440
3498
|
return { type: "pi.event", eventType: eventType ?? "unknown" };
|
|
3441
3499
|
}
|