@jmfederico/pi-web 1.202610.1 → 1.202610.3
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 +23 -134
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/index-BAl5IbYx.js +4429 -0
- package/dist/client/assets/vendor-editor-core-C2PJX3uW.js +10 -0
- package/dist/client/assets/{vendor-editor-languages-DOCw3vc9.js → vendor-editor-languages-CS6n1J8n.js} +1 -1
- package/dist/client/index.html +4 -48
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/pi-packages/captains-log/dist/browser/index.js +30 -32
- package/dist/pi-packages/captains-log/dist/companion.js +1 -1
- package/dist/pi-packages/captains-log/package.json +4 -4
- package/dist/pi-packages/relays/extensions/index.js +60 -0
- package/dist/pi-packages/relays/package.json +10 -1
- package/dist/pi-packages/relays/pi-web-plugin.js +31 -1
- package/dist/pi-packages/relays/prompts/relay-worktree.md +6 -16
- package/dist/pi-packages/relays/prompts/relay.md +6 -33
- package/dist/pi-packages/relays/relayDispatch.js +67 -0
- package/dist/pi-packages/relays/relayIdentity.js +37 -0
- package/dist/pi-packages/relays/relaysPanelElement.js +37 -12
- package/dist/pi-packages/relays/skills/relay/SKILL.md +24 -68
- package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +40 -219
- package/dist/pi-packages/workflows/browser/pi-web-plugin.js +33 -0
- package/dist/pi-packages/workflows/package.json +22 -0
- package/dist/pi-packages/workflows/prompts/worktree.md +12 -0
- package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +49 -51
- package/dist/pi-web-plugins/git/browser/git-panel.js +0 -1
- package/dist/pi-web-plugins/git/server-plugin.js +13 -2
- package/dist/pi-web-plugins/git/workspace-discovery-notify.cjs +14 -0
- package/dist/pi-web-plugins/git/workspaceDiscoveryHooks.js +186 -0
- package/dist/pi-web-plugins/info/infoInternals.js +3 -3
- package/dist/pi-web-plugins/info/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +1087 -422
- package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +30 -30
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
- package/dist/piWebUpdate.js +2 -2
- package/dist/piWebUpdate.js.map +1 -1
- package/dist/plugin-api.d.ts +229 -8
- package/dist/server/activity/workspaceActivityService.js +4 -1
- package/dist/server/activity/workspaceActivityService.js.map +1 -1
- package/dist/server/app.js +5 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +6 -0
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/knownAutoInstallPiPackages.js +6 -0
- package/dist/server/knownAutoInstallPiPackages.js.map +1 -1
- package/dist/server/navigationPreferenceRoutes.js +39 -0
- package/dist/server/navigationPreferenceRoutes.js.map +1 -0
- package/dist/server/plugins/projectPluginSettings.js +95 -0
- package/dist/server/plugins/projectPluginSettings.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +26 -3
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
- package/dist/server/sessiond/autoInstallPiPackages.js +3 -3
- package/dist/server/sessiond/workspaceCatalogRoutes.js +3 -1
- package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -1
- package/dist/server/sessiond/workspaceNotificationRoutes.js +41 -0
- package/dist/server/sessiond/workspaceNotificationRoutes.js.map +1 -0
- package/dist/server/sessiond.js +40 -7
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/askUserDomain.js +232 -0
- package/dist/server/sessions/askUserDomain.js.map +1 -0
- package/dist/server/sessions/askUserTool.js +15 -17
- package/dist/server/sessions/askUserTool.js.map +1 -1
- package/dist/server/sessions/dockerEnvironmentFacts.js +8 -0
- package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -1
- package/dist/server/sessions/extensionSessionDispatch.js +76 -0
- package/dist/server/sessions/extensionSessionDispatch.js.map +1 -0
- package/dist/server/sessions/globalProviderPolicy.js +71 -16
- package/dist/server/sessions/globalProviderPolicy.js.map +1 -1
- package/dist/server/sessions/messageHistoryTarget.js +61 -0
- package/dist/server/sessions/messageHistoryTarget.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +23 -24
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +759 -276
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/queuedMessageMutation.js +168 -0
- package/dist/server/sessions/queuedMessageMutation.js.map +1 -0
- package/dist/server/sessions/sessionArchiveStore.js +5 -1
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
- package/dist/server/sessions/sessionArchiveTree.js +2 -2
- package/dist/server/sessions/sessionArchiveTree.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +9 -3
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionImageDimensions.js +31 -0
- package/dist/server/sessions/sessionImageDimensions.js.map +1 -0
- package/dist/server/sessions/sessionMediaIndex.js +7 -1
- package/dist/server/sessions/sessionMediaIndex.js.map +1 -1
- package/dist/server/sessions/sessionMetadata.js +51 -0
- package/dist/server/sessions/sessionMetadata.js.map +1 -0
- package/dist/server/sessions/sessionNameGenerator.js +8 -28
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +20 -2
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionSummaryScanner.js +62 -5
- package/dist/server/sessions/sessionSummaryScanner.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +3 -1
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +25 -17
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/sessions/subsessionCompletionDelivery.js +15 -0
- package/dist/server/sessions/subsessionCompletionDelivery.js.map +1 -0
- package/dist/server/sessions/subsessionCompletionInbox.js +134 -0
- package/dist/server/sessions/subsessionCompletionInbox.js.map +1 -0
- package/dist/server/sessions/subsessionProvenance.js +27 -0
- package/dist/server/sessions/subsessionProvenance.js.map +1 -0
- package/dist/server/status/machineStatusService.js +17 -5
- package/dist/server/status/machineStatusService.js.map +1 -1
- package/dist/server/storage/navigationPreferenceStore.js +137 -0
- package/dist/server/storage/navigationPreferenceStore.js.map +1 -0
- package/dist/server/workspaces/projectPiWebConfig.js +88 -6
- package/dist/server/workspaces/projectPiWebConfig.js.map +1 -1
- package/dist/server/workspaces/workspaceChangeNotifier.js +12 -0
- package/dist/server/workspaces/workspaceChangeNotifier.js.map +1 -0
- package/dist/server/workspaces/workspaceNotificationCommand.js +64 -0
- package/dist/server/workspaces/workspaceNotificationCommand.js.map +1 -0
- package/dist/server/workspaces/workspaceProviderRegistry.js +55 -0
- package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
- package/dist/server-plugin-api.d.ts +18 -0
- package/dist/server-plugin-api.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +3 -3
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/apiTypes.js +2 -2
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/federatedRoutes.js +6 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/machineStatus.js +11 -2
- package/dist/shared/machineStatus.js.map +1 -1
- package/dist/shared/navigationPreferences.js +6 -0
- package/dist/shared/navigationPreferences.js.map +1 -0
- package/dist/shared/pluginApiTypes.d.ts +2 -0
- package/dist/shared/sessionMedia.js +6 -0
- package/dist/shared/sessionMedia.js.map +1 -1
- package/dist/shared/streamingBehavior.js +7 -0
- package/dist/shared/streamingBehavior.js.map +1 -0
- package/dist/shared/themePreference.js +23 -0
- package/dist/shared/themePreference.js.map +1 -0
- package/dist/workspaceNotificationCli.js +95 -0
- package/dist/workspaceNotificationCli.js.map +1 -0
- package/docs/config.md +111 -86
- package/docs/plugins.md +105 -30
- package/examples/session-bridge-plugin/package.json +2 -2
- package/examples/session-bridge-plugin/src/companion.ts +1 -1
- package/examples/workspace-provider-plugin/package.json +1 -1
- package/package.json +33 -33
- package/dist/client/assets/index-CBjDnBIN.js +0 -4178
- package/dist/client/assets/vendor-editor-core-YWog6mAR.js +0 -10
- package/dist/server/sessions/pendingAskStore.js +0 -275
- package/dist/server/sessions/pendingAskStore.js.map +0 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
1
3
|
import { statSync } from "node:fs";
|
|
2
4
|
import { join } from "node:path";
|
|
3
5
|
import { readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { QueueCompatibilityGuard, mirroredQueueEntries, mutableQueueAgent, mutableTextMirror, parseQueueMessageMutation, queuedMessageAttachmentCounts, queuedMessageAttachmentCount, snapshotAgentQueues } from "./queuedMessageMutation.js";
|
|
4
7
|
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEventBus, createEditToolDefinition, defineTool, hasTrustRequiringProjectResources, ProjectTrustStore, readStoredCredential, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
8
|
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
6
9
|
import { isSessionMediaId } from "../../shared/sessionMedia.js";
|
|
@@ -11,10 +14,12 @@ import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
|
11
14
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
12
15
|
import { SessionActivityMarker } from "./sessionActivityMarker.js";
|
|
13
16
|
import { projectSessionTree } from "./sessionTreeProjection.js";
|
|
17
|
+
import { messageHistoryTargetId } from "./messageHistoryTarget.js";
|
|
14
18
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
15
|
-
import {
|
|
19
|
+
import { findArchiveCandidateByExactId, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
16
20
|
import { PiSessionEventConnections } from "./piSessionEventConnections.js";
|
|
17
|
-
import {
|
|
21
|
+
import { fallbackSessionName, generateShortSessionName, requireInitialSessionName } from "./sessionNameGenerator.js";
|
|
22
|
+
import { createExtensionSessionDispatch } from "./extensionSessionDispatch.js";
|
|
18
23
|
import { computeEditPreview } from "./editPreview.js";
|
|
19
24
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
20
25
|
import { loadEffectiveProjectAttachmentsConfig } from "../workspaces/projectPiWebConfig.js";
|
|
@@ -22,15 +27,19 @@ import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
|
22
27
|
import { ASK_USER_ANSWERS_CUSTOM_TYPE, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
23
28
|
import { parseSessionDefaults, parseSessionDefaultsUpdate } from "../../shared/sessionDefaults.js";
|
|
24
29
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
30
|
+
import { parsePersistedChildSubsessionLink, parsePersistedParentSubsessionLink } from "./subsessionProvenance.js";
|
|
25
31
|
import { readSessionHeaderSummary } from "./sessionFileHeader.js";
|
|
26
32
|
import { createAskUserToolDefinition } from "./askUserTool.js";
|
|
27
|
-
import {
|
|
33
|
+
import { createAskUserOutcome, renderAskUserAnswersText, resolveAskUserRequest } from "./askUserDomain.js";
|
|
28
34
|
import { PendingExtensionDialogStore } from "./pendingExtensionDialogStore.js";
|
|
29
35
|
import { ExtensionDialogWaiters, effectiveExtensionDialogTimeoutMs, extensionDialogCancelValue } from "./extensionDialogWaiters.js";
|
|
30
36
|
import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS } from "../../config.js";
|
|
31
37
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
38
|
+
import { readSessionUiMetadata, requireSessionUiMetadata, SESSION_UI_METADATA_CUSTOM_TYPE } from "./sessionMetadata.js";
|
|
32
39
|
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
33
40
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
41
|
+
import { createSubsessionCompletionDelivery } from "./subsessionCompletionDelivery.js";
|
|
42
|
+
import { completionReceiptIds, persistedSubsessionCompletions, renderSubsessionCompletions, SUBSESSION_COMPLETION_ENTRY_TYPE, SUBSESSION_NOTIFICATION_CUSTOM_TYPE, SUBSESSION_RECEIPT_ENTRY_TYPE, SubsessionCompletionInbox } from "./subsessionCompletionInbox.js";
|
|
34
43
|
import { annotateAssistantThinkingLevel, historyMessagesFromEntries } from "./transcriptMessages.js";
|
|
35
44
|
import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
36
45
|
import { SessionNotificationStore, } from "./sessionNotificationStore.js";
|
|
@@ -290,14 +299,14 @@ function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, ini
|
|
|
290
299
|
});
|
|
291
300
|
};
|
|
292
301
|
}
|
|
293
|
-
export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions,
|
|
302
|
+
export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions, askUserEnabled = false) {
|
|
294
303
|
return [
|
|
295
304
|
createPiWebEditToolDefinition(cwd),
|
|
296
305
|
...(delegationEnabled && spawn !== undefined ? [createSpawnSessionToolDefinition(cwd, { spawn })] : []),
|
|
297
306
|
...(delegationEnabled && subsessions !== undefined ? createSubsessionToolDefinitions(cwd, subsessions) : []),
|
|
298
307
|
// Asking the user is not delegation: the questions land in the session the
|
|
299
308
|
// user is already watching, so tracked children may ask too.
|
|
300
|
-
...(
|
|
309
|
+
...(askUserEnabled ? [createAskUserToolDefinition()] : []),
|
|
301
310
|
];
|
|
302
311
|
}
|
|
303
312
|
/**
|
|
@@ -430,7 +439,7 @@ export function piWebResourceLoaderOptions(appendSystemPromptSections) {
|
|
|
430
439
|
return undefined;
|
|
431
440
|
return { appendSystemPromptOverride: (base) => [...base, ...appendSystemPromptSections] };
|
|
432
441
|
}
|
|
433
|
-
function createDefaultRuntimeFactory(sessionEvents, modelRuntime, sessionManagers, spawn, subsessions,
|
|
442
|
+
function createDefaultRuntimeFactory(sessionEvents, modelRuntime, sessionManagers, spawn, subsessions, askUserEnabled = false, appendSystemPromptSections = []) {
|
|
434
443
|
const resourceLoaderOptions = piWebResourceLoaderOptions(appendSystemPromptSections);
|
|
435
444
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
436
445
|
// PI WEB always honors pi's project-trust model. When the workspace ships
|
|
@@ -445,6 +454,13 @@ function createDefaultRuntimeFactory(sessionEvents, modelRuntime, sessionManager
|
|
|
445
454
|
// resources skip resolution entirely and are trusted, as before.
|
|
446
455
|
const eventBus = createEventBus();
|
|
447
456
|
const builtinFactories = await getBuiltinExtensionFactories();
|
|
457
|
+
// Resolved before session_start. The bridge shares the same delegation
|
|
458
|
+
// policy as core tools, including tracked-child and global disablement.
|
|
459
|
+
let dispatchEnabled = false;
|
|
460
|
+
const sessionDispatch = createExtensionSessionDispatch(cwd, {
|
|
461
|
+
...(spawn === undefined ? {} : { spawn }),
|
|
462
|
+
isEnabled: () => dispatchEnabled,
|
|
463
|
+
});
|
|
448
464
|
const projectTrustRequiring = hasTrustRequiringProjectResources(cwd);
|
|
449
465
|
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: !projectTrustRequiring });
|
|
450
466
|
// Pre-session-creation trust failures (`project_trust` handler errors)
|
|
@@ -456,7 +472,10 @@ function createDefaultRuntimeFactory(sessionEvents, modelRuntime, sessionManager
|
|
|
456
472
|
agentDir,
|
|
457
473
|
modelRuntime,
|
|
458
474
|
settingsManager,
|
|
459
|
-
resourceLoaderOptions: {
|
|
475
|
+
resourceLoaderOptions: {
|
|
476
|
+
...resourceLoaderOptions, eventBus,
|
|
477
|
+
extensionFactories: [...builtinFactories, sessionDispatch, ...(subsessions === undefined ? [] : [createSubsessionCompletionDelivery(subsessions)])],
|
|
478
|
+
},
|
|
460
479
|
...(projectTrustRequiring
|
|
461
480
|
? {
|
|
462
481
|
resourceLoaderReloadOptions: {
|
|
@@ -480,7 +499,8 @@ function createDefaultRuntimeFactory(sessionEvents, modelRuntime, sessionManager
|
|
|
480
499
|
services.diagnostics.push(...modelOptions.diagnostics);
|
|
481
500
|
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
482
501
|
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
483
|
-
|
|
502
|
+
dispatchEnabled = resolvedDelegationToolsEnabled;
|
|
503
|
+
const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions, askUserEnabled);
|
|
484
504
|
const result = await createAgentSessionFromServices({
|
|
485
505
|
services,
|
|
486
506
|
sessionManager,
|
|
@@ -537,8 +557,12 @@ export class PiSessionService {
|
|
|
537
557
|
* can anchor the selected branch on disk.
|
|
538
558
|
*/
|
|
539
559
|
this.unpersistedTreeBranchLeaves = new WeakMap();
|
|
560
|
+
/** Latest prepared text belongs to the session, never to a browser's unsent draft. */
|
|
561
|
+
this.suggestedInputs = new Map();
|
|
540
562
|
/** Counts async operations that may append an entry before they settle. */
|
|
541
563
|
this.sessionEntryMutationCounts = new WeakMap();
|
|
564
|
+
/** Exempt only a command's own prompt receipt while it invokes guarded session control. */
|
|
565
|
+
this.extensionCommandExecution = new AsyncLocalStorage();
|
|
542
566
|
this.pendingPromptEchoes = new WeakMap();
|
|
543
567
|
this.publishedAssistantPartials = new WeakMap();
|
|
544
568
|
/** Settings-wide queue preventing enabled-model read/modify/write races across sessions. */
|
|
@@ -554,7 +578,11 @@ export class PiSessionService {
|
|
|
554
578
|
/** Runtime/session-identity reservations for operations that must not overlap tree navigation. */
|
|
555
579
|
this.treeExclusiveRuntimeOperationCounts = new WeakMap();
|
|
556
580
|
this.treeExclusiveSessionOperationCounts = new Map();
|
|
557
|
-
|
|
581
|
+
/** Parent id/file identity -> one inbox shared by all completion delivery paths. */
|
|
582
|
+
this.subsessionInboxes = new Map();
|
|
583
|
+
this.subsessionStoppedParents = new WeakSet();
|
|
584
|
+
this.subsessionDeliveringParents = new WeakSet();
|
|
585
|
+
this.subsessionDeliveryScheduled = new WeakSet();
|
|
558
586
|
this.deferredGeneratedSessionNames = new WeakMap();
|
|
559
587
|
this.compactionPromptQueues = new Map();
|
|
560
588
|
this.compactionDrainTimers = new Map();
|
|
@@ -573,6 +601,9 @@ export class PiSessionService {
|
|
|
573
601
|
* one (not for standalone commands or compaction); completion disarms it.
|
|
574
602
|
*/
|
|
575
603
|
this.subsessionNotifyArmed = new Map();
|
|
604
|
+
this.queueCompatibility = new QueueCompatibilityGuard((session, err) => {
|
|
605
|
+
this.logger.info({ sessionId: session.sessionId, err }, "Disabled SDK-dependent queue controls after an incompatible queue interface");
|
|
606
|
+
});
|
|
576
607
|
this.notificationGenerationBySession = new WeakMap();
|
|
577
608
|
/** Last idle-poll transcript file resolution per runtime, throttled; entries die with their runtime. */
|
|
578
609
|
this.idleSessionFileResolutions = new WeakMap();
|
|
@@ -595,7 +626,6 @@ export class PiSessionService {
|
|
|
595
626
|
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
596
627
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
597
628
|
this.onUnreadChanged = deps.onUnreadChanged;
|
|
598
|
-
this.pendingAskStore = deps.pendingAskStore ?? new PendingAskStore();
|
|
599
629
|
this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
|
|
600
630
|
this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
|
|
601
631
|
this.catalogRefreshStatus = deps.catalogRefreshStatus;
|
|
@@ -609,9 +639,11 @@ export class PiSessionService {
|
|
|
609
639
|
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.sessionEvents, this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
610
640
|
spawn: (input) => this.spawnSubsession(input),
|
|
611
641
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
642
|
+
join: (parentSessionId, parentSessionFile, toolCallId) => this.joinSubsessions(parentSessionId, parentSessionFile, toolCallId),
|
|
643
|
+
prepare: (parentSessionId, parentSessionFile) => this.prepareSubsessionDelivery(parentSessionId, parentSessionFile),
|
|
644
|
+
check: (parentSessionId, sessionId, parentSessionFile, toolCallId) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile, toolCallId),
|
|
645
|
+
read: (parentSessionId, sessionId, query, parentSessionFile, toolCallId) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile, toolCallId),
|
|
646
|
+
}, deps.askUserEnabled === true, deps.appendSystemPromptSections ?? []);
|
|
615
647
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
616
648
|
this.workspaceActivity = deps.workspaceActivity;
|
|
617
649
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
@@ -728,12 +760,12 @@ export class PiSessionService {
|
|
|
728
760
|
const activeSessions = Array.from(new Set(this.active.values()));
|
|
729
761
|
for (const active of activeSessions) {
|
|
730
762
|
this.forgetUnreadActivity(active.runtime.session);
|
|
731
|
-
this.pendingAskStore.forgetSession(active.runtime.session.sessionId);
|
|
732
763
|
this.endSessionExtensionDialogs(active.runtime.session.sessionId);
|
|
733
764
|
}
|
|
734
765
|
this.active.clear();
|
|
735
766
|
this.pendingSessionOpens.clear();
|
|
736
767
|
this.startupSessions.clear();
|
|
768
|
+
this.suggestedInputs.clear();
|
|
737
769
|
this.activities.clear();
|
|
738
770
|
this.compactionPromptQueues.clear();
|
|
739
771
|
this.authLossWarnings.clear();
|
|
@@ -742,6 +774,7 @@ export class PiSessionService {
|
|
|
742
774
|
this.subsessionLinks.clear();
|
|
743
775
|
this.subsessionHydratedParents.clear();
|
|
744
776
|
this.subsessionNotifyArmed.clear();
|
|
777
|
+
this.subsessionInboxes.clear();
|
|
745
778
|
this.notificationStore.clearAll("service-dispose");
|
|
746
779
|
await Promise.all(activeSessions.map(async (active) => {
|
|
747
780
|
active.unsubscribe();
|
|
@@ -754,6 +787,7 @@ export class PiSessionService {
|
|
|
754
787
|
await active.runtime.dispose();
|
|
755
788
|
}
|
|
756
789
|
})).finally(async () => {
|
|
790
|
+
this.extensionCommandExecution.disable();
|
|
757
791
|
this.events.mediaIndex.clear();
|
|
758
792
|
await this.activityMarker.dispose();
|
|
759
793
|
});
|
|
@@ -767,7 +801,29 @@ export class PiSessionService {
|
|
|
767
801
|
for (const record of archivedForCwd) {
|
|
768
802
|
this.publishNotificationMutations(this.notificationStore.clearSession(record.sessionId, "archive-reconcile"));
|
|
769
803
|
}
|
|
770
|
-
const
|
|
804
|
+
const unarchivedById = new Map(sessions.filter((session) => !archivedById.has(session.id))
|
|
805
|
+
.map((session) => [session.id, clientSessionFromListEntry(session)]));
|
|
806
|
+
// Pi can fork before the first conversational entry without creating a file.
|
|
807
|
+
// Enumerate already-hosted runtimes only; listing must not open cold sessions.
|
|
808
|
+
for (const active of new Set(this.active.values())) {
|
|
809
|
+
const session = active.runtime.session;
|
|
810
|
+
if (!cwdPathsEqual(session.sessionManager.getCwd(), cwd) || archivedById.has(session.sessionId))
|
|
811
|
+
continue;
|
|
812
|
+
const listed = unarchivedById.get(session.sessionId);
|
|
813
|
+
const tracked = this.subsessionLinkForActiveChild(session) !== undefined;
|
|
814
|
+
if (listed !== undefined) {
|
|
815
|
+
// Match the file too: a clone with the same id must not inherit a
|
|
816
|
+
// live runtime's classification. Disk rows otherwise use the scanner.
|
|
817
|
+
if (tracked && session.sessionFile !== undefined && sessionPathsEqual(listed.path, session.sessionFile))
|
|
818
|
+
listed.agentSubsession = true;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const info = clientSessionFromActiveSession(session);
|
|
822
|
+
if (tracked)
|
|
823
|
+
info.agentSubsession = true;
|
|
824
|
+
unarchivedById.set(session.sessionId, info);
|
|
825
|
+
}
|
|
826
|
+
const unarchivedSessions = [...unarchivedById.values()].sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified));
|
|
771
827
|
const reconcilableSessionIds = this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById);
|
|
772
828
|
this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds);
|
|
773
829
|
await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
|
|
@@ -815,29 +871,37 @@ export class PiSessionService {
|
|
|
815
871
|
}
|
|
816
872
|
}
|
|
817
873
|
async startSession(cwd, options) {
|
|
818
|
-
const
|
|
874
|
+
const metadata = options.metadata === undefined ? undefined : requireSessionUiMetadata(options.metadata);
|
|
875
|
+
const name = options.name === undefined ? undefined : requireInitialSessionName(options.name);
|
|
876
|
+
const manager = this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession });
|
|
877
|
+
if (metadata !== undefined) {
|
|
878
|
+
if (manager.appendCustomEntry === undefined)
|
|
879
|
+
throw new Error("Session manager does not support UI metadata initialization");
|
|
880
|
+
manager.appendCustomEntry(SESSION_UI_METADATA_CUSTOM_TYPE, { version: 1, metadata });
|
|
881
|
+
}
|
|
882
|
+
if (name !== undefined) {
|
|
883
|
+
if (manager.appendSessionInfo === undefined)
|
|
884
|
+
throw new Error("Session manager does not support initial naming");
|
|
885
|
+
manager.appendSessionInfo(name);
|
|
886
|
+
}
|
|
887
|
+
const active = await this.create(manager, cwd, {
|
|
819
888
|
startupIntent: "create",
|
|
820
889
|
...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
|
|
821
890
|
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
822
891
|
...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
|
|
823
892
|
...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
|
|
824
893
|
});
|
|
825
|
-
return
|
|
894
|
+
return options.deferAnnouncement === true
|
|
895
|
+
? clientSessionFromActiveSession(active.runtime.session)
|
|
896
|
+
: this.announceCreatedSession(active, cwd, options.parentSession);
|
|
826
897
|
}
|
|
827
|
-
announceCreatedSession(active, cwd, parentSession) {
|
|
898
|
+
announceCreatedSession(active, cwd, parentSession, agentSubsession) {
|
|
828
899
|
const { session } = active.runtime;
|
|
829
900
|
const created = {
|
|
830
|
-
|
|
831
|
-
path: session.sessionFile ?? "",
|
|
901
|
+
...clientSessionFromActiveSession(session),
|
|
832
902
|
cwd,
|
|
833
|
-
persisted: sessionFileExists(session.sessionFile),
|
|
834
|
-
created: new Date().toISOString(),
|
|
835
|
-
modified: new Date().toISOString(),
|
|
836
|
-
messageCount: session.messages.length,
|
|
837
|
-
firstMessage: "",
|
|
838
|
-
// Include the parent so listeners can nest the new session in the tree
|
|
839
|
-
// immediately, instead of showing it flat until the next reload.
|
|
840
903
|
...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
|
|
904
|
+
...(agentSubsession === true ? { agentSubsession: true } : {}),
|
|
841
905
|
};
|
|
842
906
|
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
843
907
|
// session to their list without a manual reload.
|
|
@@ -861,6 +925,8 @@ export class PiSessionService {
|
|
|
861
925
|
? input.model
|
|
862
926
|
: await this.resolveSpawnModel({ id: input.spawningSessionId, cwd: input.spawningCwd }, input.modelSpec);
|
|
863
927
|
const created = await this.start(decision.cwd, {
|
|
928
|
+
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
|
929
|
+
...(input.name === undefined ? {} : { name: input.name }),
|
|
864
930
|
...(model === undefined ? {} : { initialModel: model }),
|
|
865
931
|
...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
|
|
866
932
|
});
|
|
@@ -902,6 +968,7 @@ export class PiSessionService {
|
|
|
902
968
|
...(model === undefined ? {} : { initialModel: model }),
|
|
903
969
|
...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
|
|
904
970
|
creationProvenance: "tracked-subsession",
|
|
971
|
+
deferAnnouncement: true,
|
|
905
972
|
});
|
|
906
973
|
const modelUsed = this.active.get(created.id)?.runtime.session.model;
|
|
907
974
|
const parentSessionFile = nonEmptyString(input.parentSessionFile);
|
|
@@ -915,6 +982,13 @@ export class PiSessionService {
|
|
|
915
982
|
await this.registerVerifiedSubsession(link);
|
|
916
983
|
this.persistSubsessionLink(link);
|
|
917
984
|
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
985
|
+
const active = this.active.get(created.id);
|
|
986
|
+
if (active === undefined)
|
|
987
|
+
throw new Error("Tracked subsession disappeared during creation");
|
|
988
|
+
// Freshly created children are known by this exact runtime/file link even
|
|
989
|
+
// before Pi flushes its buffered custom entries. Reopened sessions only
|
|
990
|
+
// enter this registry after reciprocal persisted provenance validation.
|
|
991
|
+
this.announceCreatedSession(active, decision.cwd, input.parentSessionFile, this.subsessionLinkForActiveChild(active.runtime.session) !== undefined);
|
|
918
992
|
await this.prompt(created, input.prompt);
|
|
919
993
|
this.logger.info({ parentSessionId: input.parentSessionId, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_subsession started a tracked child session");
|
|
920
994
|
return {
|
|
@@ -1055,83 +1129,30 @@ export class PiSessionService {
|
|
|
1055
1129
|
throw unknownSpawnModelError(modelSpec);
|
|
1056
1130
|
return model;
|
|
1057
1131
|
}
|
|
1058
|
-
/**
|
|
1059
|
-
* Register the question set an agent wants the user to answer as the session's
|
|
1060
|
-
* open ask. Deliberately does not wait for the user: `ask_user` terminates the
|
|
1061
|
-
* run and the submitted answers come back later as a follow-up message.
|
|
1062
|
-
*
|
|
1063
|
-
* Rejected question sets throw {@link PendingAskValidationError}, which the
|
|
1064
|
-
* agent loop reports to the model as an error tool result.
|
|
1065
|
-
*/
|
|
1066
|
-
// eslint-disable-next-line @typescript-eslint/require-await -- async so a rejected question set becomes a rejection rather than a synchronous throw from a promise-returning method.
|
|
1067
|
-
async openAsk(input) {
|
|
1068
|
-
const result = this.pendingAskStore.open(input);
|
|
1069
|
-
// A supersede closes the earlier ask, so the browsers watching it must hear
|
|
1070
|
-
// that before they hear about its replacement.
|
|
1071
|
-
if (result.superseded !== undefined)
|
|
1072
|
-
this.publishAskClosed(input.sessionId, result.superseded);
|
|
1073
|
-
this.events.publish(input.sessionId, { type: "ask.opened", ask: result.ask });
|
|
1074
|
-
this.publishStatusForSessionId(input.sessionId);
|
|
1075
|
-
return result;
|
|
1076
|
-
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Record the user's answers to the session's open ask and hand them to the
|
|
1079
|
-
* model. The answers travel as a system-authored custom message rather than a
|
|
1080
|
-
* user message, so they are not attributed to the human in the transcript;
|
|
1081
|
-
* they still wake an idle session (`triggerTurn`) and queue behind in-flight
|
|
1082
|
-
* work (`deliverAs: "followUp"`), which is how the run that `ask_user`
|
|
1083
|
-
* terminated continues.
|
|
1084
|
-
*/
|
|
1132
|
+
/** Reply to a durable question entry, even after later conversation or replies. */
|
|
1085
1133
|
async submitAsk(ref, askId, submission) {
|
|
1086
1134
|
await this.assertWritable(ref);
|
|
1087
1135
|
const session = await this.getOrOpen(ref);
|
|
1088
|
-
// Checked before the store closes the ask so a refused delivery cannot
|
|
1089
|
-
// discard answers the user already submitted.
|
|
1090
1136
|
this.assertTreeNavigationInactive(session, "answer questions");
|
|
1091
|
-
|
|
1137
|
+
const ask = resolveAskUserRequest(await this.readableSessionBranch(ref, session), askId);
|
|
1138
|
+
const outcome = createAskUserOutcome(ask, submission, this.now().toISOString());
|
|
1139
|
+
return this.deliverAskOutcome(session, outcome);
|
|
1092
1140
|
}
|
|
1093
|
-
/**
|
|
1094
|
-
* Close the open ask without answers. The model is still told, naming every
|
|
1095
|
-
* question as unanswered: it was promised a follow-up message and would
|
|
1096
|
-
* otherwise wait for one that never comes.
|
|
1097
|
-
*/
|
|
1141
|
+
/** Skip is also a transcript reply; it does not invalidate future answers. */
|
|
1098
1142
|
async cancelAsk(ref, askId) {
|
|
1099
1143
|
await this.assertWritable(ref);
|
|
1100
1144
|
const session = await this.getOrOpen(ref);
|
|
1101
|
-
this.assertTreeNavigationInactive(session, "
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
*/
|
|
1110
|
-
async closeAsk(session, result) {
|
|
1111
|
-
if (result.status === "stale")
|
|
1112
|
-
return { result: "stale", sessionStatus: this.statusFromSession(session) };
|
|
1113
|
-
const { outcome } = result;
|
|
1114
|
-
this.publishAskClosed(session.sessionId, outcome);
|
|
1145
|
+
this.assertTreeNavigationInactive(session, "skip questions");
|
|
1146
|
+
const ask = resolveAskUserRequest(await this.readableSessionBranch(ref, session), askId);
|
|
1147
|
+
return this.deliverAskOutcome(session, createAskUserOutcome(ask, undefined, this.now().toISOString()));
|
|
1148
|
+
}
|
|
1149
|
+
async deliverAskOutcome(session, outcome) {
|
|
1150
|
+
// Pi persists/displays the structured message and wakes an idle agent, or
|
|
1151
|
+
// queues it behind in-flight work. Ordinary entry-mutation protection still
|
|
1152
|
+
// applies, but there is no answer-specific stale/duplicate gate.
|
|
1115
1153
|
await this.runSessionEntryMutation(session, "deliver answers to your questions", () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: true, deliverAs: "followUp" }));
|
|
1116
1154
|
this.publishStatus(session);
|
|
1117
|
-
return
|
|
1118
|
-
}
|
|
1119
|
-
publishAskClosed(sessionId, outcome) {
|
|
1120
|
-
this.events.publish(sessionId, { type: "ask.closed", askId: outcome.askId, reason: outcome.reason });
|
|
1121
|
-
}
|
|
1122
|
-
/**
|
|
1123
|
-
* Void the session's open ask because the user sent a chat message instead of
|
|
1124
|
-
* answering it. Every browser closes the card as cancelled, and the model is
|
|
1125
|
-
* told — without being woken — so the notice rides into the turn the message
|
|
1126
|
-
* itself triggers rather than becoming a turn of its own.
|
|
1127
|
-
*/
|
|
1128
|
-
async voidOpenAskForUserMessage(session) {
|
|
1129
|
-
const outcome = this.pendingAskStore.cancelOpen(session.sessionId);
|
|
1130
|
-
if (outcome === undefined)
|
|
1131
|
-
return;
|
|
1132
|
-
this.publishAskClosed(session.sessionId, outcome);
|
|
1133
|
-
await this.runSessionEntryMutation(session, "void the open questions", () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: false, deliverAs: "followUp" }));
|
|
1134
|
-
this.publishStatus(session);
|
|
1155
|
+
return this.statusFromSession(session);
|
|
1135
1156
|
}
|
|
1136
1157
|
/**
|
|
1137
1158
|
* Record the user's answer to an open extension dialog and resolve the
|
|
@@ -1280,27 +1301,120 @@ export class PiSessionService {
|
|
|
1280
1301
|
const authorizedChildIds = [...childIds].filter((childId) => this.subsessionLinkBelongsToParent(parentSessionId, parentFile, childId));
|
|
1281
1302
|
return Promise.all(authorizedChildIds.map(async (childId) => ({ sessionId: childId, ...(await this.subsessionSummaryFields(childId)) })));
|
|
1282
1303
|
}
|
|
1304
|
+
/** Collect ready answers before deciding whether the parent needs to wait. */
|
|
1305
|
+
async joinSubsessions(parentSessionId, parentSessionFile, toolCallId) {
|
|
1306
|
+
const subsessions = await this.listSubsessions(parentSessionId, parentSessionFile);
|
|
1307
|
+
this.reconcileSubsessionCompletions(parentSessionId, parentSessionFile);
|
|
1308
|
+
const completions = this.subsessionInbox(parentSessionId, parentSessionFile).pending();
|
|
1309
|
+
this.receiveSubsessionCompletions(parentSessionId, parentSessionFile, completions, toolCallId);
|
|
1310
|
+
return { subsessions, completions };
|
|
1311
|
+
}
|
|
1283
1312
|
/** Status and final result of a subsession, scoped to the caller's children. */
|
|
1284
|
-
async checkSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
1313
|
+
async checkSubsession(parentSessionId, sessionId, parentSessionFile, toolCallId) {
|
|
1285
1314
|
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile);
|
|
1315
|
+
// Finalize the same completion even if its event/heartbeat notification has
|
|
1316
|
+
// not run yet. Inspection must not leave a future notice armed for this run.
|
|
1317
|
+
this.reconcileSubsessionCompletion(session);
|
|
1318
|
+
const status = this.subsessionStatus(session);
|
|
1286
1319
|
const messages = historyMessages(session);
|
|
1320
|
+
const completion = this.subsessionCompletionForOutput(parentSessionId, parentSessionFile, session, messages);
|
|
1321
|
+
if (status !== "working" && completion !== undefined)
|
|
1322
|
+
this.receiveSubsessionCompletions(parentSessionId, parentSessionFile, [completion], toolCallId);
|
|
1287
1323
|
return {
|
|
1288
1324
|
sessionId,
|
|
1289
1325
|
cwd: session.sessionManager.getCwd(),
|
|
1290
|
-
status
|
|
1326
|
+
status,
|
|
1291
1327
|
finalText: finalAssistantText(messages),
|
|
1292
1328
|
messageCount: messages.length,
|
|
1329
|
+
...(status === "working" || completion?.errorDetail === undefined ? {} : { errorDetail: completion.errorDetail }),
|
|
1293
1330
|
};
|
|
1294
1331
|
}
|
|
1295
|
-
/** Filtered
|
|
1296
|
-
async readSubsession(parentSessionId, sessionId, query, parentSessionFile) {
|
|
1332
|
+
/** Filtered transcript reads acknowledge only the final answer actually returned. */
|
|
1333
|
+
async readSubsession(parentSessionId, sessionId, query, parentSessionFile, toolCallId) {
|
|
1297
1334
|
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile);
|
|
1298
|
-
|
|
1335
|
+
this.reconcileSubsessionCompletion(session);
|
|
1336
|
+
const messages = historyMessages(session);
|
|
1337
|
+
const view = buildTranscriptView(messages, query);
|
|
1338
|
+
const status = this.subsessionStatus(session);
|
|
1339
|
+
const completion = this.subsessionCompletionForOutput(parentSessionId, parentSessionFile, session, messages);
|
|
1340
|
+
const errorDetail = status === "working" ? undefined : completion?.errorDetail;
|
|
1341
|
+
if (status !== "working" && completion !== undefined
|
|
1342
|
+
&& (transcriptReturnsFinalOutput(messages, view, completion.finalText) || (completion.finalText === "" && errorDetail !== undefined))) {
|
|
1343
|
+
this.receiveSubsessionCompletions(parentSessionId, parentSessionFile, [completion], toolCallId);
|
|
1344
|
+
}
|
|
1345
|
+
return { sessionId, cwd: session.sessionManager.getCwd(), status, ...view, ...(errorDetail === undefined ? {} : { errorDetail }) };
|
|
1346
|
+
}
|
|
1347
|
+
subsessionInbox(parentSessionId, parentSessionFile) {
|
|
1348
|
+
const file = nonEmptyString(parentSessionFile) ?? nonEmptyString(this.active.get(parentSessionId)?.runtime.session.sessionFile);
|
|
1349
|
+
const key = subsessionHydratedParentKey(parentSessionId, file);
|
|
1350
|
+
let inbox = this.subsessionInboxes.get(key);
|
|
1351
|
+
if (inbox === undefined) {
|
|
1352
|
+
inbox = new SubsessionCompletionInbox();
|
|
1353
|
+
this.subsessionInboxes.set(key, inbox);
|
|
1354
|
+
}
|
|
1355
|
+
return inbox;
|
|
1356
|
+
}
|
|
1357
|
+
restoreSubsessionReceipts(session, releaseClaims = true) {
|
|
1358
|
+
this.subsessionInbox(session.sessionId, session.sessionFile).restoreReceipts(session.sessionManager.getBranch(), releaseClaims);
|
|
1359
|
+
}
|
|
1360
|
+
subsessionCompletionForOutput(parentId, parentFile, child, messages) {
|
|
1361
|
+
const branch = persistedSubsessionCompletions(child.sessionManager.getBranch()).filter(({ sessionId }) => sessionId === child.sessionId);
|
|
1362
|
+
// A child can be rewound independently of its parent. Never acknowledge an
|
|
1363
|
+
// abandoned branch's newer result while returning the current branch's text.
|
|
1364
|
+
const completion = branch.at(-1) ?? (child.sessionManager.appendCustomEntry === undefined
|
|
1365
|
+
? this.subsessionInbox(parentId, parentFile).latest(child.sessionId) : undefined);
|
|
1366
|
+
return completion?.finalText === finalAssistantText(messages) ? completion : undefined;
|
|
1367
|
+
}
|
|
1368
|
+
receiveSubsessionCompletions(parentId, parentFile, completions, toolCallId) {
|
|
1369
|
+
if (completions.length === 0)
|
|
1370
|
+
return;
|
|
1371
|
+
const parent = this.active.get(parentId)?.runtime.session;
|
|
1372
|
+
if (parent === undefined || (parentFile !== undefined && !sessionFileMatches(parent, parentFile))) {
|
|
1373
|
+
throw new Error(`Parent session ${parentId} is not available to receive subsession results`);
|
|
1374
|
+
}
|
|
1375
|
+
const inbox = this.subsessionInbox(parentId, parentFile);
|
|
1376
|
+
const completionIds = completions.map(({ completionId }) => completionId).filter((id) => !inbox.hasReceived(id));
|
|
1377
|
+
if (completionIds.length === 0)
|
|
1378
|
+
return;
|
|
1379
|
+
// Persist before removing unread results. A storage failure leaves the
|
|
1380
|
+
// answer unread and fails the tool instead of losing the notification.
|
|
1381
|
+
this.assertTreeNavigationInactive(parent, "receive subsession results");
|
|
1382
|
+
parent.sessionManager.appendCustomEntry?.(SUBSESSION_RECEIPT_ENTRY_TYPE, { completionIds, ...(toolCallId === undefined ? {} : { toolCallId }) });
|
|
1383
|
+
inbox.receive(completionIds);
|
|
1384
|
+
}
|
|
1385
|
+
reconcileSubsessionCompletions(parentId, parentFile) {
|
|
1386
|
+
for (const childId of this.subsessionChildren.get(parentId) ?? []) {
|
|
1387
|
+
if (!this.subsessionLinkBelongsToParent(parentId, nonEmptyString(parentFile), childId))
|
|
1388
|
+
continue;
|
|
1389
|
+
const link = this.subsessionLinks.get(childId);
|
|
1390
|
+
const child = link === undefined ? undefined : this.activeChildForSubsessionLink(link)?.runtime.session;
|
|
1391
|
+
if (child !== undefined)
|
|
1392
|
+
this.reconcileSubsessionCompletion(child);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
reconcileSubsessionCompletion(session) {
|
|
1396
|
+
this.updateSubsessionTracking(session);
|
|
1397
|
+
if (!this.hasActiveWork(session) && this.subsessionNotifyArmed.get(session.sessionId) === true) {
|
|
1398
|
+
throw new Error(`Could not record completion of subsession ${session.sessionId}; its result remains unread. Retry after fixing session storage.`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
/** Claims commit on entry_appended (boundary drafts) or message_end (idle wakes). */
|
|
1402
|
+
async prepareSubsessionDelivery(parentId, parentFile) {
|
|
1403
|
+
await this.hydrateSubsessionsForParent(parentId, nonEmptyString(parentFile));
|
|
1404
|
+
const parent = this.active.get(parentId)?.runtime.session;
|
|
1405
|
+
if (parent === undefined || (parentFile !== undefined && !sessionFileMatches(parent, parentFile)))
|
|
1406
|
+
return undefined;
|
|
1407
|
+
// Tool receipts are intents until the successful (possibly nested) result
|
|
1408
|
+
// is persisted. Reconcile those intents without releasing in-flight drafts.
|
|
1409
|
+
this.restoreSubsessionReceipts(parent, false);
|
|
1410
|
+
this.reconcileSubsessionCompletions(parentId, parentFile);
|
|
1411
|
+
const completions = this.subsessionInbox(parentId, parentFile).claim();
|
|
1412
|
+
if (completions.length === 0)
|
|
1413
|
+
return undefined;
|
|
1299
1414
|
return {
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
...view,
|
|
1415
|
+
type: "custom_message", customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, display: true,
|
|
1416
|
+
content: renderSubsessionCompletions(completions, this.workingSubsessionIds(parentId)),
|
|
1417
|
+
details: { sessionId: completions[0]?.sessionId, completionIds: completions.map(({ completionId }) => completionId) },
|
|
1304
1418
|
};
|
|
1305
1419
|
}
|
|
1306
1420
|
/** Open a session after verifying it is one of the caller's tracked children. */
|
|
@@ -1354,6 +1468,14 @@ export class PiSessionService {
|
|
|
1354
1468
|
this.subsessionLinks.set(childSessionId, link);
|
|
1355
1469
|
if (!this.subsessionNotifyArmed.has(childSessionId))
|
|
1356
1470
|
this.subsessionNotifyArmed.set(childSessionId, false);
|
|
1471
|
+
const childManager = this.activeChildForSubsessionLink(link)?.runtime.session.sessionManager
|
|
1472
|
+
?? (link.childSessionFile === undefined ? undefined : this.sessionManager.open(link.childSessionFile));
|
|
1473
|
+
if (childManager !== undefined) {
|
|
1474
|
+
for (const completion of persistedSubsessionCompletions(childManager.getEntries?.() ?? childManager.getBranch())) {
|
|
1475
|
+
if (completion.sessionId === childSessionId)
|
|
1476
|
+
this.subsessionInbox(parentSessionId, link.parentSessionFile).record(completion);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1357
1479
|
const cwd = this.cwdForVerifiedSubsession(link);
|
|
1358
1480
|
await this.publishUnreadMutations(this.unreadStore.excludeSession(childSessionId, cwd));
|
|
1359
1481
|
}
|
|
@@ -1378,6 +1500,8 @@ export class PiSessionService {
|
|
|
1378
1500
|
this.subsessionParents.delete(childSessionId);
|
|
1379
1501
|
this.subsessionLinks.delete(childSessionId);
|
|
1380
1502
|
this.subsessionNotifyArmed.delete(childSessionId);
|
|
1503
|
+
for (const inbox of this.subsessionInboxes.values())
|
|
1504
|
+
inbox.forgetChild(childSessionId);
|
|
1381
1505
|
if (parentSessionId === undefined)
|
|
1382
1506
|
return;
|
|
1383
1507
|
const children = this.subsessionChildren.get(parentSessionId);
|
|
@@ -1534,7 +1658,7 @@ export class PiSessionService {
|
|
|
1534
1658
|
* working. Prompt failures before agent_start explicitly arm the same latch.
|
|
1535
1659
|
* Standalone commands and compactions must not arm a completion notice.
|
|
1536
1660
|
*/
|
|
1537
|
-
updateSubsessionTracking(session, agentStarted = false) {
|
|
1661
|
+
updateSubsessionTracking(session, agentStarted = false, agentSettled = false) {
|
|
1538
1662
|
const link = this.subsessionLinkForActiveChild(session);
|
|
1539
1663
|
if (link === undefined)
|
|
1540
1664
|
return;
|
|
@@ -1543,22 +1667,31 @@ export class PiSessionService {
|
|
|
1543
1667
|
this.subsessionNotifyArmed.set(childId, true);
|
|
1544
1668
|
return;
|
|
1545
1669
|
}
|
|
1546
|
-
|
|
1670
|
+
// Native settlement is authoritative even while the host prompt Promise's
|
|
1671
|
+
// mutation receipt is unwinding. Capture now, before another run can start.
|
|
1672
|
+
if (!agentSettled && this.hasActiveWork(session))
|
|
1547
1673
|
return;
|
|
1548
1674
|
if (this.subsessionNotifyArmed.get(childId) !== true)
|
|
1549
1675
|
return;
|
|
1550
|
-
this.subsessionNotifyArmed.set(childId, false);
|
|
1551
1676
|
const activity = this.activities.get(childId);
|
|
1552
|
-
const
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1677
|
+
const messages = historyMessages(session);
|
|
1678
|
+
const completion = {
|
|
1679
|
+
completionId: randomUUID(), sessionId: childId, cwd: session.sessionManager.getCwd(),
|
|
1680
|
+
status: activity?.phase === "error" ? "error" : "idle",
|
|
1681
|
+
finalText: finalAssistantText(messages), messageCount: messages.length,
|
|
1682
|
+
...(activity?.phase !== "error" || activity.detail === undefined ? {} : { errorDetail: activity.detail }),
|
|
1683
|
+
};
|
|
1684
|
+
try {
|
|
1685
|
+
session.sessionManager.appendCustomEntry?.(SUBSESSION_COMPLETION_ENTRY_TYPE, completion);
|
|
1686
|
+
}
|
|
1687
|
+
catch (error) {
|
|
1688
|
+
// Keep the latch armed so a later reconciliation can retry persistence.
|
|
1689
|
+
this.logSubsessionNotificationFailure(link.parentSessionId, childId, error);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
this.subsessionNotifyArmed.set(childId, false);
|
|
1693
|
+
this.subsessionInbox(link.parentSessionId, link.parentSessionFile).record(completion);
|
|
1694
|
+
void this.wakeParentForSubsession(link.parentSessionId, childId);
|
|
1562
1695
|
}
|
|
1563
1696
|
async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
|
|
1564
1697
|
const link = this.subsessionLinks.get(childSessionId);
|
|
@@ -1576,31 +1709,60 @@ export class PiSessionService {
|
|
|
1576
1709
|
const sessionManager = this.sessionManager.open(parentSessionFile);
|
|
1577
1710
|
return (await this.create(sessionManager, sessionManager.getCwd())).runtime.session;
|
|
1578
1711
|
}
|
|
1579
|
-
|
|
1580
|
-
* Deliver a subsession-completion notice to the parent as a system-authored
|
|
1581
|
-
* custom message rather than a user message, so it is not attributed to the
|
|
1582
|
-
* human in the transcript. It still wakes an idle parent (`triggerTurn`) and
|
|
1583
|
-
* queues behind in-flight work (`deliverAs: "followUp"`), preserving the
|
|
1584
|
-
* established "queue if busy, send and act if idle" behavior.
|
|
1585
|
-
*/
|
|
1586
|
-
async notifyParentOfSubsession(parentId, childId, text) {
|
|
1712
|
+
async wakeParentForSubsession(parentId, childId) {
|
|
1587
1713
|
try {
|
|
1588
1714
|
const session = await this.getOrOpenParentForSubsession(parentId, childId);
|
|
1589
|
-
|
|
1590
|
-
const pending = this.deferredSubsessionNotifications.get(session) ?? [];
|
|
1591
|
-
pending.push({ parentId, childId, text });
|
|
1592
|
-
this.deferredSubsessionNotifications.set(session, pending);
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
await this.deliverSubsessionNotification(session, { parentId, childId, text });
|
|
1715
|
+
this.scheduleSubsessionDelivery(session);
|
|
1596
1716
|
}
|
|
1597
1717
|
catch (error) {
|
|
1598
1718
|
this.logSubsessionNotificationFailure(parentId, childId, error);
|
|
1599
1719
|
}
|
|
1600
1720
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
this.
|
|
1721
|
+
/** Busy parents keep results in the inbox; never enqueue a second copy in Pi. */
|
|
1722
|
+
async deliverPendingSubsessionCompletions(session) {
|
|
1723
|
+
if (this.hasActiveWork(session) || !this.isCurrentActiveSession(session) || this.subsessionStoppedParents.has(session))
|
|
1724
|
+
return;
|
|
1725
|
+
const inbox = this.subsessionInbox(session.sessionId, session.sessionFile);
|
|
1726
|
+
const completions = inbox.claim();
|
|
1727
|
+
if (completions.length === 0)
|
|
1728
|
+
return;
|
|
1729
|
+
const completionIds = completions.map(({ completionId }) => completionId);
|
|
1730
|
+
this.subsessionDeliveringParents.add(session);
|
|
1731
|
+
let delivered = false;
|
|
1732
|
+
try {
|
|
1733
|
+
await this.runSessionEntryMutation(session, "deliver subsession results", () => session.sendCustomMessage({
|
|
1734
|
+
customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, display: true,
|
|
1735
|
+
content: renderSubsessionCompletions(completions, this.workingSubsessionIds(session.sessionId)),
|
|
1736
|
+
details: { sessionId: completions[0]?.sessionId, completionIds },
|
|
1737
|
+
}, { triggerTurn: true }));
|
|
1738
|
+
inbox.receive(completionIds);
|
|
1739
|
+
delivered = true;
|
|
1740
|
+
}
|
|
1741
|
+
catch (error) {
|
|
1742
|
+
this.subsessionStoppedParents.add(session);
|
|
1743
|
+
throw error;
|
|
1744
|
+
}
|
|
1745
|
+
finally {
|
|
1746
|
+
inbox.release(completionIds);
|
|
1747
|
+
this.subsessionDeliveringParents.delete(session);
|
|
1748
|
+
if (delivered)
|
|
1749
|
+
this.scheduleSubsessionDelivery(session);
|
|
1750
|
+
this.publishStatus(session);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
scheduleSubsessionDelivery(session) {
|
|
1754
|
+
if (this.subsessionDeliveryScheduled.has(session) || this.subsessionInbox(session.sessionId, session.sessionFile).pending().length === 0)
|
|
1755
|
+
return;
|
|
1756
|
+
this.subsessionDeliveryScheduled.add(session);
|
|
1757
|
+
// Settlement can precede the host prompt-mutation receipt's finally block.
|
|
1758
|
+
// Recheck on the next tick, closing the yield/child-completion race without
|
|
1759
|
+
// restarting a stopped run or putting rendered answers in a follow-up queue.
|
|
1760
|
+
setTimeout(() => {
|
|
1761
|
+
this.subsessionDeliveryScheduled.delete(session);
|
|
1762
|
+
void this.deliverPendingSubsessionCompletions(session).catch((error) => {
|
|
1763
|
+
this.logSubsessionNotificationFailure(session.sessionId, "inbox", error);
|
|
1764
|
+
});
|
|
1765
|
+
}, 0);
|
|
1604
1766
|
}
|
|
1605
1767
|
logSubsessionNotificationFailure(parentId, childId, error) {
|
|
1606
1768
|
this.logger.info({ parentSessionId: parentId, sessionId: childId, error: error instanceof Error ? error.message : String(error) }, "failed to notify parent of subsession completion");
|
|
@@ -1909,11 +2071,6 @@ export class PiSessionService {
|
|
|
1909
2071
|
this.publishStatus(session);
|
|
1910
2072
|
return;
|
|
1911
2073
|
}
|
|
1912
|
-
// A chat message answers the session's open ask in the user's own words, so
|
|
1913
|
-
// the form is void: keeping it open would invite answers to questions the
|
|
1914
|
-
// conversation has already moved past. Ignored duplicates skip this on
|
|
1915
|
-
// purpose: they must not void an ask posted after the queued original.
|
|
1916
|
-
await this.voidOpenAskForUserMessage(session);
|
|
1917
2074
|
if (session.isCompacting) {
|
|
1918
2075
|
this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp", images, echoUserMessage);
|
|
1919
2076
|
return;
|
|
@@ -1923,7 +2080,7 @@ export class PiSessionService {
|
|
|
1923
2080
|
submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1924
2081
|
return this.beginPromptSubmission(session, text, behavior, images, echoUserMessage).catch(() => undefined);
|
|
1925
2082
|
}
|
|
1926
|
-
beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true) {
|
|
2083
|
+
beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true, expandPromptTemplates = true) {
|
|
1927
2084
|
this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
|
|
1928
2085
|
const echoes = this.pendingPromptEchoes.get(session) ?? [];
|
|
1929
2086
|
const userCount = historyMessagesFromEntries(session.sessionManager.getBranch()).filter((message) => isRecord(message) && message["role"] === "user").length;
|
|
@@ -1936,8 +2093,8 @@ export class PiSessionService {
|
|
|
1936
2093
|
this.pendingPromptEchoes.set(session, [...echoes, echo]);
|
|
1937
2094
|
this.events.publish(session.sessionId, { type: "message.append", message: this.browserTranscriptMessage(session, echo.message) }, { id: session.sessionId, cwd: session.sessionManager.getCwd() });
|
|
1938
2095
|
}
|
|
1939
|
-
const promptOptions = buildPromptOptions(behavior, images);
|
|
1940
|
-
const commandName = text.startsWith("/") ? text.slice(1).split(" ")[0] : undefined;
|
|
2096
|
+
const promptOptions = expandPromptTemplates ? buildPromptOptions(behavior, images) : { ...buildPromptOptions(behavior, images), expandPromptTemplates: false };
|
|
2097
|
+
const commandName = expandPromptTemplates && text.startsWith("/") ? text.slice(1).split(" ")[0] : undefined;
|
|
1941
2098
|
const trackEarlyFailure = behavior === undefined
|
|
1942
2099
|
&& this.subsessionLinkForActiveChild(session) !== undefined
|
|
1943
2100
|
&& !session.extensionRunner.getRegisteredCommands().some((command) => command.invocationName === commandName);
|
|
@@ -1946,7 +2103,12 @@ export class PiSessionService {
|
|
|
1946
2103
|
if (getString(event, "type") === "agent_start")
|
|
1947
2104
|
agentStarted = true;
|
|
1948
2105
|
});
|
|
1949
|
-
const
|
|
2106
|
+
const submission = { session, active: true };
|
|
2107
|
+
const isExtensionCommand = session.extensionRunner.getRegisteredCommands().some((command) => command.invocationName === commandName);
|
|
2108
|
+
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => isExtensionCommand
|
|
2109
|
+
? this.extensionCommandExecution.run({ submission }, () => session.prompt(text, promptOptions))
|
|
2110
|
+
: session.prompt(text, promptOptions)).finally(() => {
|
|
2111
|
+
submission.active = false;
|
|
1950
2112
|
unsubscribe?.();
|
|
1951
2113
|
if (echo !== undefined) {
|
|
1952
2114
|
const remaining = (this.pendingPromptEchoes.get(session) ?? []).filter((pending) => pending !== echo);
|
|
@@ -1957,6 +2119,7 @@ export class PiSessionService {
|
|
|
1957
2119
|
}
|
|
1958
2120
|
});
|
|
1959
2121
|
void promptPromise.catch((error) => {
|
|
2122
|
+
this.subsessionStoppedParents.add(session);
|
|
1960
2123
|
const message = error instanceof Error ? error.message : String(error);
|
|
1961
2124
|
this.publishActivity(session, "error", "error", message);
|
|
1962
2125
|
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
@@ -2099,15 +2262,20 @@ export class PiSessionService {
|
|
|
2099
2262
|
return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
|
|
2100
2263
|
}
|
|
2101
2264
|
async navigateTree(ref, request) {
|
|
2102
|
-
|
|
2265
|
+
return this.navigateSessionTree(ref, request.targetId, request.expectedLeafId, sessionTreeNavigationOptions(request), undefined, request.retainCheckpoint);
|
|
2266
|
+
}
|
|
2267
|
+
async navigateSessionTree(ref, targetId, expectedLeafId, options, expectedSession, retainCheckpoint = false) {
|
|
2268
|
+
if (targetId.trim() === "")
|
|
2103
2269
|
throw new Error("Session tree target is required");
|
|
2104
2270
|
if (this.isTreeExclusiveSessionIdentityActive(ref.id)) {
|
|
2105
2271
|
throw new Error("Stop current session activity before navigating the session tree");
|
|
2106
2272
|
}
|
|
2107
2273
|
await this.assertWritable(ref);
|
|
2108
|
-
const options = sessionTreeNavigationOptions(request);
|
|
2109
2274
|
const session = await this.getOrOpen(ref);
|
|
2110
|
-
if (
|
|
2275
|
+
if (expectedSession !== undefined && session !== expectedSession)
|
|
2276
|
+
throw new Error("This extension command belongs to an unavailable session runtime");
|
|
2277
|
+
const navigateTree = session.navigateTree?.bind(session);
|
|
2278
|
+
if (navigateTree === undefined)
|
|
2111
2279
|
throw new Error("Session tree navigation is not supported by this Pi runtime");
|
|
2112
2280
|
if (this.hasActiveWork(session))
|
|
2113
2281
|
throw new Error("Stop current session activity before navigating the session tree");
|
|
@@ -2116,13 +2284,22 @@ export class PiSessionService {
|
|
|
2116
2284
|
this.treeNavigations.add(session);
|
|
2117
2285
|
try {
|
|
2118
2286
|
const oldLeafId = session.sessionManager.getLeafId();
|
|
2119
|
-
if (oldLeafId !==
|
|
2287
|
+
if (oldLeafId !== expectedLeafId) {
|
|
2120
2288
|
throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
|
|
2121
2289
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2290
|
+
if (retainCheckpoint)
|
|
2291
|
+
targetId = messageHistoryTargetId(session.sessionManager.getBranch(), targetId);
|
|
2292
|
+
const checkpointTail = retainCheckpoint ? session.sessionManager.getBranch().find((entry) => isRecord(entry) && entry["id"] === targetId) : undefined;
|
|
2293
|
+
const retainCustomMessage = isRecord(checkpointTail) && checkpointTail["type"] === "custom_message";
|
|
2294
|
+
if (retainCustomMessage && options.summarize === true) {
|
|
2295
|
+
throw new Error("Retaining a custom-message checkpoint requires navigation without a branch summary");
|
|
2296
|
+
}
|
|
2297
|
+
const activeEditableTargetParentId = retainCustomMessage ? undefined : activeEditableTreeTargetParentId(session.sessionManager, targetId, oldLeafId);
|
|
2298
|
+
this.publishActivity(session, options.summarize === true ? "summarizing branch" : "navigating session tree", "active");
|
|
2124
2299
|
this.publishStatus(session);
|
|
2125
|
-
let result =
|
|
2300
|
+
let result = retainCustomMessage
|
|
2301
|
+
? await navigateTreeRetainingCustomMessage(session.sessionManager, targetId, checkpointTail["parentId"], () => navigateTree(targetId, options))
|
|
2302
|
+
: await navigateTree(targetId, options);
|
|
2126
2303
|
if (activeEditableTargetParentId !== undefined
|
|
2127
2304
|
&& !result.cancelled
|
|
2128
2305
|
&& result.editorText === undefined
|
|
@@ -2134,14 +2311,14 @@ export class PiSessionService {
|
|
|
2134
2311
|
// extraction, agent-context rebuilding, and tree extension events.
|
|
2135
2312
|
setSessionTreeLeaf(session.sessionManager, activeEditableTargetParentId);
|
|
2136
2313
|
try {
|
|
2137
|
-
result = await
|
|
2314
|
+
result = await navigateTree(targetId, options);
|
|
2138
2315
|
}
|
|
2139
2316
|
catch (error) {
|
|
2140
|
-
session.sessionManager.branch(
|
|
2317
|
+
session.sessionManager.branch(targetId);
|
|
2141
2318
|
throw error;
|
|
2142
2319
|
}
|
|
2143
2320
|
if (result.cancelled)
|
|
2144
|
-
session.sessionManager.branch(
|
|
2321
|
+
session.sessionManager.branch(targetId);
|
|
2145
2322
|
}
|
|
2146
2323
|
if (result.cancelled) {
|
|
2147
2324
|
if (this.isCurrentActiveSession(session)) {
|
|
@@ -2149,13 +2326,6 @@ export class PiSessionService {
|
|
|
2149
2326
|
}
|
|
2150
2327
|
return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
|
|
2151
2328
|
}
|
|
2152
|
-
if (session.sessionManager.getLeafId() !== oldLeafId) {
|
|
2153
|
-
// Questions belong to the position we left. Close them for every browser
|
|
2154
|
-
// without appending a cancellation message or waking the rewound session.
|
|
2155
|
-
const outcome = this.pendingAskStore.cancelOpen(session.sessionId);
|
|
2156
|
-
if (outcome !== undefined)
|
|
2157
|
-
this.publishAskClosed(session.sessionId, outcome);
|
|
2158
|
-
}
|
|
2159
2329
|
if (result.summaryEntry !== undefined) {
|
|
2160
2330
|
// A summary entry durably identifies the selected branch as the file's
|
|
2161
2331
|
// newest leaf, superseding any earlier bare selection.
|
|
@@ -2169,6 +2339,8 @@ export class PiSessionService {
|
|
|
2169
2339
|
this.unpersistedTreeBranchLeaves.set(session, selectedLeafId);
|
|
2170
2340
|
}
|
|
2171
2341
|
}
|
|
2342
|
+
this.setSuggestedInput(session.sessionId, result.editorText);
|
|
2343
|
+
this.events.publish(session.sessionId, { type: "session.tree.changed" });
|
|
2172
2344
|
if (this.isCurrentActiveSession(session))
|
|
2173
2345
|
this.publishActivity(session, "session tree navigated", "idle");
|
|
2174
2346
|
return { cancelled: false, ...(result.editorText === undefined ? {} : { editorText: result.editorText }) };
|
|
@@ -2189,7 +2361,6 @@ export class PiSessionService {
|
|
|
2189
2361
|
}
|
|
2190
2362
|
else {
|
|
2191
2363
|
this.deferredGeneratedSessionNames.delete(session);
|
|
2192
|
-
this.deferredSubsessionNotifications.delete(session);
|
|
2193
2364
|
}
|
|
2194
2365
|
}
|
|
2195
2366
|
}
|
|
@@ -2200,6 +2371,9 @@ export class PiSessionService {
|
|
|
2200
2371
|
* about to join rather than the forked-from record.
|
|
2201
2372
|
*/
|
|
2202
2373
|
async forkFromTree(ref, request) {
|
|
2374
|
+
return this.forkSessionTree(ref, request);
|
|
2375
|
+
}
|
|
2376
|
+
async forkSessionTree(ref, request, options, expectedSession) {
|
|
2203
2377
|
if (request.entryId.trim() === "")
|
|
2204
2378
|
throw new Error("Session tree entry is required");
|
|
2205
2379
|
if (this.isTreeExclusiveSessionIdentityActive(ref.id)) {
|
|
@@ -2207,6 +2381,8 @@ export class PiSessionService {
|
|
|
2207
2381
|
}
|
|
2208
2382
|
await this.assertWritable(ref);
|
|
2209
2383
|
const session = await this.getOrOpen(ref);
|
|
2384
|
+
if (expectedSession !== undefined && session !== expectedSession)
|
|
2385
|
+
throw new Error("This extension command belongs to an unavailable session runtime");
|
|
2210
2386
|
if (this.hasActiveWork(session))
|
|
2211
2387
|
throw new Error("Stop current session activity before forking the session tree");
|
|
2212
2388
|
if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
|
|
@@ -2215,8 +2391,15 @@ export class PiSessionService {
|
|
|
2215
2391
|
this.publishActivity(session, "forking session from entry", "active");
|
|
2216
2392
|
this.publishStatus(session);
|
|
2217
2393
|
try {
|
|
2218
|
-
|
|
2394
|
+
// forkEntry rechecks this same leaf after asynchronous naming and inside
|
|
2395
|
+
// the replacement gate, so the resolved branch cutoff cannot go stale.
|
|
2396
|
+
const entryId = request.retainCheckpoint === true
|
|
2397
|
+
? messageHistoryTargetId(session.sessionManager.getBranch(), request.entryId)
|
|
2398
|
+
: request.entryId;
|
|
2399
|
+
const result = await this.commandService.forkEntry(session.sessionId, entryId, {
|
|
2400
|
+
...options,
|
|
2219
2401
|
expectedLeafId: request.expectedLeafId,
|
|
2402
|
+
expectedSession: session,
|
|
2220
2403
|
});
|
|
2221
2404
|
if (result.type === "unsupported")
|
|
2222
2405
|
throw new Error(result.message);
|
|
@@ -2229,6 +2412,8 @@ export class PiSessionService {
|
|
|
2229
2412
|
}
|
|
2230
2413
|
return { cancelled: true };
|
|
2231
2414
|
}
|
|
2415
|
+
this.setSuggestedInput(result.session.id, result.promptDraft);
|
|
2416
|
+
this.events.publish(session.sessionId, { type: "session.tree.changed" });
|
|
2232
2417
|
const forkedSession = this.active.get(result.session.id)?.runtime.session;
|
|
2233
2418
|
if (forkedSession !== undefined && this.isCurrentActiveSession(forkedSession)) {
|
|
2234
2419
|
this.publishActivity(forkedSession, "session forked", "idle");
|
|
@@ -2277,9 +2462,16 @@ export class PiSessionService {
|
|
|
2277
2462
|
this.publishActivity(session, "reloading resources", "active");
|
|
2278
2463
|
const priorGeneration = this.notificationGenerationBySession.get(session);
|
|
2279
2464
|
let candidateGeneration;
|
|
2465
|
+
const reloadBoundary = { beforeSessionStart: false };
|
|
2280
2466
|
try {
|
|
2281
|
-
await session.reload(
|
|
2467
|
+
await session.reload({
|
|
2282
2468
|
beforeSessionStart: () => {
|
|
2469
|
+
// The SDK invalidated the old runner. Settle its dialogs before
|
|
2470
|
+
// replacement session_start handlers can open new ones.
|
|
2471
|
+
this.endSessionExtensionDialogs(session.sessionId);
|
|
2472
|
+
reloadBoundary.beforeSessionStart = true;
|
|
2473
|
+
if (priorGeneration === undefined)
|
|
2474
|
+
return;
|
|
2283
2475
|
candidateGeneration = this.notificationStore.beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
2284
2476
|
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
2285
2477
|
this.replaceSessionNotificationContext(session, candidateGeneration);
|
|
@@ -2292,6 +2484,10 @@ export class PiSessionService {
|
|
|
2292
2484
|
this.publishStatus(session);
|
|
2293
2485
|
}
|
|
2294
2486
|
catch (error) {
|
|
2487
|
+
// Settings/resource loading can fail after SDK invalidation but
|
|
2488
|
+
// before the hook. Never sweep replacement session_start dialogs.
|
|
2489
|
+
if (!reloadBoundary.beforeSessionStart)
|
|
2490
|
+
this.endSessionExtensionDialogs(session.sessionId);
|
|
2295
2491
|
if (candidateGeneration !== undefined) {
|
|
2296
2492
|
this.publishNotificationMutations(this.notificationStore.abortReplacement(candidateGeneration, "candidate"));
|
|
2297
2493
|
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
@@ -2400,7 +2596,7 @@ export class PiSessionService {
|
|
|
2400
2596
|
async archiveTree(ref) {
|
|
2401
2597
|
const session = await this.getOrOpen(ref);
|
|
2402
2598
|
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
2403
|
-
const root =
|
|
2599
|
+
const root = findArchiveCandidateByExactId(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false);
|
|
2404
2600
|
const plan = planSessionArchiveTree(root, catalog);
|
|
2405
2601
|
const busy = plan.targets.map((target) => target.activeSession).find((target) => target !== undefined && this.hasActiveWork(target));
|
|
2406
2602
|
if (busy !== undefined)
|
|
@@ -2526,6 +2722,124 @@ export class PiSessionService {
|
|
|
2526
2722
|
this.unregisterSubsession(session.sessionId);
|
|
2527
2723
|
await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }]);
|
|
2528
2724
|
}
|
|
2725
|
+
async mutateQueueMessage(ref, input) {
|
|
2726
|
+
const mutation = parseQueueMessageMutation(input);
|
|
2727
|
+
await this.assertWritable(ref);
|
|
2728
|
+
const session = await this.getOrOpen(ref);
|
|
2729
|
+
// Compare the entire execution-order queue after all awaits, then mutate
|
|
2730
|
+
// synchronously so neither another request nor the agent loop can interleave.
|
|
2731
|
+
const current = this.queuedMessages(session);
|
|
2732
|
+
if (this.queueCompatibility.reason(session) !== undefined) {
|
|
2733
|
+
this.publishStatus(session);
|
|
2734
|
+
this.queueCompatibility.assertAvailable(session);
|
|
2735
|
+
}
|
|
2736
|
+
if (current.length !== mutation.expectedQueue.length || current.some((entry, index) => {
|
|
2737
|
+
const expected = mutation.expectedQueue[index];
|
|
2738
|
+
if (expected === undefined)
|
|
2739
|
+
return true;
|
|
2740
|
+
return entry.kind !== expected.kind || entry.text !== expected.text
|
|
2741
|
+
|| (mutation.action === "edit" && index === mutation.index && (entry.hasAttachments === true) !== (expected.hasAttachments === true));
|
|
2742
|
+
}))
|
|
2743
|
+
throw new Error("Queued messages changed; refresh the queue and try again");
|
|
2744
|
+
if (mutation.index >= current.length)
|
|
2745
|
+
throw new Error("Invalid queue index");
|
|
2746
|
+
try {
|
|
2747
|
+
this.queueCompatibility.run(session, () => { this.applyQueueMessageMutation(session, mutation); });
|
|
2748
|
+
}
|
|
2749
|
+
catch (error) {
|
|
2750
|
+
this.publishStatus(session);
|
|
2751
|
+
throw error;
|
|
2752
|
+
}
|
|
2753
|
+
this.publishStatus(session);
|
|
2754
|
+
return this.statusFromSession(session);
|
|
2755
|
+
}
|
|
2756
|
+
applyQueueMessageMutation(session, mutation) {
|
|
2757
|
+
const agent = mutableQueueAgent(session.agent);
|
|
2758
|
+
const mirrors = { steer: mutableTextMirror(session.getSteeringMessages()), followUp: mutableTextMirror(session.getFollowUpMessages()) };
|
|
2759
|
+
if (session.getSteeringMessages() !== mirrors.steer || session.getFollowUpMessages() !== mirrors.followUp) {
|
|
2760
|
+
throw new Error("SDK queue getters no longer expose stable backing arrays");
|
|
2761
|
+
}
|
|
2762
|
+
const queues = snapshotAgentQueues(agent);
|
|
2763
|
+
const originalQueues = { steer: [...queues.steer], followUp: [...queues.followUp] };
|
|
2764
|
+
const originalMirrors = { steer: [...mirrors.steer], followUp: [...mirrors.followUp] };
|
|
2765
|
+
const held = this.compactionQueuedMessages(session.sessionId).map((prompt) => ({ ...prompt }));
|
|
2766
|
+
const entries = ["steer", "followUp"].flatMap((kind) => [
|
|
2767
|
+
...mirroredQueueEntries(queues[kind], mirrors[kind]).map((entry) => ({ ...entry, kind })),
|
|
2768
|
+
...held.filter((prompt) => prompt.kind === kind).map((prompt) => ({ kind, text: prompt.text, prompt })),
|
|
2769
|
+
]);
|
|
2770
|
+
const selected = entries[mutation.index];
|
|
2771
|
+
if (selected === undefined)
|
|
2772
|
+
throw new Error("Invalid queue index");
|
|
2773
|
+
const removed = mutation.action === "edit" ? entries.slice(mutation.index) : [selected];
|
|
2774
|
+
const remaining = entries.filter((entry) => !removed.includes(entry));
|
|
2775
|
+
for (const entry of removed) {
|
|
2776
|
+
if (entry.message !== undefined) {
|
|
2777
|
+
const queue = queues[entry.kind];
|
|
2778
|
+
queue.splice(queue.indexOf(entry.message), 1);
|
|
2779
|
+
}
|
|
2780
|
+
if (entry.prompt !== undefined)
|
|
2781
|
+
held.splice(held.indexOf(entry.prompt), 1);
|
|
2782
|
+
}
|
|
2783
|
+
if (mutation.action === "toggle") {
|
|
2784
|
+
const kind = selected.kind === "steer" ? "followUp" : "steer";
|
|
2785
|
+
// Destination includes held prompts: append there when necessary to keep
|
|
2786
|
+
// status order and eventual execution order identical.
|
|
2787
|
+
if (selected.prompt !== undefined) {
|
|
2788
|
+
selected.prompt.kind = kind;
|
|
2789
|
+
held.push(selected.prompt);
|
|
2790
|
+
remaining.push({ ...selected, kind });
|
|
2791
|
+
}
|
|
2792
|
+
else if (selected.message !== undefined) {
|
|
2793
|
+
if (held.some((prompt) => prompt.kind === kind)) {
|
|
2794
|
+
const prompt = { kind, text: selected.text, message: selected.message };
|
|
2795
|
+
held.push(prompt);
|
|
2796
|
+
remaining.push({ kind, text: selected.text, prompt });
|
|
2797
|
+
}
|
|
2798
|
+
else {
|
|
2799
|
+
queues[kind].push(selected.message);
|
|
2800
|
+
remaining.push({ ...selected, kind });
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
try {
|
|
2805
|
+
agent.clearAllQueues();
|
|
2806
|
+
for (const message of queues.steer)
|
|
2807
|
+
agent.steer(message);
|
|
2808
|
+
for (const message of queues.followUp)
|
|
2809
|
+
agent.followUp(message);
|
|
2810
|
+
for (const kind of ["steer", "followUp"]) {
|
|
2811
|
+
mirrors[kind].splice(0, mirrors[kind].length, ...remaining.filter((entry) => entry.kind === kind && entry.message !== undefined).map((entry) => entry.text));
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
catch (error) {
|
|
2815
|
+
// Best-effort rollback when a callable SDK method fails during replacement.
|
|
2816
|
+
// Restoration itself can fail after an incompatible behavioral change.
|
|
2817
|
+
const failures = [error];
|
|
2818
|
+
try {
|
|
2819
|
+
agent.clearAllQueues();
|
|
2820
|
+
for (const message of originalQueues.steer)
|
|
2821
|
+
agent.steer(message);
|
|
2822
|
+
for (const message of originalQueues.followUp)
|
|
2823
|
+
agent.followUp(message);
|
|
2824
|
+
}
|
|
2825
|
+
catch (restoreError) {
|
|
2826
|
+
failures.push(restoreError);
|
|
2827
|
+
}
|
|
2828
|
+
for (const kind of ["steer", "followUp"]) {
|
|
2829
|
+
try {
|
|
2830
|
+
mirrors[kind].splice(0, mirrors[kind].length, ...originalMirrors[kind]);
|
|
2831
|
+
}
|
|
2832
|
+
catch (restoreError) {
|
|
2833
|
+
failures.push(restoreError);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
throw new AggregateError(failures, "Could not replace SDK queues; attempted to restore their original contents", { cause: error });
|
|
2837
|
+
}
|
|
2838
|
+
if (held.length === 0)
|
|
2839
|
+
this.clearCompactionPromptQueue(session.sessionId);
|
|
2840
|
+
else
|
|
2841
|
+
this.compactionPromptQueues.set(session.sessionId, held);
|
|
2842
|
+
}
|
|
2529
2843
|
async clearQueue(ref) {
|
|
2530
2844
|
await this.assertWritable(ref);
|
|
2531
2845
|
const session = await this.getOrOpen(ref);
|
|
@@ -2546,6 +2860,7 @@ export class PiSessionService {
|
|
|
2546
2860
|
return;
|
|
2547
2861
|
const sessionId = active.runtime.session.sessionId;
|
|
2548
2862
|
this.clearCompactionPromptQueue(sessionId);
|
|
2863
|
+
this.subsessionStoppedParents.add(active.runtime.session);
|
|
2549
2864
|
clearSessionQueue(active.runtime.session);
|
|
2550
2865
|
// Settle run-scoped dialogs now, at abort-request time: pi's agent loop
|
|
2551
2866
|
// waits for a parked `tool_call` dialog handler before it can emit
|
|
@@ -2716,6 +3031,7 @@ export class PiSessionService {
|
|
|
2716
3031
|
if (pendingOpens.length > 0)
|
|
2717
3032
|
await Promise.allSettled(pendingOpens);
|
|
2718
3033
|
const active = this.active.get(sessionId);
|
|
3034
|
+
this.suggestedInputs.delete(sessionId);
|
|
2719
3035
|
if (notificationPolicy.kind === "clear") {
|
|
2720
3036
|
const generation = active === undefined ? undefined : this.notificationGenerationBySession.get(active.runtime.session);
|
|
2721
3037
|
const mutations = generation === undefined
|
|
@@ -2726,11 +3042,8 @@ export class PiSessionService {
|
|
|
2726
3042
|
if (!active)
|
|
2727
3043
|
return;
|
|
2728
3044
|
this.forgetUnreadActivity(active.runtime.session);
|
|
2729
|
-
//
|
|
2730
|
-
//
|
|
2731
|
-
this.pendingAskStore.forgetSession(sessionId);
|
|
2732
|
-
// Open dialogs share that stance, but their extension waiters are parked
|
|
2733
|
-
// Promises inside the dying runtime: settle them rather than dropping them.
|
|
3045
|
+
// Extension dialog waiters are parked Promises inside the dying runtime:
|
|
3046
|
+
// settle them rather than dropping them.
|
|
2734
3047
|
this.endSessionExtensionDialogs(sessionId);
|
|
2735
3048
|
this.active.delete(sessionId);
|
|
2736
3049
|
this.events.mediaIndex.forgetSession({ id: sessionId, cwd: active.runtime.session.sessionManager.getCwd() });
|
|
@@ -2908,26 +3221,12 @@ export class PiSessionService {
|
|
|
2908
3221
|
return { id: sessionId, cwd: active.runtime.cwd };
|
|
2909
3222
|
}
|
|
2910
3223
|
activeForRef(ref) {
|
|
2911
|
-
const
|
|
2912
|
-
|
|
2913
|
-
if (exact !== undefined && refMatchesActiveSession(ref, exact))
|
|
2914
|
-
return exact;
|
|
2915
|
-
for (const [candidateId, active] of this.active.entries()) {
|
|
2916
|
-
if (candidateId.startsWith(sessionId) && refMatchesActiveSession(ref, active))
|
|
2917
|
-
return active;
|
|
2918
|
-
}
|
|
2919
|
-
return undefined;
|
|
3224
|
+
const active = this.active.get(ref.id);
|
|
3225
|
+
return active !== undefined && refMatchesActiveSession(ref, active) ? active : undefined;
|
|
2920
3226
|
}
|
|
2921
3227
|
startupSessionForRef(ref) {
|
|
2922
|
-
const
|
|
2923
|
-
|
|
2924
|
-
if (exact !== undefined && refMatchesStartupSession(ref, exact))
|
|
2925
|
-
return exact;
|
|
2926
|
-
for (const [candidateId, session] of this.startupSessions.entries()) {
|
|
2927
|
-
if (candidateId.startsWith(sessionId) && refMatchesStartupSession(ref, session))
|
|
2928
|
-
return session;
|
|
2929
|
-
}
|
|
2930
|
-
return undefined;
|
|
3228
|
+
const session = this.startupSessions.get(ref.id);
|
|
3229
|
+
return session !== undefined && refMatchesStartupSession(ref, session) ? session : undefined;
|
|
2931
3230
|
}
|
|
2932
3231
|
/**
|
|
2933
3232
|
* The session to serve a read-only status or a dialog close for, while it
|
|
@@ -3036,7 +3335,13 @@ export class PiSessionService {
|
|
|
3036
3335
|
// holds for this session; settle those waits before the new
|
|
3037
3336
|
// runtime's extensions can open fresh dialogs under the same id.
|
|
3038
3337
|
this.endSessionExtensionDialogs(boundSession.sessionId);
|
|
3338
|
+
const changedIdentity = boundSession.sessionId !== session.sessionId;
|
|
3039
3339
|
boundSession = session;
|
|
3340
|
+
// Publish existence as soon as status/dialog APIs can address it,
|
|
3341
|
+
// before session_start or arbitrary withSession code can park on a question.
|
|
3342
|
+
this.startupSessions.set(session.sessionId, session);
|
|
3343
|
+
if (changedIdentity)
|
|
3344
|
+
this.announceCreatedSession(active, session.sessionManager.getCwd());
|
|
3040
3345
|
await this.bindSessionExtensions(session, candidateGeneration);
|
|
3041
3346
|
if (candidateGeneration !== undefined) {
|
|
3042
3347
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
|
|
@@ -3111,6 +3416,7 @@ export class PiSessionService {
|
|
|
3111
3416
|
await session.bindExtensions({
|
|
3112
3417
|
uiContext,
|
|
3113
3418
|
mode: "rpc",
|
|
3419
|
+
commandContextActions: this.extensionCommandActions(session),
|
|
3114
3420
|
onError: (error) => {
|
|
3115
3421
|
const message = `${error.extensionPath}: ${error.error}`;
|
|
3116
3422
|
this.publishActivity(session, "extension error", "error", message);
|
|
@@ -3125,6 +3431,83 @@ export class PiSessionService {
|
|
|
3125
3431
|
this.startupSessions.delete(session.sessionId);
|
|
3126
3432
|
}
|
|
3127
3433
|
}
|
|
3434
|
+
extensionCommandActions(session) {
|
|
3435
|
+
const ref = { id: session.sessionId, cwd: session.sessionManager.getCwd() };
|
|
3436
|
+
return {
|
|
3437
|
+
waitForIdle: () => {
|
|
3438
|
+
this.assertCurrentExtensionSession(session);
|
|
3439
|
+
return session.waitForIdle();
|
|
3440
|
+
},
|
|
3441
|
+
navigateTree: (targetId, options) => {
|
|
3442
|
+
const expectedLeafId = session.sessionManager.getLeafId();
|
|
3443
|
+
return this.runExtensionSessionControl(session, async () => {
|
|
3444
|
+
const result = await this.navigateSessionTree(ref, targetId, expectedLeafId, options ?? { summarize: false }, session);
|
|
3445
|
+
return { cancelled: result.cancelled };
|
|
3446
|
+
});
|
|
3447
|
+
},
|
|
3448
|
+
fork: (entryId, options) => {
|
|
3449
|
+
const expectedLeafId = session.sessionManager.getLeafId();
|
|
3450
|
+
const withSession = options?.withSession;
|
|
3451
|
+
let completeReplacement;
|
|
3452
|
+
const forkOptions = {
|
|
3453
|
+
...options,
|
|
3454
|
+
position: options?.position ?? "before",
|
|
3455
|
+
// Capture Pi's fresh context, but run user code after the host's
|
|
3456
|
+
// replacement reservation is released so fresh controls can work.
|
|
3457
|
+
...(withSession === undefined ? {} : { withSession: (ctx) => {
|
|
3458
|
+
completeReplacement = () => withSession(ctx);
|
|
3459
|
+
return Promise.resolve();
|
|
3460
|
+
} }),
|
|
3461
|
+
};
|
|
3462
|
+
return this.runExtensionSessionControl(session, async () => {
|
|
3463
|
+
const runtime = this.active.get(ref.id)?.runtime;
|
|
3464
|
+
const result = await this.forkSessionTree(ref, { entryId, expectedLeafId }, forkOptions, session);
|
|
3465
|
+
if (!result.cancelled) {
|
|
3466
|
+
try {
|
|
3467
|
+
await completeReplacement?.();
|
|
3468
|
+
}
|
|
3469
|
+
catch (error) {
|
|
3470
|
+
// The fork is committed. Report callback failure on the current
|
|
3471
|
+
// runtime (which may have forked again), including its recoverable inbox.
|
|
3472
|
+
const current = runtime?.session;
|
|
3473
|
+
if (current !== undefined && this.isCurrentActiveSession(current)) {
|
|
3474
|
+
const message = errorMessage(error);
|
|
3475
|
+
current.extensionRunner.getUIContext().notify(message, "error");
|
|
3476
|
+
this.publishActivity(current, "extension callback failed", "error", message);
|
|
3477
|
+
this.events.publish(current.sessionId, { type: "session.error", message });
|
|
3478
|
+
}
|
|
3479
|
+
throw error;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
return { cancelled: result.cancelled };
|
|
3483
|
+
});
|
|
3484
|
+
},
|
|
3485
|
+
reload: () => this.runExtensionSessionControl(session, () => this.reloadSessionRuntime(session)),
|
|
3486
|
+
// These need host-owned creation/selection policies, not SDK runtime-only
|
|
3487
|
+
// replacement. Fail visibly until PI WEB supplies that integration.
|
|
3488
|
+
newSession: () => Promise.reject(new Error("ctx.newSession() is not supported in PI WEB. Start a new session from the web UI.")),
|
|
3489
|
+
switchSession: () => Promise.reject(new Error("ctx.switchSession() is not supported in PI WEB. Select a session from the web UI.")),
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
setSuggestedInput(sessionId, text) {
|
|
3493
|
+
if (text === undefined || text === "")
|
|
3494
|
+
this.suggestedInputs.delete(sessionId);
|
|
3495
|
+
else
|
|
3496
|
+
this.suggestedInputs.set(sessionId, text);
|
|
3497
|
+
}
|
|
3498
|
+
assertCurrentExtensionSession(session) {
|
|
3499
|
+
if (!this.isCurrentActiveSession(session) || this.startupSessions.has(session.sessionId)) {
|
|
3500
|
+
throw new Error("This extension command belongs to an unavailable session runtime");
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
runExtensionSessionControl(session, operation) {
|
|
3504
|
+
return this.extensionCommandExecution.run({ ...this.extensionCommandExecution.getStore(), sessionControl: true }, async () => {
|
|
3505
|
+
this.assertCurrentExtensionSession(session);
|
|
3506
|
+
await this.assertWritable({ id: session.sessionId, cwd: session.sessionManager.getCwd() });
|
|
3507
|
+
this.assertCurrentExtensionSession(session);
|
|
3508
|
+
return operation();
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3128
3511
|
replaceSessionNotificationContext(session, generation) {
|
|
3129
3512
|
session.extensionRunner.setUIContext(this.sessionUiContext(session, generation), "rpc");
|
|
3130
3513
|
}
|
|
@@ -3301,9 +3684,30 @@ export class PiSessionService {
|
|
|
3301
3684
|
this.clearCompactionPromptQueue(sessionId);
|
|
3302
3685
|
}
|
|
3303
3686
|
}
|
|
3687
|
+
this.restoreSubsessionReceipts(session);
|
|
3304
3688
|
const unsubscribe = session.subscribe((event) => {
|
|
3305
3689
|
const eventType = getString(event, "type");
|
|
3306
3690
|
const message = getProperty(event, "message");
|
|
3691
|
+
const inbox = this.subsessionInbox(session.sessionId, session.sessionFile);
|
|
3692
|
+
if (eventType === "agent_start") {
|
|
3693
|
+
this.restoreSubsessionReceipts(session, false);
|
|
3694
|
+
this.subsessionStoppedParents.delete(session);
|
|
3695
|
+
}
|
|
3696
|
+
if (eventType === "message_end" && getString(message, "role") === "assistant"
|
|
3697
|
+
&& (getString(message, "stopReason") === "aborted" || getString(message, "stopReason") === "error")) {
|
|
3698
|
+
this.subsessionStoppedParents.add(session);
|
|
3699
|
+
}
|
|
3700
|
+
const entry = getProperty(event, "entry");
|
|
3701
|
+
if (eventType === "entry_appended" && getString(entry, "type") === "custom_message"
|
|
3702
|
+
&& getString(entry, "customType") === SUBSESSION_NOTIFICATION_CUSTOM_TYPE) {
|
|
3703
|
+
inbox.receive(completionReceiptIds(getProperty(entry, "details")));
|
|
3704
|
+
}
|
|
3705
|
+
// message_end listeners run before SDK persistence. These later
|
|
3706
|
+
// boundaries can safely reconcile committed messages and failed intents.
|
|
3707
|
+
if (eventType === "turn_end" || eventType === "agent_settled")
|
|
3708
|
+
this.restoreSubsessionReceipts(session);
|
|
3709
|
+
if (eventType === "agent_settled")
|
|
3710
|
+
this.scheduleSubsessionDelivery(session);
|
|
3307
3711
|
if ((eventType === "message_start" || eventType === "message_update") && getString(message, "role") === "assistant") {
|
|
3308
3712
|
// SDK state advances before async extension hooks finish. Capture only
|
|
3309
3713
|
// published messages, detaching even blocks left intact by projection.
|
|
@@ -3318,23 +3722,18 @@ export class PiSessionService {
|
|
|
3318
3722
|
}
|
|
3319
3723
|
this.events.publish(session.sessionId, clientEvent, { id: session.sessionId, cwd: session.sessionManager.getCwd() });
|
|
3320
3724
|
this.publishActivityForEvent(session, event);
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
if (eventType === "message_start" && isRecord(event) && getString(event["message"], "role") === "user") {
|
|
3324
|
-
void this.voidOpenAskForUserMessage(session).catch((error) => {
|
|
3325
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3326
|
-
this.publishActivity(session, "error", "error", message);
|
|
3327
|
-
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
3328
|
-
});
|
|
3329
|
-
}
|
|
3725
|
+
if (eventType === "message_start" && getString(message, "role") === "user")
|
|
3726
|
+
this.suggestedInputs.delete(session.sessionId);
|
|
3330
3727
|
if (eventType === "agent_end")
|
|
3331
3728
|
this.abortRunScopedExtensionDialogs(session.sessionId);
|
|
3332
3729
|
if (eventType === "compaction_end")
|
|
3333
3730
|
this.scheduleCompactionQueueDrain(session.sessionId);
|
|
3731
|
+
if (eventType === "compaction_end" || eventType === "bash_execution_end")
|
|
3732
|
+
this.scheduleSubsessionDelivery(session);
|
|
3334
3733
|
if (eventType === "agent_start" || eventType === "agent_end")
|
|
3335
3734
|
this.scheduleCompactionQueueDrain(session.sessionId);
|
|
3336
3735
|
this.publishStatus(session);
|
|
3337
|
-
this.updateSubsessionTracking(session, eventType === "agent_start");
|
|
3736
|
+
this.updateSubsessionTracking(session, eventType === "agent_start", eventType === "agent_settled");
|
|
3338
3737
|
});
|
|
3339
3738
|
active.unsubscribe = () => {
|
|
3340
3739
|
this.sessionEvents.close(session);
|
|
@@ -3367,16 +3766,25 @@ export class PiSessionService {
|
|
|
3367
3766
|
return;
|
|
3368
3767
|
this.publishStatus(session);
|
|
3369
3768
|
for (const prompt of queued)
|
|
3370
|
-
void this.
|
|
3769
|
+
void this.submitHeldPrompt(session, prompt, prompt.kind);
|
|
3371
3770
|
return;
|
|
3372
3771
|
}
|
|
3373
3772
|
const prompt = this.shiftCompactionPrompt(sessionId);
|
|
3374
3773
|
if (prompt === undefined)
|
|
3375
3774
|
return;
|
|
3376
3775
|
this.publishStatus(session);
|
|
3377
|
-
const submitted = this.
|
|
3776
|
+
const submitted = this.submitHeldPrompt(session, prompt);
|
|
3378
3777
|
void submitted.finally(() => { this.scheduleCompactionQueueDrain(sessionId); });
|
|
3379
3778
|
}
|
|
3779
|
+
submitHeldPrompt(session, prompt, kind) {
|
|
3780
|
+
if (prompt.message === undefined)
|
|
3781
|
+
return this.submitPrompt(session, prompt.text, kind, prompt.images, prompt.echoUserMessage ?? true);
|
|
3782
|
+
// Use the public session API even after the queue adapter has been disabled.
|
|
3783
|
+
// Keep auth, run ownership and settlement SDK-owned; text is already expanded.
|
|
3784
|
+
const images = prompt.message.role === "user" && Array.isArray(prompt.message.content)
|
|
3785
|
+
? prompt.message.content.filter((part) => part.type === "image") : [];
|
|
3786
|
+
return this.beginPromptSubmission(session, prompt.text, kind, images, true, false).catch(() => undefined);
|
|
3787
|
+
}
|
|
3380
3788
|
takeCompactionPromptQueue(sessionId) {
|
|
3381
3789
|
const queued = this.compactionPromptQueues.get(sessionId) ?? [];
|
|
3382
3790
|
this.compactionPromptQueues.delete(sessionId);
|
|
@@ -3384,7 +3792,8 @@ export class PiSessionService {
|
|
|
3384
3792
|
}
|
|
3385
3793
|
shiftCompactionPrompt(sessionId) {
|
|
3386
3794
|
const queue = this.compactionPromptQueues.get(sessionId);
|
|
3387
|
-
const
|
|
3795
|
+
const steeringIndex = queue?.findIndex((prompt) => prompt.kind === "steer") ?? -1;
|
|
3796
|
+
const prompt = queue?.splice(steeringIndex < 0 ? 0 : steeringIndex, 1)[0];
|
|
3388
3797
|
if (queue === undefined || queue.length === 0)
|
|
3389
3798
|
this.compactionPromptQueues.delete(sessionId);
|
|
3390
3799
|
return prompt;
|
|
@@ -3405,11 +3814,6 @@ export class PiSessionService {
|
|
|
3405
3814
|
maybeGenerateSessionName(session, firstMessage) {
|
|
3406
3815
|
if (session.sessionName !== undefined || session.messages.length !== 0 || session.isStreaming || session.isCompacting)
|
|
3407
3816
|
return;
|
|
3408
|
-
const deterministicName = deterministicSessionName(firstMessage);
|
|
3409
|
-
if (deterministicName !== undefined) {
|
|
3410
|
-
this.applyGeneratedSessionName(session, deterministicName);
|
|
3411
|
-
return;
|
|
3412
|
-
}
|
|
3413
3817
|
const model = session.model;
|
|
3414
3818
|
if (model === undefined)
|
|
3415
3819
|
return;
|
|
@@ -3440,13 +3844,8 @@ export class PiSessionService {
|
|
|
3440
3844
|
this.logger.info({ sessionId: session.sessionId, error: error instanceof Error ? error.message : String(error) }, "failed to apply deferred session name");
|
|
3441
3845
|
}
|
|
3442
3846
|
}
|
|
3443
|
-
|
|
3444
|
-
this.
|
|
3445
|
-
for (const notification of notifications) {
|
|
3446
|
-
void this.deliverSubsessionNotification(session, notification).catch((error) => {
|
|
3447
|
-
this.logSubsessionNotificationFailure(notification.parentId, notification.childId, error);
|
|
3448
|
-
});
|
|
3449
|
-
}
|
|
3847
|
+
this.restoreSubsessionReceipts(session);
|
|
3848
|
+
this.scheduleSubsessionDelivery(session);
|
|
3450
3849
|
}
|
|
3451
3850
|
applyAuthChange(change = {}) {
|
|
3452
3851
|
// ModelRuntime.login()/logout() refresh the shared runtime before AuthService
|
|
@@ -3532,8 +3931,11 @@ export class PiSessionService {
|
|
|
3532
3931
|
return "active";
|
|
3533
3932
|
}
|
|
3534
3933
|
hasActiveWork(session) {
|
|
3934
|
+
const execution = this.extensionCommandExecution.getStore();
|
|
3935
|
+
const ownCommandReceipt = execution?.sessionControl === true
|
|
3936
|
+
&& execution.submission?.active === true && execution.submission.session === session ? 1 : 0;
|
|
3535
3937
|
return this.treeNavigations.has(session)
|
|
3536
|
-
|| this.isSessionEntryMutationActive(session)
|
|
3938
|
+
|| this.isSessionEntryMutationActive(session, ownCommandReceipt)
|
|
3537
3939
|
|| this.isTreeExclusiveOperationActive(session)
|
|
3538
3940
|
|| sessionHasActiveWork(session, this.compactionQueuedMessages(session.sessionId).length);
|
|
3539
3941
|
}
|
|
@@ -3615,9 +4017,11 @@ export class PiSessionService {
|
|
|
3615
4017
|
else
|
|
3616
4018
|
this.sessionEntryMutationCounts.set(session, remaining);
|
|
3617
4019
|
this.observeUnreadActivityState(session);
|
|
4020
|
+
if (remaining <= 0 && !this.subsessionDeliveringParents.has(session))
|
|
4021
|
+
this.scheduleSubsessionDelivery(session);
|
|
3618
4022
|
}
|
|
3619
|
-
isSessionEntryMutationActive(session) {
|
|
3620
|
-
return (this.sessionEntryMutationCounts.get(session) ?? 0) >
|
|
4023
|
+
isSessionEntryMutationActive(session, ignoredReceipts = 0) {
|
|
4024
|
+
return (this.sessionEntryMutationCounts.get(session) ?? 0) > ignoredReceipts;
|
|
3621
4025
|
}
|
|
3622
4026
|
publishActivityForEvent(session, event) {
|
|
3623
4027
|
const eventType = getString(event, "type");
|
|
@@ -3764,25 +4168,29 @@ export class PiSessionService {
|
|
|
3764
4168
|
const model = session.model === undefined ? undefined : modelToClientModel(session.model);
|
|
3765
4169
|
const contextUsage = session.getContextUsage();
|
|
3766
4170
|
const warnings = this.warningsForSession(session);
|
|
3767
|
-
const pendingAsk = this.pendingAskStore.pendingAsk(session.sessionId);
|
|
3768
4171
|
const pendingDialogs = this.pendingExtensionDialogStore.pendingDialogs(session.sessionId);
|
|
4172
|
+
const suggestedInput = this.suggestedInputs.get(session.sessionId);
|
|
4173
|
+
const queuedMessages = this.queuedMessages(session);
|
|
4174
|
+
const queueMessageActionsUnavailable = this.queueCompatibility.reason(session);
|
|
3769
4175
|
return {
|
|
3770
4176
|
sessionId: session.sessionId,
|
|
3771
4177
|
persisted: sessionFileExists(session.sessionFile),
|
|
4178
|
+
recentlyActiveElsewhere: this.activityMarker.isActiveElsewhere(session.sessionFile),
|
|
3772
4179
|
...(model === undefined ? {} : { model }),
|
|
3773
4180
|
thinkingLevel: session.thinkingLevel,
|
|
3774
4181
|
isStreaming: session.isStreaming,
|
|
3775
4182
|
isCompacting: session.isCompacting,
|
|
3776
4183
|
isBashRunning: session.isBashRunning,
|
|
3777
4184
|
pendingMessageCount: this.pendingMessageCount(session),
|
|
3778
|
-
queuedMessages
|
|
4185
|
+
queuedMessages,
|
|
4186
|
+
...(queueMessageActionsUnavailable === undefined ? {} : { queueMessageActionsUnavailable }),
|
|
3779
4187
|
messageCount,
|
|
3780
4188
|
tokens: stats.tokens,
|
|
3781
4189
|
cost: stats.cost,
|
|
3782
4190
|
...(contextUsage === undefined ? {} : { contextUsage }),
|
|
3783
4191
|
...(warnings.length === 0 ? {} : { warnings }),
|
|
3784
|
-
...(pendingAsk === undefined ? {} : { pendingAsk }),
|
|
3785
4192
|
...(pendingDialogs.length === 0 ? {} : { pendingDialogs }),
|
|
4193
|
+
...(suggestedInput === undefined ? {} : { suggestedInput }),
|
|
3786
4194
|
};
|
|
3787
4195
|
}
|
|
3788
4196
|
/**
|
|
@@ -3794,13 +4202,6 @@ export class PiSessionService {
|
|
|
3794
4202
|
warningsForSession(session) {
|
|
3795
4203
|
const runtime = this.active.get(session.sessionId)?.runtime;
|
|
3796
4204
|
const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
|
|
3797
|
-
if (this.activityMarker.isActiveElsewhere(session.sessionFile)) {
|
|
3798
|
-
warnings.push({
|
|
3799
|
-
severity: "info",
|
|
3800
|
-
message: "Recently active in another PI-WEB instance. Avoid working on this session in both instances at once.",
|
|
3801
|
-
source: "PI-WEB",
|
|
3802
|
-
});
|
|
3803
|
-
}
|
|
3804
4205
|
const anthropic = anthropicSubscriptionWarning(session, join(this.agentDir, "auth.json"));
|
|
3805
4206
|
if (anthropic !== undefined)
|
|
3806
4207
|
warnings.push(anthropic);
|
|
@@ -3812,8 +4213,12 @@ export class PiSessionService {
|
|
|
3812
4213
|
compactionQueuedMessages(sessionId) {
|
|
3813
4214
|
return this.compactionPromptQueues.get(sessionId) ?? [];
|
|
3814
4215
|
}
|
|
4216
|
+
queuedMessages(session) {
|
|
4217
|
+
const held = this.compactionQueuedMessages(session.sessionId);
|
|
4218
|
+
return this.queueCompatibility.inspect(session, () => queuedMessagesFromSession(session, held), () => queuedMessagesFromSession(session, held, false));
|
|
4219
|
+
}
|
|
3815
4220
|
hasQueuedMessageText(session, text) {
|
|
3816
|
-
return
|
|
4221
|
+
return this.queuedMessages(session).some((message) => message.text === text);
|
|
3817
4222
|
}
|
|
3818
4223
|
}
|
|
3819
4224
|
function previewResponseFromPlan(plan) {
|
|
@@ -3841,13 +4246,13 @@ function bulkRefToSessionRef(ref) {
|
|
|
3841
4246
|
return { id: ref.id, cwd: ref.cwd };
|
|
3842
4247
|
}
|
|
3843
4248
|
function findArchivedRecordForBulkRef(records, ref) {
|
|
3844
|
-
return records.find((record) => record.cwd === ref.cwd &&
|
|
4249
|
+
return records.find((record) => record.cwd === ref.cwd && record.sessionId === ref.id);
|
|
3845
4250
|
}
|
|
3846
4251
|
function findListedSessionForBulkRef(context, ref) {
|
|
3847
|
-
return
|
|
4252
|
+
return findSessionByExactId(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
3848
4253
|
}
|
|
3849
|
-
function
|
|
3850
|
-
return sessions.find((session) => session.id === sessionId)
|
|
4254
|
+
function findSessionByExactId(sessions, sessionId) {
|
|
4255
|
+
return sessions.find((session) => session.id === sessionId);
|
|
3851
4256
|
}
|
|
3852
4257
|
function uniqueStrings(values) {
|
|
3853
4258
|
return [...new Set(values)];
|
|
@@ -3888,12 +4293,35 @@ function notificationIdentityForSession(session) {
|
|
|
3888
4293
|
cwd: canonicalizeStoredCwd(session.sessionManager.getCwd()),
|
|
3889
4294
|
};
|
|
3890
4295
|
}
|
|
4296
|
+
function metadataFromActiveSession(session) {
|
|
4297
|
+
return readSessionUiMetadata(session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch());
|
|
4298
|
+
}
|
|
4299
|
+
function clientSessionFromActiveSession(session) {
|
|
4300
|
+
const header = session.sessionManager.getHeader?.();
|
|
4301
|
+
const created = header?.timestamp ?? new Date().toISOString();
|
|
4302
|
+
const metadata = metadataFromActiveSession(session);
|
|
4303
|
+
return {
|
|
4304
|
+
id: session.sessionId,
|
|
4305
|
+
path: session.sessionFile ?? "",
|
|
4306
|
+
cwd: session.sessionManager.getCwd(),
|
|
4307
|
+
persisted: sessionFileExists(session.sessionFile),
|
|
4308
|
+
created,
|
|
4309
|
+
modified: created,
|
|
4310
|
+
messageCount: session.messages.length,
|
|
4311
|
+
firstMessage: "",
|
|
4312
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
4313
|
+
...(session.sessionName === undefined ? {} : { name: session.sessionName }),
|
|
4314
|
+
...(header?.parentSession === undefined ? {} : { parentSessionPath: header.parentSession }),
|
|
4315
|
+
};
|
|
4316
|
+
}
|
|
3891
4317
|
function clientSessionFromListEntry(session) {
|
|
3892
4318
|
return {
|
|
3893
4319
|
id: session.id,
|
|
3894
4320
|
path: session.path,
|
|
3895
4321
|
cwd: session.cwd,
|
|
3896
4322
|
persisted: true,
|
|
4323
|
+
...(session.agentSubsession === true ? { agentSubsession: true } : {}),
|
|
4324
|
+
...(session.metadata === undefined ? {} : { metadata: requireSessionUiMetadata(session.metadata) }),
|
|
3897
4325
|
...(session.name === undefined ? {} : { name: session.name }),
|
|
3898
4326
|
created: session.created.toISOString(),
|
|
3899
4327
|
modified: session.modified.toISOString(),
|
|
@@ -3911,6 +4339,7 @@ function archiveInputFromListEntry(session) {
|
|
|
3911
4339
|
modified: session.modified.toISOString(),
|
|
3912
4340
|
messageCount: session.messageCount,
|
|
3913
4341
|
firstMessage: session.firstMessage,
|
|
4342
|
+
...(session.metadata === undefined ? {} : { metadata: session.metadata }),
|
|
3914
4343
|
...(session.name === undefined ? {} : { name: session.name }),
|
|
3915
4344
|
...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
|
|
3916
4345
|
};
|
|
@@ -3920,6 +4349,7 @@ function archiveInputFromActiveSession(session) {
|
|
|
3920
4349
|
if (sessionFile === undefined || sessionFile === "")
|
|
3921
4350
|
throw new Error("Session is not persisted");
|
|
3922
4351
|
const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
|
|
4352
|
+
const metadata = metadataFromActiveSession(session);
|
|
3923
4353
|
return {
|
|
3924
4354
|
sessionId: session.sessionId,
|
|
3925
4355
|
cwd: session.sessionManager.getCwd(),
|
|
@@ -3928,6 +4358,7 @@ function archiveInputFromActiveSession(session) {
|
|
|
3928
4358
|
modified: new Date().toISOString(),
|
|
3929
4359
|
messageCount: session.messages.length,
|
|
3930
4360
|
firstMessage: "",
|
|
4361
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
3931
4362
|
...(session.sessionName === undefined ? {} : { name: session.sessionName }),
|
|
3932
4363
|
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
3933
4364
|
};
|
|
@@ -3992,11 +4423,13 @@ function clientSessionFromArchivedRecord(record, fallback) {
|
|
|
3992
4423
|
if (path === undefined || created === undefined || modified === undefined || messageCount === undefined || firstMessage === undefined)
|
|
3993
4424
|
return undefined;
|
|
3994
4425
|
const name = record.name ?? fallback?.name;
|
|
4426
|
+
const metadata = record.metadata ?? fallback?.metadata;
|
|
3995
4427
|
const parentSessionPath = record.parentSessionPath ?? fallback?.parentSessionPath;
|
|
3996
4428
|
return {
|
|
3997
4429
|
id: record.sessionId,
|
|
3998
4430
|
path,
|
|
3999
4431
|
cwd: record.cwd,
|
|
4432
|
+
...(metadata === undefined ? {} : { metadata: requireSessionUiMetadata(metadata) }),
|
|
4000
4433
|
...(name === undefined ? {} : { name }),
|
|
4001
4434
|
created,
|
|
4002
4435
|
modified,
|
|
@@ -4110,37 +4543,6 @@ function persistedChildSubsessionLinkData(parentSessionId, childSessionId) {
|
|
|
4110
4543
|
spawnedSessionId: childSessionId,
|
|
4111
4544
|
};
|
|
4112
4545
|
}
|
|
4113
|
-
function parsePersistedParentSubsessionLink(entry) {
|
|
4114
|
-
if (!isRecord(entry) || entry["type"] !== "custom" || entry["customType"] !== SUBSESSION_LINK_CUSTOM_TYPE)
|
|
4115
|
-
return undefined;
|
|
4116
|
-
const data = entry["data"];
|
|
4117
|
-
if (!isRecord(data))
|
|
4118
|
-
return undefined;
|
|
4119
|
-
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
4120
|
-
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
4121
|
-
if (spawnedBySessionId === undefined || spawnedBySessionId === "" || spawnedSessionId === undefined || spawnedSessionId === "")
|
|
4122
|
-
return undefined;
|
|
4123
|
-
const spawnedSessionFile = getString(data, "spawnedSessionFile");
|
|
4124
|
-
const cwd = getString(data, "cwd");
|
|
4125
|
-
return {
|
|
4126
|
-
spawnedBySessionId,
|
|
4127
|
-
spawnedSessionId,
|
|
4128
|
-
...(spawnedSessionFile === undefined || spawnedSessionFile === "" ? {} : { spawnedSessionFile }),
|
|
4129
|
-
...(cwd === undefined || cwd === "" ? {} : { cwd }),
|
|
4130
|
-
};
|
|
4131
|
-
}
|
|
4132
|
-
function parsePersistedChildSubsessionLink(entry) {
|
|
4133
|
-
if (!isRecord(entry) || entry["type"] !== "custom" || entry["customType"] !== SUBSESSION_CHILD_LINK_CUSTOM_TYPE)
|
|
4134
|
-
return undefined;
|
|
4135
|
-
const data = entry["data"];
|
|
4136
|
-
if (!isRecord(data))
|
|
4137
|
-
return undefined;
|
|
4138
|
-
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
4139
|
-
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
4140
|
-
if (spawnedBySessionId === undefined || spawnedBySessionId === "" || spawnedSessionId === undefined || spawnedSessionId === "")
|
|
4141
|
-
return undefined;
|
|
4142
|
-
return { spawnedBySessionId, spawnedSessionId };
|
|
4143
|
-
}
|
|
4144
4546
|
function nonEmptyString(value) {
|
|
4145
4547
|
return value === undefined || value === "" ? undefined : value;
|
|
4146
4548
|
}
|
|
@@ -4199,12 +4601,37 @@ function clearParentSessionHeader(sessionManager) {
|
|
|
4199
4601
|
function clearSessionQueue(session) {
|
|
4200
4602
|
session.clearQueue();
|
|
4201
4603
|
}
|
|
4202
|
-
function queuedMessagesFromSession(session, extraQueuedMessages = []) {
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4604
|
+
function queuedMessagesFromSession(session, extraQueuedMessages = [], inspectSdk = true) {
|
|
4605
|
+
const texts = inspectSdk
|
|
4606
|
+
? { steer: mutableTextMirror(session.getSteeringMessages()), followUp: mutableTextMirror(session.getFollowUpMessages()) }
|
|
4607
|
+
: { steer: publicQueueTexts(() => session.getSteeringMessages()), followUp: publicQueueTexts(() => session.getFollowUpMessages()) };
|
|
4608
|
+
const attachmentCounts = { steer: [], followUp: [] };
|
|
4609
|
+
if (inspectSdk && (texts.steer.length > 0 || texts.followUp.length > 0 || extraQueuedMessages.length > 0)) {
|
|
4610
|
+
const queues = snapshotAgentQueues(mutableQueueAgent(session.agent));
|
|
4611
|
+
for (const kind of ["steer", "followUp"]) {
|
|
4612
|
+
attachmentCounts[kind] = queuedMessageAttachmentCounts(queues[kind], texts[kind]);
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
return ["steer", "followUp"].flatMap((kind) => [
|
|
4616
|
+
...texts[kind].map((text, index) => ({ kind, text, ...queuedAttachmentMetadata(attachmentCounts[kind][index] ?? 0) })),
|
|
4617
|
+
...extraQueuedMessages.filter((message) => message.kind === kind).map((prompt) => ({
|
|
4618
|
+
kind, text: prompt.text,
|
|
4619
|
+
...queuedAttachmentMetadata(prompt.message === undefined ? prompt.images?.length ?? 0 : queuedMessageAttachmentCount(prompt.message)),
|
|
4620
|
+
})),
|
|
4621
|
+
]);
|
|
4622
|
+
}
|
|
4623
|
+
/** Degraded status still uses public text getters where their values are usable. */
|
|
4624
|
+
function publicQueueTexts(read) {
|
|
4625
|
+
try {
|
|
4626
|
+
const value = read();
|
|
4627
|
+
return Array.isArray(value) ? value.filter((text) => typeof text === "string") : [];
|
|
4628
|
+
}
|
|
4629
|
+
catch {
|
|
4630
|
+
return [];
|
|
4631
|
+
} // The compatibility guard already records the original failure.
|
|
4632
|
+
}
|
|
4633
|
+
function queuedAttachmentMetadata(attachmentCount) {
|
|
4634
|
+
return attachmentCount > 0 ? { hasAttachments: true, attachmentCount } : {};
|
|
4208
4635
|
}
|
|
4209
4636
|
function userTextMessage(text) {
|
|
4210
4637
|
return { role: "user", content: text };
|
|
@@ -4258,6 +4685,55 @@ function activeEditableTreeTargetParentId(manager, targetId, activeLeafId) {
|
|
|
4258
4685
|
return undefined;
|
|
4259
4686
|
return parentId === null || typeof parentId === "string" ? parentId : undefined;
|
|
4260
4687
|
}
|
|
4688
|
+
/**
|
|
4689
|
+
* Pi edits custom messages from their parent. For a retained checkpoint, remap
|
|
4690
|
+
* that single leaf movement instead, before Pi rebuilds context/restores tools
|
|
4691
|
+
* and emits session_tree. Restore the manager methods before extension handlers
|
|
4692
|
+
* run: their appended history must not be overwritten by a post-event repair.
|
|
4693
|
+
* The caller holds the tree-navigation gate and excludes branch summarization.
|
|
4694
|
+
*/
|
|
4695
|
+
async function navigateTreeRetainingCustomMessage(manager, targetId, parentId, navigate) {
|
|
4696
|
+
if (parentId !== null && typeof parentId !== "string")
|
|
4697
|
+
throw new Error("This checkpoint has no valid history parent");
|
|
4698
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- Restore exact identities; calls below explicitly bind the manager.
|
|
4699
|
+
const { branch, resetLeaf } = manager;
|
|
4700
|
+
let pending = true;
|
|
4701
|
+
const restore = () => {
|
|
4702
|
+
if (!pending)
|
|
4703
|
+
return;
|
|
4704
|
+
pending = false;
|
|
4705
|
+
manager.branch = branch;
|
|
4706
|
+
manager.resetLeaf = resetLeaf;
|
|
4707
|
+
};
|
|
4708
|
+
try {
|
|
4709
|
+
manager.branch = (entryId) => {
|
|
4710
|
+
if (entryId === parentId) {
|
|
4711
|
+
restore();
|
|
4712
|
+
branch.call(manager, targetId);
|
|
4713
|
+
}
|
|
4714
|
+
else
|
|
4715
|
+
branch.call(manager, entryId);
|
|
4716
|
+
};
|
|
4717
|
+
manager.resetLeaf = () => {
|
|
4718
|
+
if (parentId === null) {
|
|
4719
|
+
restore();
|
|
4720
|
+
branch.call(manager, targetId);
|
|
4721
|
+
}
|
|
4722
|
+
else
|
|
4723
|
+
resetLeaf.call(manager);
|
|
4724
|
+
};
|
|
4725
|
+
const result = await navigate();
|
|
4726
|
+
if (result.cancelled)
|
|
4727
|
+
return result;
|
|
4728
|
+
// The retained notice is context, not text to restore into the editor.
|
|
4729
|
+
const { editorText, ...retainedResult } = result;
|
|
4730
|
+
void editorText;
|
|
4731
|
+
return retainedResult;
|
|
4732
|
+
}
|
|
4733
|
+
finally {
|
|
4734
|
+
restore();
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4261
4737
|
function setSessionTreeLeaf(manager, leafId) {
|
|
4262
4738
|
if (leafId === null)
|
|
4263
4739
|
manager.resetLeaf();
|
|
@@ -4271,15 +4747,22 @@ function transcriptBranchIncludesEntry(entries, entryId) {
|
|
|
4271
4747
|
const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
|
|
4272
4748
|
/** custom entry type used to mark a child as created by spawn_subsession. */
|
|
4273
4749
|
const SUBSESSION_CHILD_LINK_CUSTOM_TYPE = "pi-web.subsession.spawned";
|
|
4274
|
-
/**
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4750
|
+
/** A filtered read is a receipt only when it contains the complete latest final text. */
|
|
4751
|
+
function transcriptReturnsFinalOutput(messages, view, finalText) {
|
|
4752
|
+
if (finalText === "")
|
|
4753
|
+
return false;
|
|
4754
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
4755
|
+
const text = finalAssistantText([messages[index]]);
|
|
4756
|
+
if (text === "")
|
|
4757
|
+
continue;
|
|
4758
|
+
const entry = view.entries.find((candidate) => candidate.index === index && candidate.role === "assistant");
|
|
4759
|
+
if (entry === undefined || text !== finalText)
|
|
4760
|
+
return false;
|
|
4761
|
+
const texts = entry.parts.filter((part) => part.kind === "text");
|
|
4762
|
+
return texts.every((part) => part.truncated === undefined)
|
|
4763
|
+
&& texts.map((part) => part.text).join("\n").trim() === finalText.trim();
|
|
4764
|
+
}
|
|
4765
|
+
return false;
|
|
4283
4766
|
}
|
|
4284
4767
|
/** Most recent assistant text from a history message list, or "" if none. */
|
|
4285
4768
|
function finalAssistantText(messages) {
|