@chainingintention/pi-web-cn 1.202610.4 → 2.202610.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.js +31 -7
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/PiWebApp-BXSziMEg.js +4921 -0
- package/dist/client/assets/classic-CyM97Akm.css +54 -0
- package/dist/client/assets/external-B9K8duyP.js +2 -0
- package/dist/client/assets/graphite-DquaqFXU.css +54 -0
- package/dist/client/assets/index-cdtUOfOW.js +2 -0
- package/dist/client/assets/paper-CB1oLuRr.css +54 -0
- package/dist/client/assets/pi-web-dark-D_KtbM95.css +54 -0
- package/dist/client/assets/pi-web-light-DSNLMIBI.css +54 -0
- package/dist/client/assets/vendor-editor-core-BfnHL1Dg.js +10 -0
- package/dist/client/assets/{vendor-editor-languages-DOCw3vc9.js → vendor-editor-languages-bNquDJP0.js} +1 -1
- package/dist/client/index.html +3 -47
- package/dist/config.js +11 -2
- package/dist/config.js.map +1 -1
- package/dist/nativeServices/installedServiceDefinitions.js +10 -7
- package/dist/nativeServices/installedServiceDefinitions.js.map +1 -1
- package/dist/pi-packages/captains-log/dist/browser/index.js +43 -47
- package/dist/pi-packages/captains-log/dist/browser/panel.js +6 -10
- 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 +61 -0
- package/dist/pi-packages/relays/package.json +10 -1
- package/dist/pi-packages/relays/pi-web-plugin.js +52 -2
- 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 +75 -37
- 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 +42 -0
- package/dist/pi-packages/workflows/browser/worktreeDialog.js +170 -0
- package/dist/pi-packages/workflows/package.json +23 -0
- package/dist/pi-packages/workflows/prompts/worktree.md +21 -0
- package/dist/pi-packages/workflows/server-plugin.js +586 -0
- package/dist/pi-packages/workflows/vite.config.mjs +17 -0
- package/dist/pi-web-plugins/code-highlight/browser/pi-web-plugin.js +22 -0
- package/dist/pi-web-plugins/code-highlight/package.json +13 -0
- package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +115 -105
- package/dist/pi-web-plugins/git/browser/git-panel.js +60 -46
- package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +23 -3
- package/dist/pi-web-plugins/git/git-backend.js +20 -1
- package/dist/pi-web-plugins/git/server-plugin.js +20 -3
- 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/mermaid/browser/pi-web-plugin.js +8 -3
- package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +39 -33
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +13 -13
- package/dist/pi-web-plugins/updates/updatesLogic.js +4 -5
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +21 -1
- package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +39 -22
- package/dist/pi-web-plugins/zh-cn/bundled.json +44 -1
- package/dist/pi-web-plugins/zh-cn/cli.json +60 -1
- package/dist/pi-web-plugins/zh-cn/client.json +7 -1
- package/dist/pi-web-plugins/zh-cn/components.json +252 -2
- package/dist/pi-web-plugins/zh-cn/config-errors.json +17 -1
- package/dist/pi-web-plugins/zh-cn/files.json +10 -1
- package/dist/pi-web-plugins/zh-cn/main.json +1 -1
- package/dist/pi-web-plugins/zh-cn/provider-auth.json +2 -1
- package/dist/pi-web-plugins/zh-cn/server.json +83 -1
- package/dist/piAi.js +4 -0
- package/dist/piAi.js.map +1 -0
- package/dist/piRuntime.js +150 -0
- package/dist/piRuntime.js.map +1 -0
- package/dist/piSdk.js +5 -0
- package/dist/piSdk.js.map +1 -0
- package/dist/piWebUpdate.js +508 -0
- package/dist/piWebUpdate.js.map +1 -0
- package/dist/piWebVersionReport.js +43 -1
- package/dist/piWebVersionReport.js.map +1 -1
- package/dist/plugin-api.d.ts +393 -51
- package/dist/server/activity/workspaceActivityService.js +8 -1
- package/dist/server/activity/workspaceActivityService.js.map +1 -1
- package/dist/server/app.js +19 -4
- package/dist/server/app.js.map +1 -1
- package/dist/server/browserMessageProjection.js +39 -20
- package/dist/server/browserMessageProjection.js.map +1 -1
- package/dist/server/configRoutes.js +8 -0
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/index.js +10 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/knownAutoInstallPiPackages.js +6 -0
- package/dist/server/knownAutoInstallPiPackages.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +37 -2
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/navigationPreferenceRoutes.js +59 -0
- package/dist/server/navigationPreferenceRoutes.js.map +1 -0
- package/dist/server/piPackageService.js +1 -1
- package/dist/server/piPackageService.js.map +1 -1
- package/dist/server/piWebPluginCatalog.js +1 -1
- package/dist/server/piWebPluginCatalog.js.map +1 -1
- package/dist/server/piWebPluginService.js +7 -1
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebStatus.js +17 -56
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +1 -1
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -1
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +40 -42
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -1
- package/dist/server/plugins/pluginBackendProxyRoutes.js +40 -44
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
- package/dist/server/plugins/pluginBackendRegistry.js +66 -35
- package/dist/server/plugins/pluginBackendRegistry.js.map +1 -1
- package/dist/server/plugins/projectPluginSettings.js +95 -0
- package/dist/server/plugins/projectPluginSettings.js.map +1 -0
- package/dist/server/plugins/serverPluginCatalogCapability.js +106 -0
- package/dist/server/plugins/serverPluginCatalogCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +47 -11
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
- package/dist/server/projectTrustRoutes.js +1 -1
- package/dist/server/projectTrustRoutes.js.map +1 -1
- package/dist/server/projects/projectLifecycleService.js +17 -7
- package/dist/server/projects/projectLifecycleService.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +22 -9
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessiond/agentHttpDispatcher.js +1 -1
- package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -1
- package/dist/server/sessiond/autoInstallPiPackages.js +3 -3
- package/dist/server/sessiond/pluginBackendChannelRoutes.js +27 -14
- package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -1
- package/dist/server/sessiond/pluginBackendRoutes.js +56 -49
- package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
- package/dist/server/sessiond/pluginCatalogRoutes.js +41 -0
- package/dist/server/sessiond/pluginCatalogRoutes.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +46 -1
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
- package/dist/server/sessiond/workspaceCatalogRoutes.js +6 -2
- package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -1
- package/dist/server/sessiond/workspaceNotificationRoutes.js +55 -0
- package/dist/server/sessiond/workspaceNotificationRoutes.js.map +1 -0
- package/dist/server/sessiond.js +58 -8
- 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 +18 -18
- package/dist/server/sessions/askUserTool.js.map +1 -1
- package/dist/server/sessions/attachmentService.js +1 -1
- package/dist/server/sessions/attachmentService.js.map +1 -1
- package/dist/server/sessions/authRoutes.js +12 -0
- package/dist/server/sessions/authRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +11 -1
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/builtinExtensionFactories.js +36 -0
- package/dist/server/sessions/builtinExtensionFactories.js.map +1 -0
- package/dist/server/sessions/companionBackend.js +286 -0
- package/dist/server/sessions/companionBackend.js.map +1 -0
- package/dist/server/sessions/dockerEnvironmentFacts.js +16 -1
- 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 +88 -19
- package/dist/server/sessions/globalProviderPolicy.js.map +1 -1
- package/dist/server/sessions/managementAgentTools.js +2 -2
- package/dist/server/sessions/managementAgentTools.js.map +1 -1
- package/dist/server/sessions/mcpAuthFlowService.js +289 -0
- package/dist/server/sessions/mcpAuthFlowService.js.map +1 -0
- package/dist/server/sessions/mcpAuthRoutes.js +104 -0
- package/dist/server/sessions/mcpAuthRoutes.js.map +1 -0
- package/dist/server/sessions/mcpOAuthCredentialAdapter.js +92 -0
- package/dist/server/sessions/mcpOAuthCredentialAdapter.js.map +1 -0
- package/dist/server/sessions/messageHistoryTarget.js +79 -0
- package/dist/server/sessions/messageHistoryTarget.js.map +1 -0
- package/dist/server/sessions/piSessionEventConnections.js +5 -2
- package/dist/server/sessions/piSessionEventConnections.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +32 -27
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +1296 -341
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/plainTextTheme.js +1 -1
- package/dist/server/sessions/plainTextTheme.js.map +1 -1
- package/dist/server/sessions/projectAuthService.js +1 -1
- package/dist/server/sessions/projectAuthService.js.map +1 -1
- package/dist/server/sessions/projectSettingsScope.js +1 -1
- package/dist/server/sessions/projectSettingsScope.js.map +1 -1
- package/dist/server/sessions/projectTrust.js +1 -1
- package/dist/server/sessions/projectTrust.js.map +1 -1
- package/dist/server/sessions/queuedMessageMutation.js +170 -0
- package/dist/server/sessions/queuedMessageMutation.js.map +1 -0
- package/dist/server/sessions/relaySkill.js +57 -7
- package/dist/server/sessions/relaySkill.js.map +1 -1
- package/dist/server/sessions/sessionArchiveMapping.js +6 -0
- package/dist/server/sessions/sessionArchiveMapping.js.map +1 -1
- 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 +11 -24
- 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 +191 -0
- package/dist/server/sessions/sessionMediaIndex.js.map +1 -0
- package/dist/server/sessions/sessionMetadata.js +51 -0
- package/dist/server/sessions/sessionMetadata.js.map +1 -0
- package/dist/server/sessions/sessionModelScope.js +2 -2
- package/dist/server/sessions/sessionModelScope.js.map +1 -1
- package/dist/server/sessions/sessionNameGenerator.js +13 -33
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionReadAccess.js +41 -0
- package/dist/server/sessions/sessionReadAccess.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +61 -6
- 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/sessionUiEvents.js +17 -4
- package/dist/server/sessions/sessionUiEvents.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +11 -3
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +35 -19
- 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/sessions/transcriptMarkdown.js +48 -0
- package/dist/server/sessions/transcriptMarkdown.js.map +1 -0
- package/dist/server/sessions/transcriptMessages.js +3 -0
- package/dist/server/sessions/transcriptMessages.js.map +1 -1
- package/dist/server/startupLogger.js +13 -0
- package/dist/server/startupLogger.js.map +1 -0
- package/dist/server/startupUrls.js +27 -0
- package/dist/server/startupUrls.js.map +1 -0
- package/dist/server/status/machineStatusService.js +15 -9
- package/dist/server/status/machineStatusService.js.map +1 -1
- package/dist/server/storage/navigationPreferenceStore.js +138 -0
- package/dist/server/storage/navigationPreferenceStore.js.map +1 -0
- package/dist/server/webSocketBridge.js +9 -0
- package/dist/server/webSocketBridge.js.map +1 -1
- package/dist/server/workbench/skillSync.js +1 -1
- package/dist/server/workbench/skillSync.js.map +1 -1
- package/dist/server/workbench/workbenchTools.js +2 -2
- package/dist/server/workbench/workbenchTools.js.map +1 -1
- package/dist/server/workspaces/filePreviewService.js +5 -2
- package/dist/server/workspaces/filePreviewService.js.map +1 -1
- package/dist/server/workspaces/projectPiWebConfig.js +87 -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 +53 -0
- package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
- package/dist/server-plugin-api.d.ts +83 -15
- package/dist/server-plugin-api.js +25 -0
- package/dist/server-plugin-api.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +27 -3
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/apiTypes.js +3 -2
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +3 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +25 -5
- 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/piWebConfigParsing.js +54 -0
- package/dist/shared/piWebConfigParsing.js.map +1 -1
- package/dist/shared/pluginApiTypes.d.ts +55 -0
- package/dist/shared/pluginBackendProtocol.js +56 -1
- package/dist/shared/pluginBackendProtocol.js.map +1 -1
- package/dist/shared/pluginCatalogTypes.d.ts +49 -0
- package/dist/shared/pluginCatalogTypes.js +156 -0
- package/dist/shared/pluginCatalogTypes.js.map +1 -0
- package/dist/shared/sessionMedia.js +12 -0
- package/dist/shared/sessionMedia.js.map +1 -0
- package/dist/shared/streamingBehavior.js +8 -0
- package/dist/shared/streamingBehavior.js.map +1 -0
- package/dist/shared/themePreference.js +24 -0
- package/dist/shared/themePreference.js.map +1 -0
- package/dist/workspaceNotificationCli.js +95 -0
- package/dist/workspaceNotificationCli.js.map +1 -0
- package/docs/README.md +31 -0
- package/docs/config.md +77 -4
- package/docs/plugins.md +60 -11
- package/examples/session-bridge-plugin/package.json +2 -2
- package/examples/session-bridge-plugin/src/browser/index.ts +1 -1
- package/examples/session-bridge-plugin/src/companion.ts +1 -1
- package/extensions/pi-web.ts +3 -1
- package/package.json +46 -34
- package/skills/README.md +10 -9
- package/skills/relay/SKILL.md +24 -68
- package/skills/relay-runner/SKILL.md +40 -219
- package/dist/client/assets/PiWebApp-CN3g7SqA.js +0 -4362
- package/dist/client/assets/external-DUGCSDhZ.js +0 -2
- package/dist/client/assets/index-JduZlbIF.js +0 -2
- 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,11 +1,23 @@
|
|
|
1
|
+
import { parsePersistedChildSubsessionLink, parsePersistedParentSubsessionLink } from "./subsessionProvenance.js";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { readSessionUiMetadata, requireSessionUiMetadata, SESSION_UI_METADATA_CUSTOM_TYPE } from "./sessionMetadata.js";
|
|
1
5
|
import { UserFacingError, createTranslator, userFacingErrorMessage } from "../../shared/i18n.js";
|
|
2
6
|
import { realpathSync, statSync } from "node:fs";
|
|
3
7
|
import { clientSessionFirstMessagePreview } from "./clientSessionPreview.js";
|
|
4
8
|
import { readFile } from "node:fs/promises";
|
|
5
9
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
-
import {
|
|
10
|
+
import { QueueCompatibilityGuard, mirroredQueueEntries, mutableQueueAgent, mutableTextMirror, parseQueueMessageMutation, queuedMessageAttachmentCount, queuedMessageAttachmentCounts, snapshotAgentQueues } from "./queuedMessageMutation.js";
|
|
11
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEventBus, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, loadSkills, ProjectTrustStore, hasTrustRequiringProjectResources, readStoredCredential, SessionManager, VERSION, } from "../../piSdk.js";
|
|
7
12
|
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
13
|
+
import { projectTranscriptMarkdown } from "./transcriptMarkdown.js";
|
|
14
|
+
import { messageHistoryTargetId } from "./messageHistoryTarget.js";
|
|
15
|
+
import { normalSessionReadAllowed } from "./sessionReadAccess.js";
|
|
16
|
+
import { isSessionMediaId } from "../../shared/sessionMedia.js";
|
|
8
17
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
18
|
+
import { McpAuthFlowService } from "./mcpAuthFlowService.js";
|
|
19
|
+
import { mcpOAuthStorageDirectory } from "./mcpOAuthCredentialAdapter.js";
|
|
20
|
+
import { getBuiltinExtensionFactories } from "./builtinExtensionFactories.js";
|
|
9
21
|
import { eventScopeFromManagementContext, managementContextKey, NORMAL_SESSION_EVENT_SCOPE } from "../realtime/sessionEventScope.js";
|
|
10
22
|
import { builtinCommands } from "./builtinCommands.js";
|
|
11
23
|
import { loadNodeTranslator } from "../../localization.js";
|
|
@@ -13,8 +25,8 @@ import { SessionCommandService } from "./sessionCommandService.js";
|
|
|
13
25
|
import { SessionActivityMarker } from "./sessionActivityMarker.js";
|
|
14
26
|
import { projectSessionTree } from "./sessionTreeProjection.js";
|
|
15
27
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
28
|
+
import { findArchiveCandidateByExactId, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
29
|
+
import { fallbackSessionName, generateShortSessionName, requireInitialSessionName } from "./sessionNameGenerator.js";
|
|
18
30
|
import { computeEditPreview } from "./editPreview.js";
|
|
19
31
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
20
32
|
import { loadEffectiveProjectAttachmentsConfig } from "../workspaces/projectPiWebConfig.js";
|
|
@@ -35,7 +47,10 @@ import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS, piWebDataDir } from "../../config
|
|
|
35
47
|
import { createScopedSettingsManager, resolveSettingsScopeDirectory } from "./projectSettingsScope.js";
|
|
36
48
|
import { parseSessionDefaults, parseSessionDefaultsUpdate } from "../../shared/sessionDefaults.js";
|
|
37
49
|
import { createAskUserToolDefinition } from "./askUserTool.js";
|
|
38
|
-
import {
|
|
50
|
+
import { createAskUserOutcome, renderAskUserAnswersText, resolveAskUserRequest } from "./askUserDomain.js";
|
|
51
|
+
import { createSubsessionCompletionDelivery } from "./subsessionCompletionDelivery.js";
|
|
52
|
+
import { completionReceiptIds, persistedSubsessionCompletions, renderSubsessionCompletions, SUBSESSION_COMPLETION_ENTRY_TYPE, SUBSESSION_NOTIFICATION_CUSTOM_TYPE, SUBSESSION_RECEIPT_ENTRY_TYPE, SubsessionCompletionInbox } from "./subsessionCompletionInbox.js";
|
|
53
|
+
import { createExtensionSessionDispatch } from "./extensionSessionDispatch.js";
|
|
39
54
|
import { PendingExtensionDialogStore } from "./pendingExtensionDialogStore.js";
|
|
40
55
|
import { ExtensionDialogWaiters, effectiveExtensionDialogTimeoutMs, extensionDialogCancelValue } from "./extensionDialogWaiters.js";
|
|
41
56
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
@@ -46,10 +61,10 @@ import { createManagedAgentToolOptions, createManagedBashToolDefinition, createM
|
|
|
46
61
|
import { PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR, managementAgentToolNames, privilegedManagementToolNames, withRuntimeCreationEnvironment, writeManagementPermissionSystemPolicy } from "./managementPermissionSystem.js";
|
|
47
62
|
import { SessionNotificationStore } from "./sessionNotificationStore.js";
|
|
48
63
|
import { plainTextTheme } from "./plainTextTheme.js";
|
|
49
|
-
import { deduplicateBundledRelaySkills, ensureManagedRelaySkills, MANAGED_RELAY_SKILL_NAMES } from "./relaySkill.js";
|
|
64
|
+
import { deduplicateBundledRelaySkills, ensureManagedRelaySkills, MANAGED_RELAY_SKILL_NAMES, removeUnmodifiedManagedRelaySkills } from "./relaySkill.js";
|
|
50
65
|
import { SessionUnreadStore } from "./sessionUnreadStore.js";
|
|
51
66
|
import { archiveCandidateFromActiveSession, archiveCandidateFromArchivedRecord, archiveCandidateFromListEntry, archiveInputFromActiveSession, archiveInputFromCandidate, archiveInputFromListEntry, clientSessionFromArchivedRecord, } from "./sessionArchiveMapping.js";
|
|
52
|
-
import { getBoolean, getProperty, getString, toClientEvent } from "./sessionUiEvents.js";
|
|
67
|
+
import { annotateAssistantStreamId, getBoolean, getProperty, getString, toClientEvent } from "./sessionUiEvents.js";
|
|
53
68
|
import { annotateAssistantThinkingLevel, historyMessagesFromEntries } from "./transcriptMessages.js";
|
|
54
69
|
import { createWorkbenchToolDefinitions } from "../workbench/workbenchTools.js";
|
|
55
70
|
import { applyEnabledModelToggle, catalogWithEnabledFirst, modelScopeId, persistedEnabledModelPatterns, resolveEnabledModelIds, resolveSessionModelOptions, scopedModelsFromEnabledIds } from "./sessionModelScope.js";
|
|
@@ -405,6 +420,7 @@ async function createNormalPermissionSessionServices(options) {
|
|
|
405
420
|
resourceLoaderOptions: {
|
|
406
421
|
...piWebResourceLoaderOptions(options.appendSystemPromptSections),
|
|
407
422
|
eventBus: options.eventBus,
|
|
423
|
+
extensionFactories: [...await getBuiltinExtensionFactories(options.mcpScope), ...(options.extensionFactories ?? [])],
|
|
408
424
|
...(options.agentsFilesOverride === undefined ? {} : { agentsFilesOverride: options.agentsFilesOverride }),
|
|
409
425
|
skillsOverride: (base) => deduplicateBundledRelaySkills(cwd, base),
|
|
410
426
|
},
|
|
@@ -422,11 +438,11 @@ async function createNormalPermissionSessionServices(options) {
|
|
|
422
438
|
});
|
|
423
439
|
return { services, projectTrustDiagnostics };
|
|
424
440
|
}
|
|
425
|
-
function createDefaultRuntimeFactory(sessionEvents, normalModelRuntimeForCwd, managementModelRuntime, resolveSettingsScope, sessionManagers, spawn, subsessions,
|
|
441
|
+
function createDefaultRuntimeFactory(sessionEvents, normalModelRuntimeForCwd, managementModelRuntime, resolveSettingsScope, sessionManagers, spawn, subsessions, askUserEnabled = false, workbench, logger = noopLogger, managementAudit, managementProjectIdForCwd, appendSystemPromptSections = [], managementAllowPrivileged = false) {
|
|
426
442
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
427
443
|
const scopedModelRuntime = managementContext === undefined ? await normalModelRuntimeForCwd(cwd) : await managementModelRuntime();
|
|
428
444
|
if (managementContext !== undefined) {
|
|
429
|
-
return createManagementRuntimeFactory(scopedModelRuntime, resolveSettingsScope, sessionManagers, spawn, subsessions,
|
|
445
|
+
return createManagementRuntimeFactory(scopedModelRuntime, resolveSettingsScope, sessionManagers, spawn, subsessions, askUserEnabled, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd, managementAllowPrivileged, appendSystemPromptSections)({
|
|
430
446
|
cwd,
|
|
431
447
|
agentDir,
|
|
432
448
|
sessionManager,
|
|
@@ -449,6 +465,8 @@ function createDefaultRuntimeFactory(sessionEvents, normalModelRuntimeForCwd, ma
|
|
|
449
465
|
// Only normal runtimes register a host connection; privileged management
|
|
450
466
|
// packages keep their own event bus without exposing a normal-mode session.
|
|
451
467
|
const eventBus = createEventBus();
|
|
468
|
+
let dispatchEnabled = false;
|
|
469
|
+
const sessionDispatch = createExtensionSessionDispatch(cwd, { ...(spawn === undefined ? {} : { spawn }), isEnabled: () => dispatchEnabled });
|
|
452
470
|
const { services, projectTrustDiagnostics } = await createNormalPermissionSessionServices({
|
|
453
471
|
cwd,
|
|
454
472
|
agentDir,
|
|
@@ -457,6 +475,8 @@ function createDefaultRuntimeFactory(sessionEvents, normalModelRuntimeForCwd, ma
|
|
|
457
475
|
projectTrustRequiring,
|
|
458
476
|
appendSystemPromptSections,
|
|
459
477
|
eventBus,
|
|
478
|
+
mcpScope: { directory: mcpOAuthStorageDirectory(await resolveSettingsScope(cwd, "normal")) },
|
|
479
|
+
extensionFactories: [sessionDispatch, ...(subsessions === undefined ? [] : [createSubsessionCompletionDelivery(subsessions)])],
|
|
460
480
|
});
|
|
461
481
|
const modelOptions = await resolveSessionModelOptions({
|
|
462
482
|
services,
|
|
@@ -467,7 +487,8 @@ function createDefaultRuntimeFactory(sessionEvents, normalModelRuntimeForCwd, ma
|
|
|
467
487
|
services.diagnostics.push(...modelOptions.diagnostics);
|
|
468
488
|
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
469
489
|
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
470
|
-
|
|
490
|
+
dispatchEnabled = resolvedDelegationToolsEnabled;
|
|
491
|
+
const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions, askUserEnabled);
|
|
471
492
|
const result = await createAgentSessionFromServices({
|
|
472
493
|
services,
|
|
473
494
|
sessionManager,
|
|
@@ -484,13 +505,15 @@ function createDefaultRuntimeFactory(sessionEvents, normalModelRuntimeForCwd, ma
|
|
|
484
505
|
});
|
|
485
506
|
};
|
|
486
507
|
}
|
|
487
|
-
function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sessionManagers, spawn, subsessions,
|
|
508
|
+
function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sessionManagers, spawn, subsessions, askUserEnabled, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd, managementAllowPrivileged = false, appendSystemPromptSections = []) {
|
|
488
509
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
489
510
|
const normalPermissions = managementUsesNormalPermissions(managementContext, { allowPrivileged: managementAllowPrivileged });
|
|
490
511
|
const privileges = managementPrivileges(managementContext, { allowPrivileged: managementAllowPrivileged });
|
|
491
512
|
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
492
513
|
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
493
|
-
if (
|
|
514
|
+
if (normalPermissions)
|
|
515
|
+
await removeUnmodifiedManagedRelaySkills(cwd);
|
|
516
|
+
else
|
|
494
517
|
await ensureManagedRelaySkills(cwd);
|
|
495
518
|
const auditIdentity = await resolveManagementAuditIdentity(cwd, managementContext, managementProjectIdForCwd, logger);
|
|
496
519
|
const accessHandle = workbenchAccessHandle(managementContext);
|
|
@@ -523,16 +546,19 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sess
|
|
|
523
546
|
: {
|
|
524
547
|
spawn: (input) => subsessions.spawn({ ...input, managementContext }),
|
|
525
548
|
list: (parentSessionId, parentSessionFile) => subsessions.list(parentSessionId, parentSessionFile, managementContext),
|
|
526
|
-
|
|
527
|
-
|
|
549
|
+
join: (parentSessionId, parentSessionFile, toolCallId) => subsessions.join(parentSessionId, parentSessionFile, toolCallId, managementContext),
|
|
550
|
+
prepare: (parentSessionId, parentSessionFile) => subsessions.prepare(parentSessionId, parentSessionFile, managementContext),
|
|
551
|
+
check: (parentSessionId, sessionId, parentSessionFile, toolCallId) => subsessions.check(parentSessionId, sessionId, parentSessionFile, toolCallId, managementContext),
|
|
552
|
+
read: (parentSessionId, sessionId, query, parentSessionFile, toolCallId) => subsessions.read(parentSessionId, sessionId, query, parentSessionFile, toolCallId, managementContext),
|
|
528
553
|
};
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
554
|
+
const managementFactories = [
|
|
555
|
+
...(normalPermissions ? [createExtensionSessionDispatch(cwd, { ...(scopedSpawn === undefined ? {} : { spawn: scopedSpawn }), isEnabled: () => resolvedDelegationToolsEnabled })] : []),
|
|
556
|
+
...(scopedSubsessions === undefined ? [] : [createSubsessionCompletionDelivery(scopedSubsessions)]),
|
|
557
|
+
];
|
|
532
558
|
const controlledManagementTools = [
|
|
533
559
|
...(resolvedDelegationToolsEnabled && scopedSpawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn: scopedSpawn }))] : []),
|
|
534
560
|
...(resolvedDelegationToolsEnabled && scopedSubsessions !== undefined ? createSubsessionToolDefinitions(cwd, scopedSubsessions).map((tool) => defineTool(tool)) : []),
|
|
535
|
-
...(
|
|
561
|
+
...(askUserEnabled ? [createAskUserToolDefinition()] : []),
|
|
536
562
|
...workbenchTools,
|
|
537
563
|
];
|
|
538
564
|
const controlledManagementToolNames = controlledManagementTools.map((tool) => tool.name);
|
|
@@ -563,6 +589,11 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sess
|
|
|
563
589
|
projectTrustRequiring: false,
|
|
564
590
|
appendSystemPromptSections,
|
|
565
591
|
eventBus: createEventBus(),
|
|
592
|
+
mcpScope: (() => {
|
|
593
|
+
const directory = mcpOAuthStorageDirectory(settingsManager.piWebScopeKey, eventScopeFromManagementContext(managementContext));
|
|
594
|
+
return { directory, configAgentDir: directory };
|
|
595
|
+
})(),
|
|
596
|
+
extensionFactories: managementFactories,
|
|
566
597
|
agentsFilesOverride: (base) => filterPrivilegedManagementContextFiles(cwd, managementContext, base),
|
|
567
598
|
}) : { services: await createAgentSessionServices({
|
|
568
599
|
cwd,
|
|
@@ -570,6 +601,7 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sess
|
|
|
570
601
|
modelRuntime,
|
|
571
602
|
settingsManager,
|
|
572
603
|
resourceLoaderOptions: {
|
|
604
|
+
extensionFactories: managementFactories,
|
|
573
605
|
agentsFilesOverride: (base) => filterManagedGlobalContextFiles(cwd, agentDir, base),
|
|
574
606
|
skillsOverride: (base) => receipt === undefined ? filterManagedProjectSkills(cwd, base, agentDir) : filterManagedWorkbenchSkills(cwd, base, receipt, agentDir),
|
|
575
607
|
},
|
|
@@ -581,7 +613,7 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sess
|
|
|
581
613
|
...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
|
|
582
614
|
});
|
|
583
615
|
services.diagnostics.push(...modelOptions.diagnostics);
|
|
584
|
-
const customTools = normalPermissions ? createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, scopedSpawn, scopedSubsessions,
|
|
616
|
+
const customTools = normalPermissions ? createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, scopedSpawn, scopedSubsessions, askUserEnabled) : [
|
|
585
617
|
...managedAgentToolDefinitions(cwd, createManagedAgentToolOptions(cwd)),
|
|
586
618
|
createManagedPythonToolDefinition(cwd, managementContext, { network: privileges.network }),
|
|
587
619
|
createManagedBashToolDefinition(cwd, managementContext, { network: privileges.network, hostFilesystem: privileges.bash }),
|
|
@@ -601,7 +633,9 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sess
|
|
|
601
633
|
// SDK reload re-executes extensions, so it needs the same scoped environment as startup.
|
|
602
634
|
const reload = result.session.reload.bind(result.session);
|
|
603
635
|
result.session.reload = (options) => withRuntimeCreationEnvironment(resourceEnvironment, async () => {
|
|
604
|
-
if (
|
|
636
|
+
if (normalPermissions)
|
|
637
|
+
await removeUnmodifiedManagedRelaySkills(cwd);
|
|
638
|
+
else
|
|
605
639
|
await ensureManagedRelaySkills(cwd);
|
|
606
640
|
return reload(options);
|
|
607
641
|
});
|
|
@@ -609,12 +643,12 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sess
|
|
|
609
643
|
});
|
|
610
644
|
};
|
|
611
645
|
}
|
|
612
|
-
export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions,
|
|
646
|
+
export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn, subsessions, askUserEnabled = false) {
|
|
613
647
|
return [
|
|
614
648
|
...managedAgentToolDefinitions(cwd),
|
|
615
649
|
...(delegationEnabled && spawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn }))] : []),
|
|
616
650
|
...(delegationEnabled && subsessions !== undefined ? createSubsessionToolDefinitions(cwd, subsessions).map((tool) => defineTool(tool)) : []),
|
|
617
|
-
...(
|
|
651
|
+
...(askUserEnabled ? [createAskUserToolDefinition()] : []),
|
|
618
652
|
];
|
|
619
653
|
}
|
|
620
654
|
function managedAgentToolDefinitions(cwd, options) {
|
|
@@ -655,6 +689,8 @@ export class PiSessionService {
|
|
|
655
689
|
this.events = events;
|
|
656
690
|
this.active = new Map();
|
|
657
691
|
this.runtimeBySession = new WeakMap();
|
|
692
|
+
/** Media cold reads must not reopen files observed under a management identity. */
|
|
693
|
+
this.managementMediaFiles = new Set();
|
|
658
694
|
this.pendingSessionOpens = new Map();
|
|
659
695
|
/** Sessions whose extension binding is parked on a `session_start` dialog. */
|
|
660
696
|
this.startupSessions = new Map();
|
|
@@ -664,6 +700,14 @@ export class PiSessionService {
|
|
|
664
700
|
this.unpersistedTreeBranchLeaves = new WeakMap();
|
|
665
701
|
/** Counts async operations that may append an entry before they settle. */
|
|
666
702
|
this.sessionEntryMutationCounts = new WeakMap();
|
|
703
|
+
this.manualCompactionEnded = new WeakSet();
|
|
704
|
+
this.extensionCommandExecution = new AsyncLocalStorage();
|
|
705
|
+
this.runtimeMessageCounts = new WeakMap();
|
|
706
|
+
this.snapshotMessageCounts = new WeakMap();
|
|
707
|
+
this.pendingPromptEchoes = new WeakMap();
|
|
708
|
+
this.publishedAssistantPartials = new WeakMap();
|
|
709
|
+
this.assistantStreamIds = new WeakMap();
|
|
710
|
+
this.suggestedInputs = new Map();
|
|
667
711
|
/** Serializes enabled-model read/modify/write operations. */
|
|
668
712
|
this.modelScopeMutationQueue = Promise.resolve();
|
|
669
713
|
/** Effective enabled-model state keyed by the project-scoped settings store. */
|
|
@@ -674,7 +718,10 @@ export class PiSessionService {
|
|
|
674
718
|
/** Runtime/session-identity reservations for operations that must not overlap tree navigation. */
|
|
675
719
|
this.treeExclusiveRuntimeOperationCounts = new WeakMap();
|
|
676
720
|
this.treeExclusiveSessionOperationCounts = new Map();
|
|
677
|
-
this.
|
|
721
|
+
this.subsessionInboxes = new Map();
|
|
722
|
+
this.subsessionStoppedParents = new WeakSet();
|
|
723
|
+
this.subsessionDeliveringParents = new WeakSet();
|
|
724
|
+
this.subsessionDeliveryScheduled = new WeakSet();
|
|
678
725
|
this.deferredGeneratedSessionNames = new WeakMap();
|
|
679
726
|
this.compactionPromptQueues = new Map();
|
|
680
727
|
this.compactionDrainTimers = new Map();
|
|
@@ -690,14 +737,19 @@ export class PiSessionService {
|
|
|
690
737
|
/** Session file path -> its parsed header. Headers are written once, so successful reads are cached. */
|
|
691
738
|
/**
|
|
692
739
|
* Tracked subsession id -> whether a completion notification is armed.
|
|
693
|
-
* Armed
|
|
694
|
-
*
|
|
740
|
+
* Armed by agent_start or early prompt failure, never standalone commands
|
|
741
|
+
* or compaction. Completion disarms the latch until the next run.
|
|
695
742
|
*/
|
|
696
743
|
this.subsessionNotifyArmed = new Map();
|
|
744
|
+
this.queueCompatibility = new QueueCompatibilityGuard((session) => {
|
|
745
|
+
this.logger.info({ sessionId: session.sessionId, category: "incompatible-queue-interface" }, "Disabled SDK-dependent queue controls after an incompatible queue interface");
|
|
746
|
+
}, (key, fallback, values) => this.t(key, fallback, values));
|
|
697
747
|
this.scopedNotificationStores = new Map();
|
|
698
748
|
this.scopedUnreadStores = new Map();
|
|
699
749
|
this.sessionEventScopes = new WeakMap();
|
|
700
750
|
this.sessionManagementContexts = new WeakMap();
|
|
751
|
+
this.mcpAuthFlows = new McpAuthFlowService({ translator: (key, fallback, values) => this.t(key, fallback, values) });
|
|
752
|
+
this.mcpAuthOwners = new WeakMap();
|
|
701
753
|
this.notificationGenerationBySession = new WeakMap();
|
|
702
754
|
this.idleSessionFileResolutions = new WeakMap();
|
|
703
755
|
/** The parked extension Promise resolvers behind the store's open dialogs. */
|
|
@@ -706,11 +758,13 @@ export class PiSessionService {
|
|
|
706
758
|
this.unreadPublicationFlushRequested = false;
|
|
707
759
|
this.unreadPublicationStopped = false;
|
|
708
760
|
this.t = deps.translator ?? loadNodeTranslator("server");
|
|
761
|
+
this.managementAllowPrivileged = deps.managementAllowPrivileged === true;
|
|
709
762
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
710
763
|
this.pinStore = deps.pinStore ?? new SessionPinStore(join(deps.dataDir ?? piWebDataDir(), "session-pins.json"));
|
|
711
764
|
this.agentDir = deps.agentDir ?? getAgentDir();
|
|
712
765
|
this.dataDir = deps.dataDir ?? piWebDataDir();
|
|
713
766
|
this.projectPathForCwd = deps.projectPathForCwd;
|
|
767
|
+
this.managementProjectRoot = deps.managementProjectRoot;
|
|
714
768
|
this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({
|
|
715
769
|
agentDir: this.agentDir,
|
|
716
770
|
env: process.env,
|
|
@@ -739,7 +793,6 @@ export class PiSessionService {
|
|
|
739
793
|
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
740
794
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
741
795
|
this.onUnreadChanged = deps.onUnreadChanged;
|
|
742
|
-
this.pendingAskStore = deps.pendingAskStore ?? new PendingAskStore();
|
|
743
796
|
this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
|
|
744
797
|
this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
|
|
745
798
|
this.catalogRefreshStatus = deps.catalogRefreshStatus;
|
|
@@ -753,21 +806,26 @@ export class PiSessionService {
|
|
|
753
806
|
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.sessionEvents, this.normalModelRuntimeForCwd, this.managementModelRuntime, (cwd, mode) => this.resolveSettingsScopeDirectory(cwd, mode), this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
754
807
|
spawn: (input) => this.spawnSubsession(input),
|
|
755
808
|
list: (parentSessionId, parentSessionFile, managementContext) => this.listSubsessions(parentSessionId, parentSessionFile, managementContext),
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
809
|
+
join: (parentSessionId, parentSessionFile, toolCallId, managementContext) => this.joinSubsessions(parentSessionId, parentSessionFile, toolCallId, managementContext),
|
|
810
|
+
prepare: (parentSessionId, parentSessionFile, managementContext) => this.prepareSubsessionDelivery(parentSessionId, parentSessionFile, managementContext),
|
|
811
|
+
check: (parentSessionId, sessionId, parentSessionFile, toolCallId, managementContext) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile, toolCallId, managementContext),
|
|
812
|
+
read: (parentSessionId, sessionId, query, parentSessionFile, toolCallId, managementContext) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile, toolCallId, managementContext),
|
|
813
|
+
}, deps.askUserEnabled === true, deps.workbench, this.logger, this.managementAudit, this.managementProjectIdForCwd, deps.appendSystemPromptSections ?? [], deps.managementAllowPrivileged === true);
|
|
759
814
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
760
815
|
this.workspaceActivity = deps.workspaceActivity;
|
|
761
816
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
762
817
|
this.commandService = new SessionCommandService((sessionId, eventScope) => this.getActiveForCommand(sessionId, eventScope), (sessionId, text, eventScope) => this.submitCommandPrompt(sessionId, text, eventScope), events, {
|
|
763
818
|
onCompactionStart: (session) => {
|
|
764
819
|
this.beginSessionEntryMutation(session, this.t("session.action.compact", "compact the session"));
|
|
820
|
+
this.manualCompactionEnded.delete(session);
|
|
765
821
|
this.publishActivity(session, this.t("activity.compacting", "Compacting context"), "active");
|
|
766
822
|
this.publishStatus(session);
|
|
767
823
|
},
|
|
768
824
|
onCompactionEnd: (session, result, detail) => {
|
|
769
825
|
this.endSessionEntryMutation(session);
|
|
770
|
-
this.
|
|
826
|
+
if (!this.manualCompactionEnded.delete(session) && result === "error") {
|
|
827
|
+
this.publishCompactionOutcome(session, { reason: "manual", errorMessage: this.t("command.compactionFailed", "Compaction failed: {value0}", { value0: detail ?? "Unknown error" }) });
|
|
828
|
+
}
|
|
771
829
|
this.publishStatus(session);
|
|
772
830
|
},
|
|
773
831
|
reloadSession: (session) => this.reloadSessionRuntime(session),
|
|
@@ -840,6 +898,7 @@ export class PiSessionService {
|
|
|
840
898
|
});
|
|
841
899
|
}
|
|
842
900
|
async dispose() {
|
|
901
|
+
this.mcpAuthFlows.dispose();
|
|
843
902
|
this.unreadPublicationStopped = true;
|
|
844
903
|
this.clearUnreadPublicationRetry();
|
|
845
904
|
clearInterval(this.heartbeat);
|
|
@@ -854,12 +913,12 @@ export class PiSessionService {
|
|
|
854
913
|
const activeSessions = Array.from(new Set(this.active.values()));
|
|
855
914
|
for (const active of activeSessions) {
|
|
856
915
|
this.forgetUnreadActivity(active.runtime.session);
|
|
857
|
-
this.pendingAskStore.forgetSession(this.dialogSessionKey(active.runtime.session));
|
|
858
916
|
this.endSessionExtensionDialogs(active.runtime.session);
|
|
859
917
|
}
|
|
860
918
|
this.active.clear();
|
|
861
919
|
this.pendingSessionOpens.clear();
|
|
862
920
|
this.startupSessions.clear();
|
|
921
|
+
this.suggestedInputs.clear();
|
|
863
922
|
this.activities.clear();
|
|
864
923
|
this.compactionPromptQueues.clear();
|
|
865
924
|
this.authLossWarnings.clear();
|
|
@@ -868,6 +927,7 @@ export class PiSessionService {
|
|
|
868
927
|
this.subsessionLinks.clear();
|
|
869
928
|
this.subsessionHydratedParents.clear();
|
|
870
929
|
this.subsessionNotifyArmed.clear();
|
|
930
|
+
this.subsessionInboxes.clear();
|
|
871
931
|
this.notificationStore.clearAll("service-dispose");
|
|
872
932
|
for (const store of this.scopedNotificationStores.values())
|
|
873
933
|
store.clearAll("service-dispose");
|
|
@@ -881,7 +941,11 @@ export class PiSessionService {
|
|
|
881
941
|
finally {
|
|
882
942
|
await active.runtime.dispose();
|
|
883
943
|
}
|
|
884
|
-
})).finally(() =>
|
|
944
|
+
})).finally(async () => {
|
|
945
|
+
this.extensionCommandExecution.disable();
|
|
946
|
+
this.events.mediaIndex.clear();
|
|
947
|
+
await this.activityMarker.dispose();
|
|
948
|
+
});
|
|
885
949
|
await this.publishUnreadMutations([]);
|
|
886
950
|
}
|
|
887
951
|
async list(cwd, managementContext) {
|
|
@@ -942,7 +1006,27 @@ export class PiSessionService {
|
|
|
942
1006
|
for (const record of archivedForCwd) {
|
|
943
1007
|
this.publishNotificationMutations(this.notificationsForScope(eventScopeFromManagementContext(managementContext)).clearSession(record.sessionId, "archive-reconcile"));
|
|
944
1008
|
}
|
|
945
|
-
const
|
|
1009
|
+
const unarchivedById = new Map(sessions.filter((session) => !archivedById.has(session.id)).map((session) => [session.id, clientSessionFromListEntry(session)]));
|
|
1010
|
+
const eventScope = eventScopeFromManagementContext(managementContext);
|
|
1011
|
+
for (const active of new Set(this.active.values())) {
|
|
1012
|
+
if (active.eventScope !== eventScope)
|
|
1013
|
+
continue;
|
|
1014
|
+
const session = active.runtime.session;
|
|
1015
|
+
if (!cwdPathsEqual(session.sessionManager.getCwd(), cwd) || archivedById.has(session.sessionId))
|
|
1016
|
+
continue;
|
|
1017
|
+
const listed = unarchivedById.get(session.sessionId);
|
|
1018
|
+
const tracked = this.subsessionLinkForActiveChild(session) !== undefined;
|
|
1019
|
+
if (listed !== undefined) {
|
|
1020
|
+
if (tracked && session.sessionFile !== undefined && sessionPathsEqual(listed.path, session.sessionFile))
|
|
1021
|
+
listed.agentSubsession = true;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
const info = clientSessionFromActiveSession(session);
|
|
1025
|
+
if (tracked)
|
|
1026
|
+
info.agentSubsession = true;
|
|
1027
|
+
unarchivedById.set(session.sessionId, info);
|
|
1028
|
+
}
|
|
1029
|
+
const unarchivedSessions = [...unarchivedById.values()].sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified));
|
|
946
1030
|
const reconcilableSessionIds = this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById);
|
|
947
1031
|
this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds, eventScopeFromManagementContext(managementContext));
|
|
948
1032
|
if (managementContext === undefined) {
|
|
@@ -958,6 +1042,53 @@ export class PiSessionService {
|
|
|
958
1042
|
archivedById,
|
|
959
1043
|
};
|
|
960
1044
|
}
|
|
1045
|
+
/** Read-only plugin catalog: do not open runtimes or reconcile unread/activity state. */
|
|
1046
|
+
async listCatalogSessions(cwd, signal) {
|
|
1047
|
+
signal?.throwIfAborted();
|
|
1048
|
+
await this.assertNormalSessionReadAccess(cwd);
|
|
1049
|
+
signal?.throwIfAborted();
|
|
1050
|
+
const [entries, archives] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
|
|
1051
|
+
signal?.throwIfAborted();
|
|
1052
|
+
const managementFiles = [...new Set(this.active.values())]
|
|
1053
|
+
.filter((active) => active.eventScope !== NORMAL_SESSION_EVENT_SCOPE)
|
|
1054
|
+
.map((active) => active.runtime.session.sessionFile)
|
|
1055
|
+
.filter((path) => path !== undefined);
|
|
1056
|
+
const archived = archives.filter((record) => cwdPathsEqual(record.cwd, cwd));
|
|
1057
|
+
const archivedIds = new Set(archived.map((record) => record.sessionId));
|
|
1058
|
+
const rows = new Map(entries.filter((entry) => !archivedIds.has(entry.id)
|
|
1059
|
+
&& !managementFiles.some((path) => sessionPathsEqual(path, entry.path))).map((entry) => [entry.id, clientSessionFromListEntry(entry)]));
|
|
1060
|
+
const runtimes = new Map();
|
|
1061
|
+
for (const active of new Set(this.active.values())) {
|
|
1062
|
+
if (active.eventScope !== NORMAL_SESSION_EVENT_SCOPE)
|
|
1063
|
+
continue;
|
|
1064
|
+
const session = active.runtime.session;
|
|
1065
|
+
if (!cwdPathsEqual(session.sessionManager.getCwd(), cwd) || archivedIds.has(session.sessionId))
|
|
1066
|
+
continue;
|
|
1067
|
+
const row = rows.get(session.sessionId);
|
|
1068
|
+
// A disk clone with the same id must not inherit another file's runtime.
|
|
1069
|
+
if (row !== undefined && (session.sessionFile === undefined || !sessionPathsEqual(row.path, session.sessionFile)))
|
|
1070
|
+
continue;
|
|
1071
|
+
runtimes.set(session.sessionId, session);
|
|
1072
|
+
rows.set(session.sessionId, clientSessionFromActiveSession(session));
|
|
1073
|
+
}
|
|
1074
|
+
const entriesById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
1075
|
+
for (const record of archived) {
|
|
1076
|
+
const row = clientSessionFromArchivedRecord(record, entriesById.get(record.sessionId));
|
|
1077
|
+
if (row !== undefined)
|
|
1078
|
+
rows.set(row.id, row);
|
|
1079
|
+
}
|
|
1080
|
+
return [...rows.values()].sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified)).map((row) => {
|
|
1081
|
+
const runtime = runtimes.get(row.id);
|
|
1082
|
+
return Object.freeze({
|
|
1083
|
+
id: row.id,
|
|
1084
|
+
cwd: row.cwd,
|
|
1085
|
+
...(row.name === undefined ? {} : { name: row.name }),
|
|
1086
|
+
...(row.metadata === undefined ? {} : { metadata: structuredClone(row.metadata) }),
|
|
1087
|
+
archived: row.archived === true,
|
|
1088
|
+
runtime: runtime === undefined ? "not-hosted" : this.hasActiveWork(runtime) ? "busy" : "idle",
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
961
1092
|
async start(cwd, options = {}) {
|
|
962
1093
|
if (options.managementContext === undefined)
|
|
963
1094
|
await this.normalModelRuntimeForCwd(cwd);
|
|
@@ -998,26 +1129,32 @@ export class PiSessionService {
|
|
|
998
1129
|
throw error;
|
|
999
1130
|
}
|
|
1000
1131
|
}
|
|
1001
|
-
announceCreatedSession(active, cwd, parentSession) {
|
|
1132
|
+
announceCreatedSession(active, cwd, parentSession, agentSubsession) {
|
|
1002
1133
|
const { session } = active.runtime;
|
|
1003
1134
|
const created = {
|
|
1004
|
-
|
|
1005
|
-
path: session.sessionFile ?? "",
|
|
1135
|
+
...clientSessionFromActiveSession(session),
|
|
1006
1136
|
cwd,
|
|
1007
|
-
persisted: sessionFileExists(session.sessionFile),
|
|
1008
|
-
created: new Date().toISOString(),
|
|
1009
|
-
modified: new Date().toISOString(),
|
|
1010
|
-
messageCount: session.messages.length,
|
|
1011
|
-
firstMessage: "",
|
|
1012
|
-
// Include the parent so listeners can nest the new session in the tree
|
|
1013
|
-
// immediately, instead of showing it flat until the next reload.
|
|
1014
1137
|
...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
|
|
1138
|
+
...(agentSubsession === true ? { agentSubsession: true } : {}),
|
|
1015
1139
|
};
|
|
1016
1140
|
this.events.publishGlobal({ type: "session.created", session: created }, active.eventScope);
|
|
1017
1141
|
return created;
|
|
1018
1142
|
}
|
|
1019
1143
|
async startSession(cwd, options) {
|
|
1020
|
-
const
|
|
1144
|
+
const metadata = options.metadata === undefined ? undefined : requireSessionUiMetadata(options.metadata);
|
|
1145
|
+
const name = options.name === undefined ? undefined : requireInitialSessionName(options.name);
|
|
1146
|
+
const manager = this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession });
|
|
1147
|
+
if (metadata !== undefined) {
|
|
1148
|
+
if (manager.appendCustomEntry === undefined)
|
|
1149
|
+
throw new UserFacingError("session.metadataUnsupported", "Session manager does not support UI metadata initialization");
|
|
1150
|
+
manager.appendCustomEntry(SESSION_UI_METADATA_CUSTOM_TYPE, { version: 1, metadata });
|
|
1151
|
+
}
|
|
1152
|
+
if (name !== undefined) {
|
|
1153
|
+
if (manager.appendSessionInfo === undefined)
|
|
1154
|
+
throw new UserFacingError("session.initialNameUnsupported", "Session manager does not support initial naming");
|
|
1155
|
+
manager.appendSessionInfo(name);
|
|
1156
|
+
}
|
|
1157
|
+
const active = await this.create(manager, cwd, {
|
|
1021
1158
|
...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
|
|
1022
1159
|
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
1023
1160
|
...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
|
|
@@ -1025,7 +1162,9 @@ export class PiSessionService {
|
|
|
1025
1162
|
...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
|
|
1026
1163
|
startupIntent: "create",
|
|
1027
1164
|
});
|
|
1028
|
-
return
|
|
1165
|
+
return options.deferAnnouncement === true
|
|
1166
|
+
? clientSessionFromActiveSession(active.runtime.session)
|
|
1167
|
+
: this.announceCreatedSession(active, cwd, options.parentSession);
|
|
1029
1168
|
}
|
|
1030
1169
|
/**
|
|
1031
1170
|
* Start a new session on behalf of a LLM and deliver an initial prompt to it.
|
|
@@ -1047,6 +1186,8 @@ export class PiSessionService {
|
|
|
1047
1186
|
const created = await this.start(decision.cwd, {
|
|
1048
1187
|
...(model === undefined ? {} : { initialModel: model }),
|
|
1049
1188
|
...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
|
|
1189
|
+
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
|
1190
|
+
...(input.name === undefined ? {} : { name: input.name }),
|
|
1050
1191
|
...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
|
|
1051
1192
|
});
|
|
1052
1193
|
const modelUsed = this.activeForLookup(created.id, input.managementContext)?.runtime.session.model;
|
|
@@ -1081,6 +1222,7 @@ export class PiSessionService {
|
|
|
1081
1222
|
...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
|
|
1082
1223
|
...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
|
|
1083
1224
|
creationProvenance: "tracked-subsession",
|
|
1225
|
+
deferAnnouncement: true,
|
|
1084
1226
|
});
|
|
1085
1227
|
const modelUsed = this.activeForLookup(created.id, input.managementContext)?.runtime.session.model;
|
|
1086
1228
|
const parentSessionFile = nonEmptyString(input.parentSessionFile);
|
|
@@ -1094,6 +1236,10 @@ export class PiSessionService {
|
|
|
1094
1236
|
const scopedLink = await this.registerVerifiedSubsession(link, input.managementContext);
|
|
1095
1237
|
this.persistSubsessionLink(scopedLink);
|
|
1096
1238
|
this.persistSubsessionChildMarker(scopedLink);
|
|
1239
|
+
const active = this.activeForLookup(created.id, input.managementContext);
|
|
1240
|
+
if (active === undefined)
|
|
1241
|
+
throw new Error("Created subsession is no longer active");
|
|
1242
|
+
this.announceCreatedSession(active, decision.cwd, input.parentSessionFile, this.subsessionLinkForActiveChild(active.runtime.session) !== undefined);
|
|
1097
1243
|
await this.prompt(created.id, input.prompt, undefined, undefined, {
|
|
1098
1244
|
...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
|
|
1099
1245
|
});
|
|
@@ -1219,85 +1365,22 @@ export class PiSessionService {
|
|
|
1219
1365
|
throw unknownSpawnModelError(modelSpec, this.t);
|
|
1220
1366
|
return model;
|
|
1221
1367
|
}
|
|
1222
|
-
/**
|
|
1223
|
-
* Register the question set an agent wants the user to answer as the session's
|
|
1224
|
-
* open ask. Deliberately does not wait for the user: `ask_user` terminates the
|
|
1225
|
-
* run and the submitted answers come back later as a follow-up message.
|
|
1226
|
-
*
|
|
1227
|
-
* Rejected question sets throw {@link PendingAskValidationError}, which the
|
|
1228
|
-
* agent loop reports to the model as an error tool result.
|
|
1229
|
-
*/
|
|
1230
|
-
// 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.
|
|
1231
|
-
async openAsk(input, managementContext) {
|
|
1232
|
-
const scope = eventScopeFromManagementContext(managementContext);
|
|
1233
|
-
const sessionKey = scope === NORMAL_SESSION_EVENT_SCOPE ? input.sessionId : activeSessionKey(input.sessionId, scope);
|
|
1234
|
-
const result = this.pendingAskStore.open({ ...input, sessionId: sessionKey });
|
|
1235
|
-
// A supersede closes the earlier ask, so the browsers watching it must hear
|
|
1236
|
-
// that before they hear about its replacement.
|
|
1237
|
-
if (result.superseded !== undefined)
|
|
1238
|
-
this.publishAskClosed(input.sessionId, result.superseded, scope);
|
|
1239
|
-
this.events.publish(input.sessionId, { type: "ask.opened", ask: result.ask }, scope);
|
|
1240
|
-
this.publishStatusForSessionId(input.sessionId, scope);
|
|
1241
|
-
return result;
|
|
1242
|
-
}
|
|
1243
|
-
/**
|
|
1244
|
-
* Record the user's answers to the session's open ask and hand them to the
|
|
1245
|
-
* model. The answers travel as a system-authored custom message rather than a
|
|
1246
|
-
* user message, so they are not attributed to the human in the transcript;
|
|
1247
|
-
* they still wake an idle session (`triggerTurn`) and queue behind in-flight
|
|
1248
|
-
* work (`deliverAs: "followUp"`), which is how the run that `ask_user`
|
|
1249
|
-
* terminated continues.
|
|
1250
|
-
*/
|
|
1368
|
+
/** Reply to a durable request on the active branch; repeated answers remain ordinary replies. */
|
|
1251
1369
|
async submitAsk(ref, askId, submission, managementContext) {
|
|
1252
|
-
await this.assertWritable(ref);
|
|
1370
|
+
await this.assertWritable(ref, managementContext);
|
|
1253
1371
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1254
|
-
// Checked before the store closes the ask so a refused delivery cannot
|
|
1255
|
-
// discard answers the user already submitted.
|
|
1256
1372
|
this.assertTreeNavigationInactive(session, this.t("session.action.answer", "answer questions"));
|
|
1257
|
-
|
|
1373
|
+
const ask = resolveAskUserRequest(await this.readableSessionBranch(session), askId);
|
|
1374
|
+
const outcome = createAskUserOutcome(ask, submission, this.now().toISOString());
|
|
1375
|
+
return this.deliverAskOutcome(session, outcome);
|
|
1258
1376
|
}
|
|
1259
|
-
/**
|
|
1260
|
-
* Close the open ask without answers. The model is still told, naming every
|
|
1261
|
-
* question as unanswered: it was promised a follow-up message and would
|
|
1262
|
-
* otherwise wait for one that never comes.
|
|
1263
|
-
*/
|
|
1377
|
+
/** Record a skip as a new reply without consuming the original request. */
|
|
1264
1378
|
async cancelAsk(ref, askId, managementContext) {
|
|
1265
|
-
await this.assertWritable(ref);
|
|
1379
|
+
await this.assertWritable(ref, managementContext);
|
|
1266
1380
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1267
|
-
this.assertTreeNavigationInactive(session, this.t("session.action.dismiss", "
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
/**
|
|
1271
|
-
* Publish and deliver a closed ask. A stale close is reported rather than
|
|
1272
|
-
* thrown: losing the race against a supersede, another browser, or a session
|
|
1273
|
-
* that went away is ordinary, and the returned status tells the browser what
|
|
1274
|
-
* the session's open ask is now.
|
|
1275
|
-
*/
|
|
1276
|
-
async closeAsk(session, result) {
|
|
1277
|
-
if (result.status === "stale")
|
|
1278
|
-
return { result: "stale", sessionStatus: this.statusFromSession(session) };
|
|
1279
|
-
const { outcome } = result;
|
|
1280
|
-
this.publishAskClosed(session.sessionId, outcome, this.eventScopeForSession(session));
|
|
1281
|
-
await this.runSessionEntryMutation(session, this.t("server.main.07f784061918", "deliver answers to your questions"), () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: true, deliverAs: "followUp" }));
|
|
1282
|
-
this.publishStatus(session);
|
|
1283
|
-
return { result: "closed", outcome, sessionStatus: this.statusFromSession(session) };
|
|
1284
|
-
}
|
|
1285
|
-
publishAskClosed(sessionId, outcome, scope) {
|
|
1286
|
-
this.events.publish(sessionId, { type: "ask.closed", askId: outcome.askId, reason: outcome.reason }, scope);
|
|
1287
|
-
}
|
|
1288
|
-
/**
|
|
1289
|
-
* Void the session's open ask because the user sent a chat message instead of
|
|
1290
|
-
* answering it. Every browser closes the card as cancelled, and the model is
|
|
1291
|
-
* told — without being woken — so the notice rides into the turn the message
|
|
1292
|
-
* itself triggers rather than becoming a turn of its own.
|
|
1293
|
-
*/
|
|
1294
|
-
async voidOpenAskForUserMessage(session) {
|
|
1295
|
-
const outcome = this.pendingAskStore.cancelOpen(this.dialogSessionKey(session));
|
|
1296
|
-
if (outcome === undefined)
|
|
1297
|
-
return;
|
|
1298
|
-
this.publishAskClosed(session.sessionId, outcome, this.eventScopeForSession(session));
|
|
1299
|
-
await this.runSessionEntryMutation(session, this.t("server.main.90b5a8ac7c7d", "void the open questions"), () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: false, deliverAs: "followUp" }));
|
|
1300
|
-
this.publishStatus(session);
|
|
1381
|
+
this.assertTreeNavigationInactive(session, this.t("session.action.dismiss", "skip questions"));
|
|
1382
|
+
const ask = resolveAskUserRequest(await this.readableSessionBranch(session), askId);
|
|
1383
|
+
return this.deliverAskOutcome(session, createAskUserOutcome(ask, undefined, this.now().toISOString()));
|
|
1301
1384
|
}
|
|
1302
1385
|
/**
|
|
1303
1386
|
* Record the user's answer to an open extension dialog and resolve the
|
|
@@ -1306,7 +1389,7 @@ export class PiSessionService {
|
|
|
1306
1389
|
* (or an idle handler), so no custom message and no turn are triggered.
|
|
1307
1390
|
*/
|
|
1308
1391
|
async answerDialog(ref, dialogId, value, managementContext) {
|
|
1309
|
-
await this.assertWritable(ref);
|
|
1392
|
+
await this.assertWritable(ref, managementContext);
|
|
1310
1393
|
const session = await this.sessionForStatusOrDialogClose(ref, managementContext);
|
|
1311
1394
|
const result = this.pendingExtensionDialogStore.answer(this.dialogSessionKey(session), dialogId, value);
|
|
1312
1395
|
if (result.status === "stale")
|
|
@@ -1320,7 +1403,7 @@ export class PiSessionService {
|
|
|
1320
1403
|
}
|
|
1321
1404
|
/** Close an open extension dialog without an answer; the extension's wait settles with its kind's cancel value. */
|
|
1322
1405
|
async cancelDialog(ref, dialogId, managementContext) {
|
|
1323
|
-
await this.assertWritable(ref);
|
|
1406
|
+
await this.assertWritable(ref, managementContext);
|
|
1324
1407
|
const session = await this.sessionForStatusOrDialogClose(ref, managementContext);
|
|
1325
1408
|
const result = this.pendingExtensionDialogStore.cancel(this.dialogSessionKey(session), dialogId, "cancelled");
|
|
1326
1409
|
if (result.status === "stale")
|
|
@@ -1439,27 +1522,41 @@ export class PiSessionService {
|
|
|
1439
1522
|
return Promise.all(authorizedChildIds.map(async (childId) => ({ sessionId: childId, ...(await this.subsessionSummaryFields(childId, scope)) })));
|
|
1440
1523
|
}
|
|
1441
1524
|
/** Status and final result of a subsession, scoped to the caller's children. */
|
|
1442
|
-
async checkSubsession(parentSessionId, sessionId, parentSessionFile, managementContext) {
|
|
1525
|
+
async checkSubsession(parentSessionId, sessionId, parentSessionFile, toolCallId, managementContext) {
|
|
1526
|
+
const scope = eventScopeFromManagementContext(managementContext);
|
|
1443
1527
|
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile, managementContext);
|
|
1528
|
+
// Finalize the same completion even if its event/heartbeat notification has
|
|
1529
|
+
// not run yet. Inspection must not leave a future notice armed for this run.
|
|
1530
|
+
this.reconcileSubsessionCompletion(session);
|
|
1531
|
+
const status = this.subsessionStatus(session);
|
|
1444
1532
|
const messages = historyMessages(session);
|
|
1533
|
+
const completion = this.subsessionCompletionForOutput(parentSessionId, parentSessionFile, session, messages, scope);
|
|
1534
|
+
if (status !== "working" && completion !== undefined)
|
|
1535
|
+
this.receiveSubsessionCompletions(parentSessionId, parentSessionFile, [completion], toolCallId, scope);
|
|
1445
1536
|
return {
|
|
1446
1537
|
sessionId,
|
|
1447
1538
|
cwd: session.sessionManager.getCwd(),
|
|
1448
|
-
status
|
|
1539
|
+
status,
|
|
1449
1540
|
finalText: finalAssistantText(messages),
|
|
1450
1541
|
messageCount: messages.length,
|
|
1542
|
+
...(status === "working" || completion?.errorDetail === undefined ? {} : { errorDetail: completion.errorDetail }),
|
|
1451
1543
|
};
|
|
1452
1544
|
}
|
|
1453
1545
|
/** Filtered, paginated transcript of a subsession, scoped to the caller's children. */
|
|
1454
|
-
async readSubsession(parentSessionId, sessionId, query, parentSessionFile, managementContext) {
|
|
1546
|
+
async readSubsession(parentSessionId, sessionId, query, parentSessionFile, toolCallId, managementContext) {
|
|
1547
|
+
const scope = eventScopeFromManagementContext(managementContext);
|
|
1455
1548
|
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile, managementContext);
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1549
|
+
this.reconcileSubsessionCompletion(session);
|
|
1550
|
+
const messages = historyMessages(session);
|
|
1551
|
+
const view = buildTranscriptView(messages, query);
|
|
1552
|
+
const status = this.subsessionStatus(session);
|
|
1553
|
+
const completion = this.subsessionCompletionForOutput(parentSessionId, parentSessionFile, session, messages, scope);
|
|
1554
|
+
const errorDetail = status === "working" ? undefined : completion?.errorDetail;
|
|
1555
|
+
if (status !== "working" && completion !== undefined
|
|
1556
|
+
&& (transcriptReturnsFinalOutput(messages, view, completion.finalText) || (completion.finalText === "" && errorDetail !== undefined))) {
|
|
1557
|
+
this.receiveSubsessionCompletions(parentSessionId, parentSessionFile, [completion], toolCallId, scope);
|
|
1558
|
+
}
|
|
1559
|
+
return { sessionId, cwd: session.sessionManager.getCwd(), status, ...view, ...(errorDetail === undefined ? {} : { errorDetail }) };
|
|
1463
1560
|
}
|
|
1464
1561
|
/** Open a session after verifying it is one of the caller's tracked children. */
|
|
1465
1562
|
async openSubsession(parentSessionId, sessionId, parentSessionFile, managementContext) {
|
|
@@ -1514,6 +1611,14 @@ export class PiSessionService {
|
|
|
1514
1611
|
this.subsessionLinks.set(childKey, scopedLink);
|
|
1515
1612
|
if (!this.subsessionNotifyArmed.has(childKey))
|
|
1516
1613
|
this.subsessionNotifyArmed.set(childKey, false);
|
|
1614
|
+
const childManager = this.activeChildForSubsessionLink(scopedLink)?.runtime.session.sessionManager
|
|
1615
|
+
?? (link.childSessionFile === undefined ? undefined : this.sessionManager.open(link.childSessionFile));
|
|
1616
|
+
if (childManager !== undefined) {
|
|
1617
|
+
for (const completion of persistedSubsessionCompletions(childManager.getEntries?.() ?? childManager.getBranch())) {
|
|
1618
|
+
if (completion.sessionId === childSessionId)
|
|
1619
|
+
this.subsessionInbox(parentSessionId, link.parentSessionFile, scope).record(completion);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1517
1622
|
const cwd = this.cwdForVerifiedSubsession(scopedLink);
|
|
1518
1623
|
await this.publishUnreadMutations(this.unreadForScope(scope).excludeSession(childSessionId, cwd));
|
|
1519
1624
|
return scopedLink;
|
|
@@ -1540,6 +1645,10 @@ export class PiSessionService {
|
|
|
1540
1645
|
this.subsessionParents.delete(childKey);
|
|
1541
1646
|
this.subsessionLinks.delete(childKey);
|
|
1542
1647
|
this.subsessionNotifyArmed.delete(childKey);
|
|
1648
|
+
for (const [key, inbox] of this.subsessionInboxes) {
|
|
1649
|
+
if (key.startsWith(activeSessionKey(parentSessionId ?? "", scope) + "\0"))
|
|
1650
|
+
inbox.forgetChild(childSessionId);
|
|
1651
|
+
}
|
|
1543
1652
|
if (parentSessionId === undefined)
|
|
1544
1653
|
return;
|
|
1545
1654
|
const parentKey = activeSessionKey(parentSessionId, scope);
|
|
@@ -1696,34 +1805,72 @@ export class PiSessionService {
|
|
|
1696
1805
|
return active !== undefined && this.hasActiveWork(active.runtime.session);
|
|
1697
1806
|
});
|
|
1698
1807
|
}
|
|
1699
|
-
/**
|
|
1700
|
-
|
|
1701
|
-
* notification while the child is working, and when it stops fires a single
|
|
1702
|
-
* follow-up message to the parent via {@link prompt} (which queues if the
|
|
1703
|
-
* parent is busy and delivers immediately when it is idle).
|
|
1704
|
-
*/
|
|
1705
|
-
updateSubsessionTracking(session) {
|
|
1808
|
+
/** Persist one immutable result per agent run, shared by all delivery paths. */
|
|
1809
|
+
updateSubsessionTracking(session, agentStarted = false, agentSettled = false) {
|
|
1706
1810
|
const link = this.subsessionLinkForActiveChild(session);
|
|
1707
1811
|
if (link === undefined)
|
|
1708
1812
|
return;
|
|
1709
1813
|
const childId = link.childSessionId;
|
|
1710
1814
|
const childKey = activeSessionKey(childId, link.eventScope);
|
|
1711
|
-
if (
|
|
1815
|
+
if (agentStarted) {
|
|
1712
1816
|
this.subsessionNotifyArmed.set(childKey, true);
|
|
1713
1817
|
return;
|
|
1714
1818
|
}
|
|
1819
|
+
// Native settlement is authoritative even while the host prompt Promise's
|
|
1820
|
+
// mutation receipt is unwinding. Capture now, before another run can start.
|
|
1821
|
+
if (!agentSettled && this.hasActiveWork(session))
|
|
1822
|
+
return;
|
|
1715
1823
|
if (this.subsessionNotifyArmed.get(childKey) !== true)
|
|
1716
1824
|
return;
|
|
1825
|
+
const activity = this.activityForSession(session);
|
|
1826
|
+
const messages = historyMessages(session);
|
|
1827
|
+
const completion = {
|
|
1828
|
+
completionId: randomUUID(), sessionId: childId, cwd: session.sessionManager.getCwd(),
|
|
1829
|
+
status: activity?.phase === "error" ? "error" : "idle",
|
|
1830
|
+
finalText: finalAssistantText(messages), messageCount: messages.length,
|
|
1831
|
+
...(activity?.phase !== "error" || activity.detail === undefined ? {} : { errorDetail: activity.detail }),
|
|
1832
|
+
};
|
|
1833
|
+
try {
|
|
1834
|
+
session.sessionManager.appendCustomEntry?.(SUBSESSION_COMPLETION_ENTRY_TYPE, completion);
|
|
1835
|
+
}
|
|
1836
|
+
catch (error) {
|
|
1837
|
+
// Keep the latch armed so a later reconciliation can retry persistence.
|
|
1838
|
+
this.logSubsessionNotificationFailure(link.parentSessionId, childId, error);
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1717
1841
|
this.subsessionNotifyArmed.set(childKey, false);
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1842
|
+
this.subsessionInbox(link.parentSessionId, link.parentSessionFile, link.eventScope).record(completion);
|
|
1843
|
+
void this.wakeParentForSubsession(link);
|
|
1844
|
+
}
|
|
1845
|
+
async wakeParentForSubsession(link) {
|
|
1846
|
+
try {
|
|
1847
|
+
const session = await this.getOrOpenParentForSubsession(link);
|
|
1848
|
+
this.scheduleSubsessionDelivery(session);
|
|
1849
|
+
}
|
|
1850
|
+
catch (error) {
|
|
1851
|
+
this.logSubsessionNotificationFailure(link.parentSessionId, link.childSessionId, error);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
subsessionReceiptOwner(session) {
|
|
1855
|
+
return {
|
|
1856
|
+
parentSessionId: session.sessionId,
|
|
1857
|
+
parentSessionFile: session.sessionFile ?? null,
|
|
1858
|
+
scopeHash: createHash("sha256").update(this.eventScopeForSession(session)).digest("hex"),
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
subsessionReceiptMatches(session, entry) {
|
|
1862
|
+
if (!isRecord(entry))
|
|
1863
|
+
return true;
|
|
1864
|
+
const data = entry["type"] === "custom" && entry["customType"] === SUBSESSION_RECEIPT_ENTRY_TYPE
|
|
1865
|
+
? entry["data"] : entry["type"] === "custom_message" && entry["customType"] === SUBSESSION_NOTIFICATION_CUSTOM_TYPE ? entry["details"] : undefined;
|
|
1866
|
+
if (!isRecord(data))
|
|
1867
|
+
return true;
|
|
1868
|
+
const owner = data["receiptOwner"];
|
|
1869
|
+
if (owner === undefined)
|
|
1870
|
+
return this.eventScopeForSession(session) === NORMAL_SESSION_EVENT_SCOPE;
|
|
1871
|
+
const expected = this.subsessionReceiptOwner(session);
|
|
1872
|
+
return isRecord(owner) && owner["parentSessionId"] === expected.parentSessionId
|
|
1873
|
+
&& owner["parentSessionFile"] === expected.parentSessionFile && owner["scopeHash"] === expected.scopeHash;
|
|
1727
1874
|
}
|
|
1728
1875
|
async getOrOpenParentForSubsession(link) {
|
|
1729
1876
|
const { parentSessionId } = link;
|
|
@@ -1741,33 +1888,98 @@ export class PiSessionService {
|
|
|
1741
1888
|
...(link.managementContext === undefined ? {} : { managementContext: link.managementContext }),
|
|
1742
1889
|
})).runtime.session;
|
|
1743
1890
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
const
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1891
|
+
browserTranscriptMessage(session, message) {
|
|
1892
|
+
return projectTranscriptMarkdown(message, session.extensionRunner.getMarkdownTransformers?.() ?? [], (_error, transformerIndex) => {
|
|
1893
|
+
this.logger.info({ sessionId: session.sessionId, transformerIndex, category: "markdown-transformer-failed" }, "Transcript Markdown transformer failed");
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
async transcriptSnapshot(ref, page, managementContext) {
|
|
1897
|
+
const session = await this.getOrOpen(ref, managementContext);
|
|
1898
|
+
const seqBeforeRead = this.events.currentSeq(session.sessionId, this.eventScopeForSession(session));
|
|
1899
|
+
const activeAtCapture = this.hasActiveWork(session);
|
|
1900
|
+
const diskBranch = activeAtCapture ? undefined : await this.readableSessionBranch(session);
|
|
1901
|
+
// Disk reads yield, even if a whole turn starts AND ends while they run.
|
|
1902
|
+
// From the final runtime read through the watermark capture nothing may yield.
|
|
1903
|
+
const branch = diskBranch === undefined || this.hasActiveWork(session) || this.events.currentSeq(session.sessionId, this.eventScopeForSession(session)) !== seqBeforeRead
|
|
1904
|
+
? session.sessionManager.getBranch()
|
|
1905
|
+
: diskBranch;
|
|
1906
|
+
const messages = historyMessagesFromEntries(branch);
|
|
1907
|
+
const userCount = messages.filter((message) => isRecord(message) && message["role"] === "user").length;
|
|
1908
|
+
const echoes = (this.pendingPromptEchoes.get(session) ?? []).filter((echo) => echo.userIndex >= userCount);
|
|
1909
|
+
messages.push(...echoes.map((echo) => echo.message));
|
|
1910
|
+
const messagePage = pageMessagesAtSafeBoundary(messages, page);
|
|
1911
|
+
return {
|
|
1912
|
+
page: { ...messagePage, messages: messagePage.messages.map((message) => this.browserTranscriptMessage(session, message)), leafId: session.sessionManager.getLeafId() },
|
|
1913
|
+
status: this.statusFromSession(session, transcriptMessageCount(branch) + echoes.length),
|
|
1914
|
+
seq: this.events.currentSeq(session.sessionId, this.eventScopeForSession(session)),
|
|
1915
|
+
partial: this.publishedAssistantPartials.get(session) ?? null,
|
|
1916
|
+
};
|
|
1766
1917
|
}
|
|
1767
1918
|
async messages(ref, page, managementContext) {
|
|
1768
1919
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1769
1920
|
const branch = await this.readableSessionBranch(session);
|
|
1770
|
-
|
|
1921
|
+
const messagePage = pageMessagesAtSafeBoundary(historyMessagesFromEntries(branch), page);
|
|
1922
|
+
return { ...messagePage, messages: messagePage.messages.map((message) => this.browserTranscriptMessage(session, message)), leafId: session.sessionManager.getLeafId() };
|
|
1923
|
+
}
|
|
1924
|
+
async media(ref, mediaId, managementContext) {
|
|
1925
|
+
if (!isSessionMediaId(mediaId))
|
|
1926
|
+
throw new Error(this.t("session.media.invalidId", "Invalid media id"));
|
|
1927
|
+
if (managementContext === undefined)
|
|
1928
|
+
await this.assertNormalSessionReadAccess(ref.cwd);
|
|
1929
|
+
const index = this.events.mediaIndex;
|
|
1930
|
+
const session = this.activeForLookup(ref, managementContext)?.runtime.session ?? this.startupSessionForLookup(ref, managementContext);
|
|
1931
|
+
const eventScope = eventScopeFromManagementContext(managementContext);
|
|
1932
|
+
const unavailable = () => new Error(this.t("routing.sessionNotFound", "Session not found"));
|
|
1933
|
+
// No durable management ownership marker exists. Management binary reads
|
|
1934
|
+
// require an already-hosted owner rather than inferring identity from disk.
|
|
1935
|
+
if (managementContext !== undefined && session === undefined)
|
|
1936
|
+
throw unavailable();
|
|
1937
|
+
const assertVisible = (path) => {
|
|
1938
|
+
if (eventScope !== NORMAL_SESSION_EVENT_SCOPE)
|
|
1939
|
+
return;
|
|
1940
|
+
if (session === undefined && ([...this.active.values()].some((active) => active.eventScope !== eventScope && active.runtime.session.sessionId === ref.id && lookupMatchesActiveSession(ref, active))
|
|
1941
|
+
|| [...this.startupSessions.values()].some((startup) => startup.eventScope !== eventScope && startup.session.sessionId === ref.id && lookupMatchesStartupSession(ref, startup.session))))
|
|
1942
|
+
throw unavailable();
|
|
1943
|
+
if (path !== undefined && [...this.managementMediaFiles].some((managed) => sessionPathsEqual(managed, path)))
|
|
1944
|
+
throw unavailable();
|
|
1945
|
+
};
|
|
1946
|
+
assertVisible(session?.sessionFile ?? session?.sessionManager.getSessionFile());
|
|
1947
|
+
// Verify session/cwd ownership before consulting the session-scoped cache.
|
|
1948
|
+
// Cold binary reads do not construct an SDK runtime (which can migrate files).
|
|
1949
|
+
const archived = session === undefined ? await this.getArchived(ref) : undefined;
|
|
1950
|
+
const resolved = session === undefined && archived?.archivePath === undefined
|
|
1951
|
+
? await this.sessionManager.resolveSessionFile(ref.cwd, ref.id)
|
|
1952
|
+
: undefined;
|
|
1953
|
+
const sessionId = session?.sessionId ?? archived?.sessionId ?? resolved?.id;
|
|
1954
|
+
if (sessionId === undefined || (resolved !== undefined && !cwdPathsEqual(resolved.cwd, ref.cwd)))
|
|
1955
|
+
throw new Error(this.t("routing.sessionNotFound", "Session not found"));
|
|
1956
|
+
assertVisible(archived?.originalPath);
|
|
1957
|
+
assertVisible(session?.sessionFile ?? session?.sessionManager.getSessionFile() ?? archived?.archivePath ?? resolved?.path);
|
|
1958
|
+
const scope = { id: sessionId, eventScope, cwd: canonicalizeStoredCwd(session?.sessionManager.getCwd() ?? archived?.cwd ?? resolved?.cwd ?? ref.cwd) };
|
|
1959
|
+
const cached = index.get(scope, mediaId);
|
|
1960
|
+
if (cached !== undefined)
|
|
1961
|
+
return cached;
|
|
1962
|
+
if (session !== undefined) {
|
|
1963
|
+
const live = index.find(scope, mediaId, [
|
|
1964
|
+
session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch(),
|
|
1965
|
+
session.messages,
|
|
1966
|
+
session.state.streamingMessage,
|
|
1967
|
+
this.publishedAssistantPartials.get(session),
|
|
1968
|
+
...(this.pendingPromptEchoes.get(session) ?? []).map((echo) => echo.message),
|
|
1969
|
+
]);
|
|
1970
|
+
if (live !== undefined)
|
|
1971
|
+
return live;
|
|
1972
|
+
}
|
|
1973
|
+
// Read disk on misses even for a cached idle runtime: other processes can
|
|
1974
|
+
// append images on any branch without emitting events through this daemon.
|
|
1975
|
+
const path = session?.sessionFile ?? session?.sessionManager.getSessionFile() ?? archived?.archivePath ?? resolved?.path
|
|
1976
|
+
?? (session === undefined ? undefined : (await this.sessionManager.resolveSessionFile(ref.cwd, sessionId))?.path);
|
|
1977
|
+
if (path === undefined)
|
|
1978
|
+
return undefined;
|
|
1979
|
+
assertVisible(path);
|
|
1980
|
+
const entries = await this.sessionManager.readEntries?.(path);
|
|
1981
|
+
assertVisible(path);
|
|
1982
|
+
return entries === undefined ? undefined : index.find(scope, mediaId, entries);
|
|
1771
1983
|
}
|
|
1772
1984
|
async status(ref, managementContext) {
|
|
1773
1985
|
const session = await this.sessionForStatusOrDialogClose(ref, managementContext);
|
|
@@ -1777,7 +1989,12 @@ export class PiSessionService {
|
|
|
1777
1989
|
if (this.hasActiveWork(session))
|
|
1778
1990
|
return this.statusFromSession(session);
|
|
1779
1991
|
const branch = await this.readableSessionBranch(session);
|
|
1780
|
-
return this.statusFromSession(session,
|
|
1992
|
+
return this.statusFromSession(session, this.snapshotMessageCount(branch));
|
|
1993
|
+
}
|
|
1994
|
+
async assertNormalSessionReadAccess(cwd) {
|
|
1995
|
+
if (!await normalSessionReadAllowed(cwd, this.projectPathForCwd, this.managementProjectRoot)) {
|
|
1996
|
+
throw new Error(this.t("routing.sessionNotFound", "Session not found"));
|
|
1997
|
+
}
|
|
1781
1998
|
}
|
|
1782
1999
|
async searchCandidates(cwd, normalizedQuery, managementContext) {
|
|
1783
2000
|
const listing = await this.listWorkspaceSessions(cwd, managementContext);
|
|
@@ -1805,7 +2022,7 @@ export class PiSessionService {
|
|
|
1805
2022
|
const streamingMessage = session.state.streamingMessage;
|
|
1806
2023
|
const partial = streamingMessage === undefined || streamingMessage === null
|
|
1807
2024
|
? null
|
|
1808
|
-
: annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
|
|
2025
|
+
: annotateAssistantStreamId(annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel), this.assistantStreamIds.get(session));
|
|
1809
2026
|
return { seq, partial };
|
|
1810
2027
|
}
|
|
1811
2028
|
async getSessionDefaults(ref, managementContext) {
|
|
@@ -1815,7 +2032,7 @@ export class PiSessionService {
|
|
|
1815
2032
|
}
|
|
1816
2033
|
async setSessionDefaults(ref, defaults, managementContext) {
|
|
1817
2034
|
const update = parseSessionDefaultsUpdate({ ...defaults });
|
|
1818
|
-
await this.assertWritable(ref);
|
|
2035
|
+
await this.assertWritable(ref, managementContext);
|
|
1819
2036
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1820
2037
|
return this.runModelScopeMutation(async () => {
|
|
1821
2038
|
const settings = await this.freshSessionDefaultsSettings(session);
|
|
@@ -1875,7 +2092,7 @@ export class PiSessionService {
|
|
|
1875
2092
|
async setModelEnabled(ref, provider, modelId, enabled, managementContext) {
|
|
1876
2093
|
if (managementContext !== undefined)
|
|
1877
2094
|
throw new Error(this.t("server.main.2a7cb0e35521", "Enabled model scope cannot be changed in management embed mode"));
|
|
1878
|
-
await this.assertWritable(ref);
|
|
2095
|
+
await this.assertWritable(ref, managementContext);
|
|
1879
2096
|
const session = await this.getOrOpen(ref);
|
|
1880
2097
|
return this.runModelScopeMutation(async () => {
|
|
1881
2098
|
const scope = this.modelScopeContext(session);
|
|
@@ -1901,7 +2118,7 @@ export class PiSessionService {
|
|
|
1901
2118
|
async setModelScope(ref, mode, managementContext) {
|
|
1902
2119
|
if (managementContext !== undefined)
|
|
1903
2120
|
throw new Error(this.t("server.main.2a7cb0e35521", "Enabled model scope cannot be changed in management embed mode"));
|
|
1904
|
-
await this.assertWritable(ref);
|
|
2121
|
+
await this.assertWritable(ref, managementContext);
|
|
1905
2122
|
const session = await this.getOrOpen(ref);
|
|
1906
2123
|
return this.runModelScopeMutation(async () => {
|
|
1907
2124
|
const scope = this.modelScopeContext(session);
|
|
@@ -1924,7 +2141,7 @@ export class PiSessionService {
|
|
|
1924
2141
|
});
|
|
1925
2142
|
}
|
|
1926
2143
|
async setModel(ref, provider, modelId, managementContext) {
|
|
1927
|
-
await this.assertWritable(ref);
|
|
2144
|
+
await this.assertWritable(ref, managementContext);
|
|
1928
2145
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1929
2146
|
this.assertTreeNavigationInactive(session, this.t("server.main.130226a572df", "change models"));
|
|
1930
2147
|
const candidates = await this.sessionModelCandidates(session);
|
|
@@ -1939,7 +2156,7 @@ export class PiSessionService {
|
|
|
1939
2156
|
return this.statusFromSession(session);
|
|
1940
2157
|
}
|
|
1941
2158
|
async cycleModel(ref, direction, managementContext) {
|
|
1942
|
-
await this.assertWritable(ref);
|
|
2159
|
+
await this.assertWritable(ref, managementContext);
|
|
1943
2160
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1944
2161
|
const result = await this.runModelScopeMutation(async () => {
|
|
1945
2162
|
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
@@ -1957,7 +2174,7 @@ export class PiSessionService {
|
|
|
1957
2174
|
return session.getAvailableThinkingLevels();
|
|
1958
2175
|
}
|
|
1959
2176
|
async setThinkingLevel(ref, level, managementContext) {
|
|
1960
|
-
await this.assertWritable(ref);
|
|
2177
|
+
await this.assertWritable(ref, managementContext);
|
|
1961
2178
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1962
2179
|
this.assertTreeNavigationInactive(session, this.t("session.action.changeThinking", "change the thinking level"));
|
|
1963
2180
|
// pi owns the valid set; validate against the session's live levels rather
|
|
@@ -1975,7 +2192,7 @@ export class PiSessionService {
|
|
|
1975
2192
|
return this.statusFromSession(session);
|
|
1976
2193
|
}
|
|
1977
2194
|
async cycleThinkingLevel(ref, managementContext) {
|
|
1978
|
-
await this.assertWritable(ref);
|
|
2195
|
+
await this.assertWritable(ref, managementContext);
|
|
1979
2196
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1980
2197
|
const level = await this.runSessionEntryMutation(session, this.t("session.action.changeThinking", "change the thinking level"), () => Promise.resolve(session.cycleThinkingLevel()));
|
|
1981
2198
|
if (level === undefined)
|
|
@@ -2013,7 +2230,7 @@ export class PiSessionService {
|
|
|
2013
2230
|
const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
|
|
2014
2231
|
const parsedAttachments = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false });
|
|
2015
2232
|
const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
|
|
2016
|
-
await this.assertWritable(ref);
|
|
2233
|
+
await this.assertWritable(ref, options?.managementContext);
|
|
2017
2234
|
const session = await this.getOrOpen(ref, options?.managementContext);
|
|
2018
2235
|
this.assertTreeNavigationInactive(session, this.t("server.main.94306b2fc35c", "send a prompt"));
|
|
2019
2236
|
this.maybeGenerateSessionName(session, promptText);
|
|
@@ -2028,7 +2245,6 @@ export class PiSessionService {
|
|
|
2028
2245
|
// the form is void: keeping it open would invite answers to questions the
|
|
2029
2246
|
// conversation has already moved past. Ignored duplicates skip this on
|
|
2030
2247
|
// purpose: they must not void an ask posted after the queued original.
|
|
2031
|
-
await this.voidOpenAskForUserMessage(session);
|
|
2032
2248
|
if (session.isCompacting) {
|
|
2033
2249
|
this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp", images, echoUserMessage);
|
|
2034
2250
|
return;
|
|
@@ -2040,18 +2256,58 @@ export class PiSessionService {
|
|
|
2040
2256
|
void promptPromise;
|
|
2041
2257
|
return promptPromise;
|
|
2042
2258
|
}
|
|
2043
|
-
beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true) {
|
|
2044
|
-
|
|
2259
|
+
beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true, options = {}) {
|
|
2260
|
+
if (options.auditUserPrompt !== false)
|
|
2261
|
+
this.auditUserPrompt(session, text);
|
|
2045
2262
|
this.publishActivity(session, behavior === "steer" ? this.t("activity.steerQueued", "Steering message queued") : behavior === "followUp" ? this.t("activity.messageQueued", "Message queued") : this.t("activity.messageReceived", "Message received"), "active");
|
|
2046
2263
|
const eventScope = this.eventScopeForSession(session);
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
const
|
|
2050
|
-
|
|
2264
|
+
const echoes = this.pendingPromptEchoes.get(session) ?? [];
|
|
2265
|
+
const userCount = historyMessagesFromEntries(session.sessionManager.getBranch()).filter((message) => isRecord(message) && message["role"] === "user").length;
|
|
2266
|
+
const echo = behavior === undefined && echoUserMessage
|
|
2267
|
+
? { userIndex: Math.max(userCount, ...echoes.map((pending) => pending.userIndex + 1)), message: userMessage(text, images) }
|
|
2268
|
+
: undefined;
|
|
2269
|
+
if (echo !== undefined) {
|
|
2270
|
+
// SDK input hooks may await before appending the user message. Keep the
|
|
2271
|
+
// already-published echo visible in snapshots until that append occurs.
|
|
2272
|
+
this.pendingPromptEchoes.set(session, [...echoes, echo]);
|
|
2273
|
+
this.events.publish(session.sessionId, { type: "message.append", message: this.browserTranscriptMessage(session, echo.message) }, eventScope, { id: session.sessionId, cwd: session.sessionManager.getCwd(), eventScope });
|
|
2274
|
+
}
|
|
2275
|
+
const promptOptions = options.expandPromptTemplates === false
|
|
2276
|
+
? { ...buildPromptOptions(behavior, images), expandPromptTemplates: false }
|
|
2277
|
+
: buildPromptOptions(behavior, images);
|
|
2278
|
+
const submission = { session, active: true };
|
|
2279
|
+
const commandName = text.trim().split(/\s+/)[0]?.replace(/^\//, "");
|
|
2280
|
+
const isExtensionCommand = options.expandPromptTemplates !== false && text.trim().startsWith("/") && session.extensionRunner.getRegisteredCommands().some((command) => command.invocationName === commandName);
|
|
2281
|
+
const trackEarlyFailure = behavior === undefined && this.subsessionLinkForActiveChild(session) !== undefined && !isExtensionCommand;
|
|
2282
|
+
let agentStarted = false;
|
|
2283
|
+
const unsubscribe = !trackEarlyFailure ? undefined : session.subscribe((event) => {
|
|
2284
|
+
if (getString(event, "type") === "agent_start")
|
|
2285
|
+
agentStarted = true;
|
|
2286
|
+
});
|
|
2287
|
+
const promptPromise = this.runSessionEntryMutation(session, this.t("server.main.94306b2fc35c", "send a prompt"), () => isExtensionCommand
|
|
2288
|
+
? this.extensionCommandExecution.run({ submission }, () => session.prompt(text, promptOptions))
|
|
2289
|
+
: session.prompt(text, promptOptions)).finally(() => {
|
|
2290
|
+
submission.active = false;
|
|
2291
|
+
unsubscribe?.();
|
|
2292
|
+
if (echo !== undefined) {
|
|
2293
|
+
const remaining = (this.pendingPromptEchoes.get(session) ?? []).filter((pending) => pending !== echo);
|
|
2294
|
+
if (remaining.length === 0)
|
|
2295
|
+
this.pendingPromptEchoes.delete(session);
|
|
2296
|
+
else
|
|
2297
|
+
this.pendingPromptEchoes.set(session, remaining);
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2051
2300
|
void promptPromise.catch((error) => {
|
|
2301
|
+
this.subsessionStoppedParents.add(session);
|
|
2052
2302
|
const message = error instanceof Error ? error.message : String(error);
|
|
2053
2303
|
this.publishActivity(session, this.t("activity.error", "An error occurred"), "error", message);
|
|
2054
2304
|
this.events.publish(session.sessionId, { type: "session.error", message }, eventScope);
|
|
2305
|
+
if (this.isCurrentActiveSession(session)) {
|
|
2306
|
+
const link = this.subsessionLinkForActiveChild(session);
|
|
2307
|
+
if (trackEarlyFailure && !agentStarted && link !== undefined)
|
|
2308
|
+
this.subsessionNotifyArmed.set(activeSessionKey(link.childSessionId, link.eventScope), true);
|
|
2309
|
+
this.updateSubsessionTracking(session);
|
|
2310
|
+
}
|
|
2055
2311
|
});
|
|
2056
2312
|
return promptPromise;
|
|
2057
2313
|
}
|
|
@@ -2115,7 +2371,7 @@ export class PiSessionService {
|
|
|
2115
2371
|
const parsed = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false, allowFileAttachments: true });
|
|
2116
2372
|
if (parsed.length === 0)
|
|
2117
2373
|
return [];
|
|
2118
|
-
await this.assertWritable(ref);
|
|
2374
|
+
await this.assertWritable(ref, managementContext);
|
|
2119
2375
|
const active = await this.getActive(ref, managementContext);
|
|
2120
2376
|
const cwd = active.runtime.cwd;
|
|
2121
2377
|
const effectiveFolder = folder ?? (await this.workspaceAttachmentsConfig(cwd)).defaultFolder;
|
|
@@ -2126,7 +2382,7 @@ export class PiSessionService {
|
|
|
2126
2382
|
return loadEffectiveProjectAttachmentsConfig(cwd, globalConfig);
|
|
2127
2383
|
}
|
|
2128
2384
|
async shell(ref, text, managementContext) {
|
|
2129
|
-
await this.assertWritable(ref);
|
|
2385
|
+
await this.assertWritable(ref, managementContext);
|
|
2130
2386
|
const active = await this.getActive(ref, managementContext);
|
|
2131
2387
|
const { session } = active.runtime;
|
|
2132
2388
|
this.assertTreeNavigationInactive(session, this.t("session.action.shell", "run a shell command"));
|
|
@@ -2163,25 +2419,30 @@ export class PiSessionService {
|
|
|
2163
2419
|
});
|
|
2164
2420
|
}
|
|
2165
2421
|
async runCommand(ref, text, managementContext) {
|
|
2166
|
-
await this.assertWritable(ref);
|
|
2422
|
+
await this.assertWritable(ref, managementContext);
|
|
2167
2423
|
const active = await this.getActive(ref, managementContext);
|
|
2168
2424
|
return this.commandService.run(active.runtime.session.sessionId, text, active.eventScope);
|
|
2169
2425
|
}
|
|
2170
2426
|
async respondToCommand(ref, requestId, value, managementContext) {
|
|
2171
|
-
await this.assertWritable(ref);
|
|
2427
|
+
await this.assertWritable(ref, managementContext);
|
|
2172
2428
|
const active = await this.getActive(ref, managementContext);
|
|
2173
2429
|
return this.commandService.respond(active.runtime.session.sessionId, requestId, value, active.eventScope);
|
|
2174
2430
|
}
|
|
2175
2431
|
async navigateTree(ref, request, managementContext) {
|
|
2176
|
-
|
|
2432
|
+
return this.navigateSessionTree(ref, request.targetId, request.expectedLeafId, sessionTreeNavigationOptions(request, this.t), managementContext, undefined, request.retainCheckpoint);
|
|
2433
|
+
}
|
|
2434
|
+
async navigateSessionTree(ref, targetId, expectedLeafId, options, managementContext, expectedSession, retainCheckpoint = false) {
|
|
2435
|
+
if (targetId.trim() === "")
|
|
2177
2436
|
throw new Error(this.t("server.main.f39f37acdac1", "Session tree target is required"));
|
|
2178
2437
|
if (this.isTreeExclusiveSessionIdentityActive(sessionIdFromLookup(ref))) {
|
|
2179
2438
|
throw new Error(this.t("server.main.e39e6b98e072", "Stop current session activity before navigating the session tree"));
|
|
2180
2439
|
}
|
|
2181
|
-
await this.assertWritable(ref);
|
|
2182
|
-
const options = sessionTreeNavigationOptions(request, this.t);
|
|
2440
|
+
await this.assertWritable(ref, managementContext);
|
|
2183
2441
|
const session = await this.getOrOpen(ref, managementContext);
|
|
2184
|
-
if (
|
|
2442
|
+
if (expectedSession !== undefined && session !== expectedSession)
|
|
2443
|
+
throw new Error(this.t("session.extensionUnavailable", "This extension command belongs to an unavailable session runtime"));
|
|
2444
|
+
const navigateTree = session.navigateTree?.bind(session);
|
|
2445
|
+
if (navigateTree === undefined)
|
|
2185
2446
|
throw new Error(this.t("session.treeUnsupported", "Session tree navigation is not supported by this Pi runtime"));
|
|
2186
2447
|
if (this.hasActiveWork(session))
|
|
2187
2448
|
throw new Error(this.t("server.main.e39e6b98e072", "Stop current session activity before navigating the session tree"));
|
|
@@ -2190,26 +2451,34 @@ export class PiSessionService {
|
|
|
2190
2451
|
this.treeNavigations.add(session);
|
|
2191
2452
|
try {
|
|
2192
2453
|
const oldLeafId = session.sessionManager.getLeafId();
|
|
2193
|
-
if (oldLeafId !==
|
|
2454
|
+
if (oldLeafId !== expectedLeafId) {
|
|
2194
2455
|
throw new Error(this.t("session.treeStale", "The session changed since /tree was opened. Reopen /tree and try again."));
|
|
2195
2456
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2457
|
+
if (retainCheckpoint)
|
|
2458
|
+
targetId = messageHistoryTargetId(session.sessionManager.getBranch(), targetId);
|
|
2459
|
+
const checkpointTail = retainCheckpoint ? session.sessionManager.getBranch().find((entry) => isRecord(entry) && entry["id"] === targetId) : undefined;
|
|
2460
|
+
const retainCustomMessage = isRecord(checkpointTail) && checkpointTail["type"] === "custom_message";
|
|
2461
|
+
if (retainCustomMessage && options.summarize === true)
|
|
2462
|
+
throw new Error(this.t("session.checkpointSummary", "Retaining a custom-message checkpoint requires navigation without a branch summary"));
|
|
2463
|
+
const activeEditableTargetParentId = retainCustomMessage ? undefined : activeEditableTreeTargetParentId(session.sessionManager, targetId, oldLeafId);
|
|
2464
|
+
this.publishActivity(session, options.summarize === true ? this.t("activity.summarizing", "Summarizing branch") : this.t("activity.navigating", "Navigating session tree"), "active");
|
|
2198
2465
|
this.publishStatus(session);
|
|
2199
|
-
let result =
|
|
2466
|
+
let result = retainCustomMessage
|
|
2467
|
+
? await navigateTreeRetainingCustomMessage(session.sessionManager, targetId, checkpointTail["parentId"], () => navigateTree(targetId, options))
|
|
2468
|
+
: await navigateTree(targetId, options);
|
|
2200
2469
|
if (activeEditableTargetParentId !== undefined && !result.cancelled
|
|
2201
2470
|
&& result.editorText === undefined && result.summaryEntry === undefined
|
|
2202
2471
|
&& session.sessionManager.getLeafId() === oldLeafId) {
|
|
2203
2472
|
setSessionTreeLeaf(session.sessionManager, activeEditableTargetParentId);
|
|
2204
2473
|
try {
|
|
2205
|
-
result = await
|
|
2474
|
+
result = await navigateTree(targetId, options);
|
|
2206
2475
|
}
|
|
2207
2476
|
catch (error) {
|
|
2208
|
-
session.sessionManager.branch(
|
|
2477
|
+
session.sessionManager.branch(targetId);
|
|
2209
2478
|
throw error;
|
|
2210
2479
|
}
|
|
2211
2480
|
if (result.cancelled)
|
|
2212
|
-
session.sessionManager.branch(
|
|
2481
|
+
session.sessionManager.branch(targetId);
|
|
2213
2482
|
}
|
|
2214
2483
|
if (result.cancelled) {
|
|
2215
2484
|
if (this.isCurrentActiveSession(session)) {
|
|
@@ -2217,13 +2486,6 @@ export class PiSessionService {
|
|
|
2217
2486
|
}
|
|
2218
2487
|
return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
|
|
2219
2488
|
}
|
|
2220
|
-
if (session.sessionManager.getLeafId() !== oldLeafId) {
|
|
2221
|
-
// Questions belong to the branch we left. Close them without appending a
|
|
2222
|
-
// cancellation message or waking the rewound session.
|
|
2223
|
-
const outcome = this.pendingAskStore.cancelOpen(this.dialogSessionKey(session));
|
|
2224
|
-
if (outcome !== undefined)
|
|
2225
|
-
this.publishAskClosed(session.sessionId, outcome, this.eventScopeForSession(session));
|
|
2226
|
-
}
|
|
2227
2489
|
if (result.summaryEntry !== undefined) {
|
|
2228
2490
|
this.unpersistedTreeBranchLeaves.delete(session);
|
|
2229
2491
|
}
|
|
@@ -2232,12 +2494,14 @@ export class PiSessionService {
|
|
|
2232
2494
|
if (selectedLeafId !== oldLeafId)
|
|
2233
2495
|
this.unpersistedTreeBranchLeaves.set(session, selectedLeafId);
|
|
2234
2496
|
}
|
|
2497
|
+
this.setSuggestedInput(session, result.editorText);
|
|
2498
|
+
this.events.publish(session.sessionId, { type: "session.tree.changed" }, this.eventScopeForSession(session));
|
|
2235
2499
|
if (this.isCurrentActiveSession(session))
|
|
2236
2500
|
this.publishActivity(session, this.t("activity.navigated", "Session tree changed"), "idle");
|
|
2237
2501
|
return { cancelled: false, ...(result.editorText === undefined ? {} : { editorText: result.editorText }) };
|
|
2238
2502
|
}
|
|
2239
2503
|
catch (error) {
|
|
2240
|
-
const message = error
|
|
2504
|
+
const message = userFacingErrorMessage(error, this.t);
|
|
2241
2505
|
if (this.isCurrentActiveSession(session)) {
|
|
2242
2506
|
this.publishActivity(session, this.t("server.main.8cae209986b4", "tree navigation failed"), "error", message);
|
|
2243
2507
|
this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
|
|
@@ -2248,23 +2512,29 @@ export class PiSessionService {
|
|
|
2248
2512
|
this.treeNavigations.delete(session);
|
|
2249
2513
|
if (this.isCurrentActiveSession(session)) {
|
|
2250
2514
|
this.flushDeferredTreeNavigationWork(session);
|
|
2515
|
+
this.reconcileRuntimeMessageCount(session);
|
|
2251
2516
|
this.publishStatus(session);
|
|
2252
2517
|
}
|
|
2253
2518
|
else {
|
|
2254
2519
|
this.deferredGeneratedSessionNames.delete(session);
|
|
2255
|
-
this.
|
|
2520
|
+
this.subsessionInbox(session.sessionId, session.sessionFile, this.eventScopeForSession(session)).releaseClaims();
|
|
2256
2521
|
}
|
|
2257
2522
|
}
|
|
2258
2523
|
}
|
|
2259
2524
|
async forkFromTree(ref, request, managementContext) {
|
|
2525
|
+
return this.forkSessionTree(ref, request, managementContext);
|
|
2526
|
+
}
|
|
2527
|
+
async forkSessionTree(ref, request, managementContext, options, expectedSession) {
|
|
2260
2528
|
if (request.entryId.trim() === "")
|
|
2261
2529
|
throw new Error(this.t("server.main.f10a242f597a", "Session tree entry is required"));
|
|
2262
2530
|
const requestedSessionId = sessionIdFromLookup(ref);
|
|
2263
2531
|
if (this.isTreeExclusiveSessionIdentityActive(requestedSessionId)) {
|
|
2264
2532
|
throw new Error(this.t("server.main.3f5b7f08e91c", "Stop current session activity before forking the session tree"));
|
|
2265
2533
|
}
|
|
2266
|
-
await this.assertWritable(ref);
|
|
2534
|
+
await this.assertWritable(ref, managementContext);
|
|
2267
2535
|
const session = await this.getOrOpen(ref, managementContext);
|
|
2536
|
+
if (expectedSession !== undefined && session !== expectedSession)
|
|
2537
|
+
throw new Error(this.t("session.extensionUnavailable", "This extension command belongs to an unavailable session runtime"));
|
|
2268
2538
|
if (this.hasActiveWork(session))
|
|
2269
2539
|
throw new Error(this.t("server.main.3f5b7f08e91c", "Stop current session activity before forking the session tree"));
|
|
2270
2540
|
if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
|
|
@@ -2276,8 +2546,9 @@ export class PiSessionService {
|
|
|
2276
2546
|
this.publishActivity(session, this.t("server.main.4de911d3b72a", "forking session from entry"), "active");
|
|
2277
2547
|
this.publishStatus(session);
|
|
2278
2548
|
try {
|
|
2279
|
-
const
|
|
2280
|
-
|
|
2549
|
+
const entryId = request.retainCheckpoint === true ? messageHistoryTargetId(session.sessionManager.getBranch(), request.entryId) : request.entryId;
|
|
2550
|
+
const result = await this.commandService.forkEntry(session.sessionId, entryId, sourceEventScope, {
|
|
2551
|
+
...options, expectedLeafId: request.expectedLeafId, expectedSession: session,
|
|
2281
2552
|
});
|
|
2282
2553
|
if (result.type === "unsupported")
|
|
2283
2554
|
throw new Error(result.message);
|
|
@@ -2291,6 +2562,9 @@ export class PiSessionService {
|
|
|
2291
2562
|
return { cancelled: true };
|
|
2292
2563
|
}
|
|
2293
2564
|
const forked = this.activeForLookup({ id: result.session.id, cwd: result.session.cwd }, managementContext)?.runtime.session;
|
|
2565
|
+
if (forked !== undefined)
|
|
2566
|
+
this.setSuggestedInput(forked, result.promptDraft);
|
|
2567
|
+
this.events.publish(sourceSessionId, { type: "session.tree.changed" }, sourceEventScope);
|
|
2294
2568
|
if (forked !== undefined && this.isCurrentActiveSession(forked)) {
|
|
2295
2569
|
this.publishActivity(forked, this.t("command.forked", "Session forked"), "idle");
|
|
2296
2570
|
this.publishStatus(forked);
|
|
@@ -2304,7 +2578,7 @@ export class PiSessionService {
|
|
|
2304
2578
|
};
|
|
2305
2579
|
}
|
|
2306
2580
|
catch (error) {
|
|
2307
|
-
const message = error
|
|
2581
|
+
const message = userFacingErrorMessage(error, this.t);
|
|
2308
2582
|
if (this.isCurrentActiveSession(session)) {
|
|
2309
2583
|
this.publishActivity(session, this.t("server.main.08e8f3ea9eaa", "fork failed"), "error", message);
|
|
2310
2584
|
this.events.publish(session.sessionId, { type: "session.error", message }, sourceEventScope);
|
|
@@ -2424,7 +2698,7 @@ export class PiSessionService {
|
|
|
2424
2698
|
async archiveTree(ref, managementContext) {
|
|
2425
2699
|
const session = await this.getOrOpen(ref, managementContext);
|
|
2426
2700
|
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
2427
|
-
const root =
|
|
2701
|
+
const root = findArchiveCandidateByExactId(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false, this.t);
|
|
2428
2702
|
const plan = planSessionArchiveTree(root, catalog);
|
|
2429
2703
|
const busy = plan.targets.map((target) => target.activeSession).find((target) => target !== undefined && this.hasActiveWork(target));
|
|
2430
2704
|
if (busy !== undefined)
|
|
@@ -2517,7 +2791,7 @@ export class PiSessionService {
|
|
|
2517
2791
|
};
|
|
2518
2792
|
}
|
|
2519
2793
|
async reload(ref, managementContext) {
|
|
2520
|
-
await this.assertWritable(ref);
|
|
2794
|
+
await this.assertWritable(ref, managementContext);
|
|
2521
2795
|
const active = await this.getActive(ref, managementContext);
|
|
2522
2796
|
const session = active.runtime.session;
|
|
2523
2797
|
if (this.hasActiveWork(session))
|
|
@@ -2554,7 +2828,7 @@ export class PiSessionService {
|
|
|
2554
2828
|
this.publishStatus(reopenedSession);
|
|
2555
2829
|
}
|
|
2556
2830
|
async detachParent(ref, managementContext) {
|
|
2557
|
-
await this.assertWritable(ref);
|
|
2831
|
+
await this.assertWritable(ref, managementContext);
|
|
2558
2832
|
const session = await this.getOrOpen(ref, managementContext);
|
|
2559
2833
|
const sessionFile = session.sessionFile;
|
|
2560
2834
|
if (sessionFile === undefined || sessionFile === "")
|
|
@@ -2569,6 +2843,10 @@ export class PiSessionService {
|
|
|
2569
2843
|
const active = this.activeForLookup(ref, managementContext);
|
|
2570
2844
|
if (active === undefined)
|
|
2571
2845
|
return;
|
|
2846
|
+
this.subsessionStoppedParents.add(active.runtime.session);
|
|
2847
|
+
const mcpOwner = this.mcpAuthOwners.get(active.runtime.session);
|
|
2848
|
+
if (mcpOwner !== undefined)
|
|
2849
|
+
this.mcpAuthFlows.cancelSession(mcpOwner);
|
|
2572
2850
|
this.clearCompactionPromptQueue(active.runtime.session);
|
|
2573
2851
|
clearSessionQueue(active.runtime.session);
|
|
2574
2852
|
this.abortRunScopedExtensionDialogs(active.runtime.session);
|
|
@@ -2585,8 +2863,136 @@ export class PiSessionService {
|
|
|
2585
2863
|
this.publishStatus(active.runtime.session);
|
|
2586
2864
|
}
|
|
2587
2865
|
}
|
|
2866
|
+
async mutateQueueMessage(ref, input, managementContext) {
|
|
2867
|
+
const mutation = parseQueueMessageMutation(input);
|
|
2868
|
+
await this.assertWritable(ref, managementContext);
|
|
2869
|
+
const session = await this.getOrOpen(ref, managementContext);
|
|
2870
|
+
await this.assertWritable(ref, managementContext);
|
|
2871
|
+
if (this.activeForLookup(ref, managementContext)?.runtime.session !== session
|
|
2872
|
+
|| !cwdPathsEqual(ref.cwd, session.sessionManager.getCwd())
|
|
2873
|
+
|| this.eventScopeForSession(session) !== eventScopeFromManagementContext(managementContext)) {
|
|
2874
|
+
throw new UserFacingError("routing.sessionNotFound", "Session not found");
|
|
2875
|
+
}
|
|
2876
|
+
// Compare the entire execution-order queue after all awaits, then mutate
|
|
2877
|
+
// synchronously so neither another request nor the agent loop can interleave.
|
|
2878
|
+
const current = this.queuedMessages(session);
|
|
2879
|
+
if (this.queueCompatibility.reason(session) !== undefined) {
|
|
2880
|
+
this.publishStatus(session);
|
|
2881
|
+
this.queueCompatibility.assertAvailable(session);
|
|
2882
|
+
}
|
|
2883
|
+
if (current.length !== mutation.expectedQueue.length || current.some((entry, index) => {
|
|
2884
|
+
const expected = mutation.expectedQueue[index];
|
|
2885
|
+
if (expected === undefined)
|
|
2886
|
+
return true;
|
|
2887
|
+
return entry.kind !== expected.kind || entry.text !== expected.text
|
|
2888
|
+
|| (mutation.action === "edit" && index === mutation.index && (entry.hasAttachments === true) !== (expected.hasAttachments === true));
|
|
2889
|
+
}))
|
|
2890
|
+
throw new UserFacingError("queueControls.changed", "Queued messages changed; refresh the queue and try again");
|
|
2891
|
+
if (mutation.index >= current.length)
|
|
2892
|
+
throw new UserFacingError("queueControls.invalidIndex", "Invalid queue index");
|
|
2893
|
+
this.beginSessionEntryMutation(session, this.t("queueControls.mutateAction", "change queued messages"));
|
|
2894
|
+
try {
|
|
2895
|
+
this.queueCompatibility.run(session, () => { this.applyQueueMessageMutation(session, mutation); });
|
|
2896
|
+
}
|
|
2897
|
+
catch (error) {
|
|
2898
|
+
this.publishStatus(session);
|
|
2899
|
+
throw error;
|
|
2900
|
+
}
|
|
2901
|
+
finally {
|
|
2902
|
+
this.endSessionEntryMutation(session);
|
|
2903
|
+
}
|
|
2904
|
+
this.publishStatus(session);
|
|
2905
|
+
return this.statusFromSession(session);
|
|
2906
|
+
}
|
|
2907
|
+
applyQueueMessageMutation(session, mutation) {
|
|
2908
|
+
const agent = mutableQueueAgent(session.agent);
|
|
2909
|
+
const mirrors = { steer: mutableTextMirror(session.getSteeringMessages()), followUp: mutableTextMirror(session.getFollowUpMessages()) };
|
|
2910
|
+
if (session.getSteeringMessages() !== mirrors.steer || session.getFollowUpMessages() !== mirrors.followUp) {
|
|
2911
|
+
throw new Error("SDK queue getters no longer expose stable backing arrays");
|
|
2912
|
+
}
|
|
2913
|
+
const queues = snapshotAgentQueues(agent);
|
|
2914
|
+
const originalQueues = { steer: [...queues.steer], followUp: [...queues.followUp] };
|
|
2915
|
+
const originalMirrors = { steer: [...mirrors.steer], followUp: [...mirrors.followUp] };
|
|
2916
|
+
const held = this.compactionQueuedMessages(session).map((prompt) => ({ ...prompt }));
|
|
2917
|
+
const entries = ["steer", "followUp"].flatMap((kind) => [
|
|
2918
|
+
...mirroredQueueEntries(queues[kind], mirrors[kind]).map((entry) => ({ ...entry, kind })),
|
|
2919
|
+
...held.filter((prompt) => prompt.kind === kind).map((prompt) => ({ kind, text: prompt.text, prompt })),
|
|
2920
|
+
]);
|
|
2921
|
+
const selected = entries[mutation.index];
|
|
2922
|
+
if (selected === undefined)
|
|
2923
|
+
throw new UserFacingError("queueControls.invalidIndex", "Invalid queue index");
|
|
2924
|
+
const removed = mutation.action === "edit" ? entries.slice(mutation.index) : [selected];
|
|
2925
|
+
const remaining = entries.filter((entry) => !removed.includes(entry));
|
|
2926
|
+
for (const entry of removed) {
|
|
2927
|
+
if (entry.message !== undefined) {
|
|
2928
|
+
const queue = queues[entry.kind];
|
|
2929
|
+
queue.splice(queue.indexOf(entry.message), 1);
|
|
2930
|
+
}
|
|
2931
|
+
if (entry.prompt !== undefined)
|
|
2932
|
+
held.splice(held.indexOf(entry.prompt), 1);
|
|
2933
|
+
}
|
|
2934
|
+
if (mutation.action === "toggle") {
|
|
2935
|
+
const kind = selected.kind === "steer" ? "followUp" : "steer";
|
|
2936
|
+
// Destination includes held prompts: append there when necessary to keep
|
|
2937
|
+
// status order and eventual execution order identical.
|
|
2938
|
+
if (selected.prompt !== undefined) {
|
|
2939
|
+
selected.prompt.kind = kind;
|
|
2940
|
+
held.push(selected.prompt);
|
|
2941
|
+
remaining.push({ ...selected, kind });
|
|
2942
|
+
}
|
|
2943
|
+
else if (selected.message !== undefined) {
|
|
2944
|
+
if (held.some((prompt) => prompt.kind === kind)) {
|
|
2945
|
+
const prompt = { kind, text: selected.text, message: selected.message };
|
|
2946
|
+
held.push(prompt);
|
|
2947
|
+
remaining.push({ kind, text: selected.text, prompt });
|
|
2948
|
+
}
|
|
2949
|
+
else {
|
|
2950
|
+
queues[kind].push(selected.message);
|
|
2951
|
+
remaining.push({ ...selected, kind });
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
try {
|
|
2956
|
+
agent.clearAllQueues();
|
|
2957
|
+
for (const message of queues.steer)
|
|
2958
|
+
agent.steer(message);
|
|
2959
|
+
for (const message of queues.followUp)
|
|
2960
|
+
agent.followUp(message);
|
|
2961
|
+
for (const kind of ["steer", "followUp"]) {
|
|
2962
|
+
mirrors[kind].splice(0, mirrors[kind].length, ...remaining.filter((entry) => entry.kind === kind && entry.message !== undefined).map((entry) => entry.text));
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
catch (error) {
|
|
2966
|
+
// Best-effort rollback when a callable SDK method fails during replacement.
|
|
2967
|
+
// Restoration itself can fail after an incompatible behavioral change.
|
|
2968
|
+
const failures = [error];
|
|
2969
|
+
try {
|
|
2970
|
+
agent.clearAllQueues();
|
|
2971
|
+
for (const message of originalQueues.steer)
|
|
2972
|
+
agent.steer(message);
|
|
2973
|
+
for (const message of originalQueues.followUp)
|
|
2974
|
+
agent.followUp(message);
|
|
2975
|
+
}
|
|
2976
|
+
catch (restoreError) {
|
|
2977
|
+
failures.push(restoreError);
|
|
2978
|
+
}
|
|
2979
|
+
for (const kind of ["steer", "followUp"]) {
|
|
2980
|
+
try {
|
|
2981
|
+
mirrors[kind].splice(0, mirrors[kind].length, ...originalMirrors[kind]);
|
|
2982
|
+
}
|
|
2983
|
+
catch (restoreError) {
|
|
2984
|
+
failures.push(restoreError);
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
throw new AggregateError(failures, "Could not replace SDK queues; attempted to restore their original contents", { cause: error });
|
|
2988
|
+
}
|
|
2989
|
+
if (held.length === 0)
|
|
2990
|
+
this.clearCompactionPromptQueue(session);
|
|
2991
|
+
else
|
|
2992
|
+
this.compactionPromptQueues.set(activeSessionKey(session.sessionId, this.eventScopeForSession(session)), held);
|
|
2993
|
+
}
|
|
2588
2994
|
async clearQueue(ref, managementContext) {
|
|
2589
|
-
await this.assertWritable(ref);
|
|
2995
|
+
await this.assertWritable(ref, managementContext);
|
|
2590
2996
|
const session = await this.getOrOpen(ref, managementContext);
|
|
2591
2997
|
this.clearCompactionPromptQueue(session);
|
|
2592
2998
|
clearSessionQueue(session);
|
|
@@ -2767,9 +3173,14 @@ export class PiSessionService {
|
|
|
2767
3173
|
this.publishNotificationMutations(mutations);
|
|
2768
3174
|
}
|
|
2769
3175
|
this.forgetUnreadActivity(active.runtime.session);
|
|
2770
|
-
this.pendingAskStore.forgetSession(this.dialogSessionKey(active.runtime.session));
|
|
2771
3176
|
this.endSessionExtensionDialogs(active.runtime.session);
|
|
3177
|
+
const mcpOwner = this.mcpAuthOwners.get(active.runtime.session);
|
|
3178
|
+
if (mcpOwner !== undefined)
|
|
3179
|
+
this.mcpAuthFlows.cancelSession(mcpOwner);
|
|
3180
|
+
this.rememberManagementMediaFile(active.runtime.session, active.eventScope);
|
|
2772
3181
|
this.active.delete(activeSessionKey(sessionId, active.eventScope));
|
|
3182
|
+
this.suggestedInputs.delete(this.dialogSessionKey(active.runtime.session));
|
|
3183
|
+
this.events.mediaIndex.forgetSession({ id: sessionId, cwd: active.runtime.session.sessionManager.getCwd(), eventScope: active.eventScope });
|
|
2773
3184
|
this.activities.delete(activeSessionKey(sessionId, active.eventScope));
|
|
2774
3185
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
|
|
2775
3186
|
this.clearAuthLossWarningsForSession(sessionId);
|
|
@@ -2813,9 +3224,13 @@ export class PiSessionService {
|
|
|
2813
3224
|
if (branchSummaryAbortFailed)
|
|
2814
3225
|
throw branchSummaryAbortError;
|
|
2815
3226
|
}
|
|
2816
|
-
async assertWritable(ref) {
|
|
3227
|
+
async assertWritable(ref, managementContext) {
|
|
2817
3228
|
if (await this.getArchived(ref) !== undefined)
|
|
2818
3229
|
throw new Error(this.t("session.archiveReadOnly", "Archived sessions are read-only. Restore the session to continue."));
|
|
3230
|
+
const session = this.activeForLookup(ref, managementContext)?.runtime.session;
|
|
3231
|
+
const owner = session === undefined ? undefined : this.mcpAuthOwners.get(session);
|
|
3232
|
+
if (owner !== undefined)
|
|
3233
|
+
this.mcpAuthFlows.assertSessionAvailable(owner);
|
|
2819
3234
|
}
|
|
2820
3235
|
async getOrOpen(ref, managementContext) {
|
|
2821
3236
|
return (await this.getActive(ref, managementContext)).runtime.session;
|
|
@@ -2827,12 +3242,6 @@ export class PiSessionService {
|
|
|
2827
3242
|
const exact = this.startupSessions.get(activeSessionKey(sessionId, eventScope));
|
|
2828
3243
|
if (exact !== undefined && exact.managementContextKey === contextKey && lookupMatchesStartupSession(ref, exact.session))
|
|
2829
3244
|
return exact.session;
|
|
2830
|
-
for (const startup of this.startupSessions.values()) {
|
|
2831
|
-
if (startup.managementContextKey !== contextKey || !startup.session.sessionId.startsWith(sessionId))
|
|
2832
|
-
continue;
|
|
2833
|
-
if (lookupMatchesStartupSession(ref, startup.session))
|
|
2834
|
-
return startup.session;
|
|
2835
|
-
}
|
|
2836
3245
|
return undefined;
|
|
2837
3246
|
}
|
|
2838
3247
|
/** Resolve status and dialog-close calls even while extension startup is waiting for the browser. */
|
|
@@ -2900,7 +3309,7 @@ export class PiSessionService {
|
|
|
2900
3309
|
}
|
|
2901
3310
|
const match = isPiSessionRef(ref)
|
|
2902
3311
|
? await this.sessionManager.resolveSessionFile(ref.cwd, ref.id)
|
|
2903
|
-
:
|
|
3312
|
+
: findSessionByExactId(await this.sessionManager.listAll(), ref);
|
|
2904
3313
|
if (!match)
|
|
2905
3314
|
throw new Error(this.t("routing.sessionNotFound", "Session not found"));
|
|
2906
3315
|
return this.openPending(match.id, match.cwd, managementContext, () => this.sessionManager.open(match.path));
|
|
@@ -2948,11 +3357,6 @@ export class PiSessionService {
|
|
|
2948
3357
|
const exact = this.active.get(activeSessionKey(sessionId, eventScope));
|
|
2949
3358
|
if (exact !== undefined && lookupMatchesActiveSession(ref, exact) && exact.managementContextKey === key)
|
|
2950
3359
|
return exact;
|
|
2951
|
-
for (const active of this.active.values()) {
|
|
2952
|
-
const candidateId = active.runtime.session.sessionId;
|
|
2953
|
-
if (candidateId.startsWith(sessionId) && lookupMatchesActiveSession(ref, active) && active.managementContextKey === key)
|
|
2954
|
-
return active;
|
|
2955
|
-
}
|
|
2956
3360
|
return undefined;
|
|
2957
3361
|
}
|
|
2958
3362
|
activeForLookupAny(ref) {
|
|
@@ -2960,11 +3364,6 @@ export class PiSessionService {
|
|
|
2960
3364
|
const exact = this.activeForSessionId(sessionId);
|
|
2961
3365
|
if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
|
|
2962
3366
|
return exact;
|
|
2963
|
-
for (const active of this.active.values()) {
|
|
2964
|
-
const candidateId = active.runtime.session.sessionId;
|
|
2965
|
-
if (candidateId.startsWith(sessionId) && lookupMatchesActiveSession(ref, active))
|
|
2966
|
-
return active;
|
|
2967
|
-
}
|
|
2968
3367
|
return undefined;
|
|
2969
3368
|
}
|
|
2970
3369
|
getActiveForCommand(sessionId, eventScope) {
|
|
@@ -3030,8 +3429,14 @@ export class PiSessionService {
|
|
|
3030
3429
|
await session.bindExtensions({
|
|
3031
3430
|
uiContext,
|
|
3032
3431
|
mode: "rpc",
|
|
3432
|
+
commandContextActions: this.extensionCommandActions(session),
|
|
3033
3433
|
onError: (error) => {
|
|
3034
3434
|
const message = `${error.extensionPath}: ${error.error}`;
|
|
3435
|
+
const operation = McpAuthFlowService.current(session);
|
|
3436
|
+
if (operation !== undefined) {
|
|
3437
|
+
operation.error(message);
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3035
3440
|
this.publishActivity(session, this.t("activity.extensionError", "Extension error"), "error", message);
|
|
3036
3441
|
this.events.publish(session.sessionId, { type: "session.error", message }, owner.eventScope);
|
|
3037
3442
|
},
|
|
@@ -3045,9 +3450,89 @@ export class PiSessionService {
|
|
|
3045
3450
|
this.startupSessions.delete(key);
|
|
3046
3451
|
}
|
|
3047
3452
|
}
|
|
3453
|
+
extensionCommandActions(session) {
|
|
3454
|
+
const ref = { id: session.sessionId, cwd: session.sessionManager.getCwd() };
|
|
3455
|
+
const managementContext = this.sessionManagementContexts.get(session);
|
|
3456
|
+
return {
|
|
3457
|
+
waitForIdle: () => {
|
|
3458
|
+
this.assertCurrentExtensionSession(session);
|
|
3459
|
+
return session.waitForIdle();
|
|
3460
|
+
},
|
|
3461
|
+
navigateTree: (targetId, options) => {
|
|
3462
|
+
const expectedLeafId = session.sessionManager.getLeafId();
|
|
3463
|
+
return this.runExtensionSessionControl(session, async () => {
|
|
3464
|
+
const result = await this.navigateSessionTree(ref, targetId, expectedLeafId, options ?? { summarize: false }, managementContext, session);
|
|
3465
|
+
return { cancelled: result.cancelled };
|
|
3466
|
+
});
|
|
3467
|
+
},
|
|
3468
|
+
fork: (entryId, options) => {
|
|
3469
|
+
const expectedLeafId = session.sessionManager.getLeafId();
|
|
3470
|
+
const withSession = options?.withSession;
|
|
3471
|
+
let completeReplacement;
|
|
3472
|
+
const forkOptions = {
|
|
3473
|
+
...options,
|
|
3474
|
+
position: options?.position ?? "before",
|
|
3475
|
+
// Capture Pi's fresh context, but run user code after the host's
|
|
3476
|
+
// replacement reservation is released so fresh controls can work.
|
|
3477
|
+
...(withSession === undefined ? {} : { withSession: (ctx) => {
|
|
3478
|
+
completeReplacement = () => withSession(ctx);
|
|
3479
|
+
return Promise.resolve();
|
|
3480
|
+
} }),
|
|
3481
|
+
};
|
|
3482
|
+
return this.runExtensionSessionControl(session, async () => {
|
|
3483
|
+
const runtime = this.activeForLookup(ref, managementContext)?.runtime;
|
|
3484
|
+
const result = await this.forkSessionTree(ref, { entryId, expectedLeafId }, managementContext, forkOptions, session);
|
|
3485
|
+
if (!result.cancelled) {
|
|
3486
|
+
try {
|
|
3487
|
+
await completeReplacement?.();
|
|
3488
|
+
}
|
|
3489
|
+
catch (error) {
|
|
3490
|
+
// The fork is committed. Report callback failure on the current
|
|
3491
|
+
// runtime (which may have forked again), including its recoverable inbox.
|
|
3492
|
+
const current = runtime?.session;
|
|
3493
|
+
if (current !== undefined && this.isCurrentActiveSession(current)) {
|
|
3494
|
+
const message = userFacingErrorMessage(error, this.t);
|
|
3495
|
+
current.extensionRunner.getUIContext().notify(message, "error");
|
|
3496
|
+
this.publishActivity(current, "extension callback failed", "error", message);
|
|
3497
|
+
this.events.publish(current.sessionId, { type: "session.error", message }, this.eventScopeForSession(current));
|
|
3498
|
+
}
|
|
3499
|
+
throw error;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
return { cancelled: result.cancelled };
|
|
3503
|
+
});
|
|
3504
|
+
},
|
|
3505
|
+
reload: () => this.runExtensionSessionControl(session, () => this.reloadSessionRuntime(session)),
|
|
3506
|
+
// These need host-owned creation/selection policies, not SDK runtime-only
|
|
3507
|
+
// replacement. Fail visibly until PI WEB supplies that integration.
|
|
3508
|
+
newSession: () => Promise.reject(new Error(this.t("session.extensionNewUnsupported", "ctx.newSession() is not supported in PI WEB. Start a new session from the web UI."))),
|
|
3509
|
+
switchSession: () => Promise.reject(new Error(this.t("session.extensionSwitchUnsupported", "ctx.switchSession() is not supported in PI WEB. Select a session from the web UI."))),
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
assertCurrentExtensionSession(session) {
|
|
3513
|
+
if (!this.isCurrentActiveSession(session) || this.startupSessions.has(activeSessionKey(session.sessionId, this.eventScopeForSession(session)))) {
|
|
3514
|
+
throw new Error(this.t("session.extensionUnavailable", "This extension command belongs to an unavailable session runtime"));
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
runExtensionSessionControl(session, operation) {
|
|
3518
|
+
return this.extensionCommandExecution.run({ ...this.extensionCommandExecution.getStore(), sessionControl: true }, async () => {
|
|
3519
|
+
this.assertCurrentExtensionSession(session);
|
|
3520
|
+
await this.assertWritable({ id: session.sessionId, cwd: session.sessionManager.getCwd() }, this.sessionManagementContexts.get(session));
|
|
3521
|
+
this.assertCurrentExtensionSession(session);
|
|
3522
|
+
return operation();
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
setSuggestedInput(session, text) {
|
|
3526
|
+
const key = this.dialogSessionKey(session);
|
|
3527
|
+
if (text === undefined || text === "")
|
|
3528
|
+
this.suggestedInputs.delete(key);
|
|
3529
|
+
else
|
|
3530
|
+
this.suggestedInputs.set(key, text);
|
|
3531
|
+
}
|
|
3048
3532
|
bindRuntime(active, session = active.runtime.session) {
|
|
3049
3533
|
this.sessionEventScopes.set(session, active.eventScope);
|
|
3050
3534
|
this.runtimeBySession.set(session, active.runtime);
|
|
3535
|
+
this.rememberManagementMediaFile(session, active.eventScope);
|
|
3051
3536
|
active.unsubscribe();
|
|
3052
3537
|
for (const [sessionKey, candidate] of this.active.entries()) {
|
|
3053
3538
|
if (candidate === active) {
|
|
@@ -3059,34 +3544,97 @@ export class PiSessionService {
|
|
|
3059
3544
|
if (active.eventScope === NORMAL_SESSION_EVENT_SCOPE && active.runtime.session !== session) {
|
|
3060
3545
|
this.sessionEvents.close(active.runtime.session);
|
|
3061
3546
|
}
|
|
3547
|
+
this.reconcileRuntimeMessageCount(session);
|
|
3548
|
+
this.restoreSubsessionReceipts(session);
|
|
3062
3549
|
const unsubscribe = session.subscribe((event) => {
|
|
3063
|
-
|
|
3550
|
+
const eventType = getString(event, "type");
|
|
3551
|
+
const message = getProperty(event, "message");
|
|
3552
|
+
const entry = getProperty(event, "entry");
|
|
3553
|
+
const inbox = this.subsessionInbox(session.sessionId, session.sessionFile, active.eventScope);
|
|
3554
|
+
if (eventType === "agent_start") {
|
|
3555
|
+
this.restoreSubsessionReceipts(session, false);
|
|
3556
|
+
this.subsessionStoppedParents.delete(session);
|
|
3557
|
+
}
|
|
3558
|
+
if (eventType === "message_end" && getString(message, "role") === "assistant"
|
|
3559
|
+
&& (getString(message, "stopReason") === "aborted" || getString(message, "stopReason") === "error"))
|
|
3560
|
+
this.subsessionStoppedParents.add(session);
|
|
3561
|
+
if (eventType === "entry_appended" && getString(entry, "type") === "custom_message"
|
|
3562
|
+
&& getString(entry, "customType") === SUBSESSION_NOTIFICATION_CUSTOM_TYPE && this.subsessionReceiptMatches(session, entry))
|
|
3563
|
+
inbox.receive(completionReceiptIds(getProperty(entry, "details")));
|
|
3564
|
+
if (eventType === "turn_end" || eventType === "agent_settled")
|
|
3565
|
+
this.restoreSubsessionReceipts(session);
|
|
3566
|
+
if (eventType === "agent_settled")
|
|
3567
|
+
this.scheduleSubsessionDelivery(session);
|
|
3568
|
+
const role = getString(message, "role");
|
|
3569
|
+
if ((eventType === "message_end" && (role === "user" || role === "assistant" || role === "toolResult" || role === "system"))
|
|
3570
|
+
|| (eventType === "entry_appended" && getString(entry, "type") === "message")) {
|
|
3571
|
+
this.runtimeMessageCounts.set(session, this.runtimeMessageCount(session) + 1);
|
|
3572
|
+
}
|
|
3573
|
+
if (eventType === "agent_settled" || eventType === "session_tree")
|
|
3574
|
+
this.reconcileRuntimeMessageCount(session);
|
|
3575
|
+
if (eventType === "message_start" && role === "assistant")
|
|
3576
|
+
this.assistantStreamIds.set(session, randomUUID());
|
|
3577
|
+
const streamId = this.assistantStreamIds.get(session);
|
|
3578
|
+
if ((eventType === "message_start" || eventType === "message_update") && role === "assistant") {
|
|
3579
|
+
this.publishedAssistantPartials.set(session, structuredClone(annotateAssistantStreamId(annotateAssistantThinkingLevel(projectBrowserMessage(message), session.thinkingLevel), streamId)));
|
|
3580
|
+
}
|
|
3581
|
+
else if ((eventType === "message_end" && role === "assistant") || eventType === "agent_end") {
|
|
3582
|
+
this.publishedAssistantPartials.delete(session);
|
|
3583
|
+
}
|
|
3584
|
+
const clientEvent = toClientEvent(event, session.thinkingLevel, streamId);
|
|
3585
|
+
if ((clientEvent.type === "message.end" || clientEvent.type === "message.append") && clientEvent.message !== undefined) {
|
|
3586
|
+
clientEvent.message = this.browserTranscriptMessage(session, clientEvent.message);
|
|
3587
|
+
}
|
|
3588
|
+
this.events.publish(session.sessionId, clientEvent, active.eventScope, { id: session.sessionId, cwd: session.sessionManager.getCwd() });
|
|
3589
|
+
if ((eventType === "message_end" && role === "assistant") || eventType === "agent_end")
|
|
3590
|
+
this.assistantStreamIds.delete(session);
|
|
3064
3591
|
this.auditToolExecutionEvent(active, event);
|
|
3065
3592
|
this.auditAssistantResponseEvent(active, event);
|
|
3066
3593
|
this.publishActivityForEvent(session, event);
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
void this.voidOpenAskForUserMessage(session).catch((error) => {
|
|
3070
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3071
|
-
this.publishActivity(session, this.t("activity.extensionError", "Extension error"), "error", message);
|
|
3072
|
-
this.events.publish(session.sessionId, { type: "session.error", message }, active.eventScope);
|
|
3073
|
-
});
|
|
3074
|
-
}
|
|
3594
|
+
if (eventType === "message_start" && role === "user")
|
|
3595
|
+
this.suggestedInputs.delete(this.dialogSessionKey(session));
|
|
3075
3596
|
if (eventType === "agent_end")
|
|
3076
3597
|
this.abortRunScopedExtensionDialogs(session);
|
|
3077
|
-
if (eventType === "compaction_end")
|
|
3598
|
+
if (eventType === "compaction_end") {
|
|
3599
|
+
if (getString(event, "reason") === "manual")
|
|
3600
|
+
this.manualCompactionEnded.add(session);
|
|
3601
|
+
this.publishCompactionOutcome(session, event);
|
|
3078
3602
|
this.scheduleCompactionQueueDrain(session);
|
|
3603
|
+
}
|
|
3079
3604
|
if (eventType === "agent_start" || eventType === "agent_end")
|
|
3080
3605
|
this.scheduleCompactionQueueDrain(session);
|
|
3081
3606
|
this.publishStatus(session);
|
|
3082
|
-
this.updateSubsessionTracking(session);
|
|
3607
|
+
this.updateSubsessionTracking(session, eventType === "agent_start", eventType === "agent_settled");
|
|
3608
|
+
if (eventType === "bash_execution_end" || eventType === "compaction_end")
|
|
3609
|
+
this.scheduleSubsessionDelivery(session);
|
|
3083
3610
|
});
|
|
3084
3611
|
active.unsubscribe = () => {
|
|
3085
3612
|
if (active.eventScope === NORMAL_SESSION_EVENT_SCOPE)
|
|
3086
3613
|
this.sessionEvents.close(session);
|
|
3087
3614
|
unsubscribe();
|
|
3615
|
+
this.publishedAssistantPartials.delete(session);
|
|
3616
|
+
this.assistantStreamIds.delete(session);
|
|
3088
3617
|
};
|
|
3089
3618
|
this.active.set(activeSessionKey(session.sessionId, active.eventScope), active);
|
|
3619
|
+
this.scheduleSubsessionDelivery(session);
|
|
3620
|
+
}
|
|
3621
|
+
publishCompactionOutcome(session, event) {
|
|
3622
|
+
const aborted = getBoolean(event, "aborted") === true;
|
|
3623
|
+
const errorMessage = getString(event, "errorMessage");
|
|
3624
|
+
const automatic = getString(event, "reason") !== "manual";
|
|
3625
|
+
const message = aborted
|
|
3626
|
+
? (automatic ? this.t("session.compaction.autoCancelled", "Automatic compaction cancelled.") : this.t("session.compaction.cancelled", "Compaction cancelled."))
|
|
3627
|
+
: errorMessage === undefined ? undefined : this.t("session.compaction.failed", "Could not compact history. Try again.");
|
|
3628
|
+
if (!aborted && errorMessage !== undefined) {
|
|
3629
|
+
this.events.publish(session.sessionId, { type: "session.diagnostic", message: errorMessage }, this.eventScopeForSession(session));
|
|
3630
|
+
}
|
|
3631
|
+
this.publishActivity(session, aborted ? this.t("activity.compactionCancelled", "Compaction cancelled") : errorMessage === undefined ? this.t("activity.compacted", "Context compacted") : this.t("activity.compactionFailed", "Context compaction failed"), !aborted && errorMessage !== undefined ? "error" : "idle", message);
|
|
3632
|
+
// Successful compaction already has a canonical summary in the transcript.
|
|
3633
|
+
// Keep cancellation/failure feedback in the refresh-safe session inbox only.
|
|
3634
|
+
const generation = this.notificationGenerationBySession.get(session);
|
|
3635
|
+
if (message !== undefined && generation !== undefined) {
|
|
3636
|
+
this.publishNotificationMutations(this.notificationsForSession(session).addNotification(generation, message, aborted ? "info" : "error").mutations);
|
|
3637
|
+
}
|
|
3090
3638
|
}
|
|
3091
3639
|
scheduleCompactionQueueDrain(session, delayMs = 0) {
|
|
3092
3640
|
const sessionKey = activeSessionKey(session.sessionId, this.eventScopeForSession(session));
|
|
@@ -3113,16 +3661,27 @@ export class PiSessionService {
|
|
|
3113
3661
|
return;
|
|
3114
3662
|
this.publishStatus(session);
|
|
3115
3663
|
for (const prompt of queued)
|
|
3116
|
-
void this.
|
|
3664
|
+
void this.submitHeldPrompt(session, prompt, prompt.kind);
|
|
3117
3665
|
return;
|
|
3118
3666
|
}
|
|
3119
3667
|
const prompt = this.shiftCompactionPrompt(sessionKey);
|
|
3120
3668
|
if (prompt === undefined)
|
|
3121
3669
|
return;
|
|
3122
3670
|
this.publishStatus(session);
|
|
3123
|
-
const submitted = this.
|
|
3671
|
+
const submitted = this.submitHeldPrompt(session, prompt);
|
|
3124
3672
|
void submitted.finally(() => { this.scheduleCompactionQueueDrain(session); });
|
|
3125
3673
|
}
|
|
3674
|
+
submitHeldPrompt(session, prompt, kind) {
|
|
3675
|
+
if (prompt.message === undefined)
|
|
3676
|
+
return this.submitPrompt(session, prompt.text, kind, prompt.images, prompt.echoUserMessage ?? true);
|
|
3677
|
+
const images = prompt.message.role === "user" && Array.isArray(prompt.message.content)
|
|
3678
|
+
? prompt.message.content.filter((part) => part.type === "image") : [];
|
|
3679
|
+
// This SDK message was already submitted/audited before moving into held
|
|
3680
|
+
// storage. Preserve public runtime ownership without re-expansion or audit.
|
|
3681
|
+
return this.beginPromptSubmission(session, prompt.text, kind, images, prompt.echoUserMessage ?? true, {
|
|
3682
|
+
expandPromptTemplates: false, auditUserPrompt: false,
|
|
3683
|
+
}).catch(() => undefined);
|
|
3684
|
+
}
|
|
3126
3685
|
takeCompactionPromptQueue(sessionId) {
|
|
3127
3686
|
const queued = this.compactionPromptQueues.get(sessionId) ?? [];
|
|
3128
3687
|
this.compactionPromptQueues.delete(sessionId);
|
|
@@ -3130,7 +3689,8 @@ export class PiSessionService {
|
|
|
3130
3689
|
}
|
|
3131
3690
|
shiftCompactionPrompt(sessionId) {
|
|
3132
3691
|
const queue = this.compactionPromptQueues.get(sessionId);
|
|
3133
|
-
const
|
|
3692
|
+
const steeringIndex = queue?.findIndex((prompt) => prompt.kind === "steer") ?? -1;
|
|
3693
|
+
const prompt = queue?.splice(steeringIndex < 0 ? 0 : steeringIndex, 1)[0];
|
|
3134
3694
|
if (queue === undefined || queue.length === 0)
|
|
3135
3695
|
this.compactionPromptQueues.delete(sessionId);
|
|
3136
3696
|
return prompt;
|
|
@@ -3154,20 +3714,30 @@ export class PiSessionService {
|
|
|
3154
3714
|
maybeGenerateSessionName(session, firstMessage) {
|
|
3155
3715
|
if (session.sessionName !== undefined || session.messages.length !== 0 || session.isStreaming || session.isCompacting)
|
|
3156
3716
|
return;
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
return;
|
|
3161
|
-
}
|
|
3162
|
-
const model = session.model;
|
|
3163
|
-
if (model === undefined)
|
|
3164
|
-
return;
|
|
3165
|
-
void generateShortSessionName(session.agent.streamFunction, model, firstMessage).then((name) => {
|
|
3717
|
+
void this.generateSessionName(session, firstMessage).then((name) => {
|
|
3718
|
+
if (name === undefined)
|
|
3719
|
+
this.logger.info({ sessionId: session.sessionId }, "Session naming returned no title; using the first-message fallback");
|
|
3166
3720
|
this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
|
|
3167
|
-
}).catch(() => {
|
|
3721
|
+
}).catch((error) => {
|
|
3722
|
+
const errorCategory = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError") ? "timeout" : "generation";
|
|
3723
|
+
this.logger.info({ sessionId: session.sessionId, errorCategory }, "Session naming failed; using the first-message fallback");
|
|
3168
3724
|
this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
|
|
3169
3725
|
});
|
|
3170
3726
|
}
|
|
3727
|
+
async generateSessionName(session, firstMessage) {
|
|
3728
|
+
// One request keeps its settings even if the operator edits the config while it runs.
|
|
3729
|
+
const naming = this.config === undefined ? {} : (await this.config.read()).effectiveConfig.sessionNaming ?? {};
|
|
3730
|
+
let model = session.model;
|
|
3731
|
+
if (naming.model !== undefined) {
|
|
3732
|
+
const parsed = parseModelSpec(naming.model);
|
|
3733
|
+
model = parsed === undefined ? undefined : session.modelRuntime.getModel(parsed.provider, parsed.modelId);
|
|
3734
|
+
if (model === undefined)
|
|
3735
|
+
throw new Error("Configured session naming model is unavailable");
|
|
3736
|
+
}
|
|
3737
|
+
if (model === undefined)
|
|
3738
|
+
return undefined;
|
|
3739
|
+
return generateShortSessionName(session.agent.streamFunction, model, firstMessage, naming);
|
|
3740
|
+
}
|
|
3171
3741
|
applyGeneratedSessionName(session, name) {
|
|
3172
3742
|
if (name === undefined || session.sessionName !== undefined)
|
|
3173
3743
|
return;
|
|
@@ -3262,8 +3832,10 @@ export class PiSessionService {
|
|
|
3262
3832
|
return this.t("activity.working", "Working");
|
|
3263
3833
|
}
|
|
3264
3834
|
hasActiveWork(session) {
|
|
3835
|
+
const execution = this.extensionCommandExecution.getStore();
|
|
3836
|
+
const ownCommandReceipt = execution?.sessionControl === true && execution.submission?.active === true && execution.submission.session === session ? 1 : 0;
|
|
3265
3837
|
return this.treeNavigations.has(session)
|
|
3266
|
-
|| this.isSessionEntryMutationActive(session)
|
|
3838
|
+
|| this.isSessionEntryMutationActive(session, ownCommandReceipt)
|
|
3267
3839
|
|| this.isTreeExclusiveOperationActive(session)
|
|
3268
3840
|
|| sessionHasActiveWork(session, this.compactionQueuedMessages(session).length);
|
|
3269
3841
|
}
|
|
@@ -3337,6 +3909,9 @@ export class PiSessionService {
|
|
|
3337
3909
|
}
|
|
3338
3910
|
}
|
|
3339
3911
|
beginSessionEntryMutation(session, action) {
|
|
3912
|
+
const owner = this.mcpAuthOwners.get(session);
|
|
3913
|
+
if (owner !== undefined)
|
|
3914
|
+
this.mcpAuthFlows.assertSessionAvailable(owner);
|
|
3340
3915
|
this.assertTreeNavigationInactive(session, action);
|
|
3341
3916
|
this.sessionEntryMutationCounts.set(session, (this.sessionEntryMutationCounts.get(session) ?? 0) + 1);
|
|
3342
3917
|
this.observeUnreadActivityState(session);
|
|
@@ -3348,9 +3923,11 @@ export class PiSessionService {
|
|
|
3348
3923
|
else
|
|
3349
3924
|
this.sessionEntryMutationCounts.set(session, remaining);
|
|
3350
3925
|
this.observeUnreadActivityState(session);
|
|
3926
|
+
if (remaining <= 0 && !this.subsessionDeliveringParents.has(session))
|
|
3927
|
+
this.scheduleSubsessionDelivery(session);
|
|
3351
3928
|
}
|
|
3352
|
-
isSessionEntryMutationActive(session) {
|
|
3353
|
-
return (this.sessionEntryMutationCounts.get(session) ?? 0) >
|
|
3929
|
+
isSessionEntryMutationActive(session, ignoredReceipts = 0) {
|
|
3930
|
+
return (this.sessionEntryMutationCounts.get(session) ?? 0) > ignoredReceipts;
|
|
3354
3931
|
}
|
|
3355
3932
|
publishActivityForEvent(session, event) {
|
|
3356
3933
|
const eventType = getString(event, "type");
|
|
@@ -3561,7 +4138,15 @@ export class PiSessionService {
|
|
|
3561
4138
|
this.publishStatus(session);
|
|
3562
4139
|
}
|
|
3563
4140
|
}
|
|
4141
|
+
rememberManagementMediaFile(session, eventScope) {
|
|
4142
|
+
if (eventScope === NORMAL_SESSION_EVENT_SCOPE)
|
|
4143
|
+
return;
|
|
4144
|
+
const path = session.sessionFile ?? session.sessionManager.getSessionFile();
|
|
4145
|
+
if (path !== undefined)
|
|
4146
|
+
this.managementMediaFiles.add(path);
|
|
4147
|
+
}
|
|
3564
4148
|
publishStatus(session) {
|
|
4149
|
+
this.rememberManagementMediaFile(session, this.eventScopeForSession(session));
|
|
3565
4150
|
void this.refreshActivityMarker(session);
|
|
3566
4151
|
const status = this.statusFromSession(session);
|
|
3567
4152
|
this.clearStaleActiveActivity(session);
|
|
@@ -3617,30 +4202,52 @@ export class PiSessionService {
|
|
|
3617
4202
|
this.publishStatus(session);
|
|
3618
4203
|
}
|
|
3619
4204
|
}
|
|
3620
|
-
|
|
4205
|
+
reconcileRuntimeMessageCount(session) {
|
|
4206
|
+
const count = transcriptMessageCount(session.sessionManager.getBranch());
|
|
4207
|
+
this.runtimeMessageCounts.set(session, count);
|
|
4208
|
+
return count;
|
|
4209
|
+
}
|
|
4210
|
+
runtimeMessageCount(session) {
|
|
4211
|
+
return this.runtimeMessageCounts.get(session) ?? this.reconcileRuntimeMessageCount(session);
|
|
4212
|
+
}
|
|
4213
|
+
snapshotMessageCount(branch) {
|
|
4214
|
+
// The gateway reuses immutable disk snapshots until the file changes.
|
|
4215
|
+
// Keep disk counts separate: an idle runtime may lag external appends.
|
|
4216
|
+
const cached = this.snapshotMessageCounts.get(branch);
|
|
4217
|
+
if (cached !== undefined)
|
|
4218
|
+
return cached;
|
|
4219
|
+
const count = transcriptMessageCount(branch);
|
|
4220
|
+
this.snapshotMessageCounts.set(branch, count);
|
|
4221
|
+
return count;
|
|
4222
|
+
}
|
|
4223
|
+
statusFromSession(session, messageCount = this.runtimeMessageCount(session)) {
|
|
3621
4224
|
const stats = session.getSessionStats();
|
|
3622
4225
|
const model = session.model === undefined ? undefined : modelToClientModel(session.model);
|
|
3623
4226
|
const contextUsage = session.getContextUsage();
|
|
3624
4227
|
const warnings = this.warningsForSession(session);
|
|
3625
|
-
const pendingAsk = this.pendingAskStore.pendingAsk(this.dialogSessionKey(session));
|
|
3626
4228
|
const pendingDialogs = this.pendingExtensionDialogStore.pendingDialogs(this.dialogSessionKey(session));
|
|
4229
|
+
const suggestedInput = this.suggestedInputs.get(this.dialogSessionKey(session));
|
|
4230
|
+
const queuedMessages = this.queuedMessages(session);
|
|
4231
|
+
const queueMessageActionsUnavailable = this.queueCompatibility.reason(session);
|
|
3627
4232
|
return {
|
|
3628
4233
|
sessionId: session.sessionId,
|
|
3629
4234
|
persisted: sessionFileExists(session.sessionFile),
|
|
4235
|
+
recentlyActiveElsewhere: this.eventScopeForSession(session) === NORMAL_SESSION_EVENT_SCOPE && this.activityMarker.isActiveElsewhere(session.sessionFile),
|
|
3630
4236
|
...(model === undefined ? {} : { model }),
|
|
3631
4237
|
thinkingLevel: session.thinkingLevel,
|
|
3632
4238
|
isStreaming: session.isStreaming,
|
|
3633
4239
|
isCompacting: session.isCompacting,
|
|
3634
4240
|
isBashRunning: session.isBashRunning,
|
|
3635
4241
|
pendingMessageCount: this.pendingMessageCount(session),
|
|
3636
|
-
queuedMessages
|
|
4242
|
+
queuedMessages,
|
|
4243
|
+
...(queueMessageActionsUnavailable === undefined ? {} : { queueMessageActionsUnavailable }),
|
|
3637
4244
|
messageCount,
|
|
3638
4245
|
tokens: stats.tokens,
|
|
3639
4246
|
cost: stats.cost,
|
|
3640
4247
|
...(contextUsage === undefined ? {} : { contextUsage }),
|
|
3641
4248
|
...(warnings.length === 0 ? {} : { warnings }),
|
|
3642
|
-
...(pendingAsk === undefined ? {} : { pendingAsk }),
|
|
3643
4249
|
...(pendingDialogs.length === 0 ? {} : { pendingDialogs }),
|
|
4250
|
+
...(suggestedInput === undefined ? {} : { suggestedInput }),
|
|
3644
4251
|
};
|
|
3645
4252
|
}
|
|
3646
4253
|
/**
|
|
@@ -3652,14 +4259,6 @@ export class PiSessionService {
|
|
|
3652
4259
|
warningsForSession(session) {
|
|
3653
4260
|
const runtime = [...this.active.values()].find((active) => active.runtime.session === session)?.runtime;
|
|
3654
4261
|
const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
|
|
3655
|
-
if (this.eventScopeForSession(session) === NORMAL_SESSION_EVENT_SCOPE
|
|
3656
|
-
&& this.activityMarker.isActiveElsewhere(session.sessionFile)) {
|
|
3657
|
-
warnings.push({
|
|
3658
|
-
severity: "info",
|
|
3659
|
-
message: this.t("server.main.b80808e6ef11", "Recently active in another PI-WEB instance. Avoid working on this session in both instances at once."),
|
|
3660
|
-
source: "PI-WEB",
|
|
3661
|
-
});
|
|
3662
|
-
}
|
|
3663
4262
|
const authPath = modelRuntimeAuthPath(session.modelRuntime);
|
|
3664
4263
|
const anthropic = authPath === undefined ? undefined : anthropicSubscriptionWarning(session, authPath);
|
|
3665
4264
|
if (anthropic !== undefined)
|
|
@@ -3673,7 +4272,11 @@ export class PiSessionService {
|
|
|
3673
4272
|
return this.compactionPromptQueues.get(activeSessionKey(session.sessionId, this.eventScopeForSession(session))) ?? [];
|
|
3674
4273
|
}
|
|
3675
4274
|
hasQueuedMessageText(session, text) {
|
|
3676
|
-
return
|
|
4275
|
+
return this.queuedMessages(session).some((message) => message.text === text);
|
|
4276
|
+
}
|
|
4277
|
+
queuedMessages(session) {
|
|
4278
|
+
const held = this.compactionQueuedMessages(session);
|
|
4279
|
+
return this.queueCompatibility.inspect(session, () => queuedMessagesFromSession(session, held), () => queuedMessagesFromSession(session, held, false));
|
|
3677
4280
|
}
|
|
3678
4281
|
notificationsForScope(scope) {
|
|
3679
4282
|
if (scope === NORMAL_SESSION_EVENT_SCOPE)
|
|
@@ -3735,10 +4338,6 @@ export class PiSessionService {
|
|
|
3735
4338
|
this.publishNotificationMutations(result.mutations);
|
|
3736
4339
|
return result.snapshot;
|
|
3737
4340
|
}
|
|
3738
|
-
async deliverSubsessionNotification(session, notification) {
|
|
3739
|
-
await this.runSessionEntryMutation(session, this.t("session.action.notify", "deliver a subsession notification"), () => session.sendCustomMessage({ customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, content: notification.text, display: true, details: { sessionId: notification.childId } }, { triggerTurn: true, deliverAs: "followUp" }));
|
|
3740
|
-
this.publishStatus(session);
|
|
3741
|
-
}
|
|
3742
4341
|
logSubsessionNotificationFailure(parentId, childId, error) {
|
|
3743
4342
|
this.logger.info({ parentSessionId: parentId, sessionId: childId, error: error instanceof Error ? error.message : String(error) }, "failed to notify parent of subsession completion");
|
|
3744
4343
|
}
|
|
@@ -3749,9 +4348,14 @@ export class PiSessionService {
|
|
|
3749
4348
|
this.publishActivity(session, this.t("activity.reloading", "Reloading resources"), "active");
|
|
3750
4349
|
const priorGeneration = this.notificationGenerationBySession.get(session);
|
|
3751
4350
|
let candidateGeneration;
|
|
4351
|
+
const reloadBoundary = { beforeSessionStart: false };
|
|
3752
4352
|
try {
|
|
3753
|
-
await session.reload(
|
|
4353
|
+
await session.reload({
|
|
3754
4354
|
beforeSessionStart: () => {
|
|
4355
|
+
this.endSessionExtensionDialogs(session);
|
|
4356
|
+
reloadBoundary.beforeSessionStart = true;
|
|
4357
|
+
if (priorGeneration === undefined)
|
|
4358
|
+
return;
|
|
3755
4359
|
candidateGeneration = this.notificationsForSession(session).beginReplacement(priorGeneration, notificationIdentityForSession(session));
|
|
3756
4360
|
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
3757
4361
|
this.replaceSessionNotificationContext(session, candidateGeneration);
|
|
@@ -3765,6 +4369,8 @@ export class PiSessionService {
|
|
|
3765
4369
|
this.publishStatus(session);
|
|
3766
4370
|
}
|
|
3767
4371
|
catch (error) {
|
|
4372
|
+
if (!reloadBoundary.beforeSessionStart)
|
|
4373
|
+
this.endSessionExtensionDialogs(session);
|
|
3768
4374
|
if (candidateGeneration !== undefined) {
|
|
3769
4375
|
this.publishNotificationMutations(this.notificationsForSession(session).abortReplacement(candidateGeneration, "candidate"));
|
|
3770
4376
|
this.notificationGenerationBySession.set(session, candidateGeneration);
|
|
@@ -3880,7 +4486,11 @@ export class PiSessionService {
|
|
|
3880
4486
|
// holds for this session; settle those waits before the new
|
|
3881
4487
|
// runtime's extensions can open fresh dialogs under the same id.
|
|
3882
4488
|
this.endSessionExtensionDialogs(boundSession);
|
|
4489
|
+
const changedIdentity = boundSession.sessionId !== session.sessionId;
|
|
3883
4490
|
boundSession = session;
|
|
4491
|
+
this.startupSessions.set(activeSessionKey(session.sessionId, active.eventScope), { session, eventScope: active.eventScope, managementContextKey: active.managementContextKey });
|
|
4492
|
+
if (changedIdentity)
|
|
4493
|
+
this.announceCreatedSession(active, session.sessionManager.getCwd());
|
|
3884
4494
|
await this.bindSessionExtensions(session, candidateGeneration, active);
|
|
3885
4495
|
if (candidateGeneration !== undefined) {
|
|
3886
4496
|
this.publishNotificationMutations(notificationStore.commitReplacement(candidateGeneration));
|
|
@@ -3950,6 +4560,11 @@ export class PiSessionService {
|
|
|
3950
4560
|
const eventScope = this.eventScopeForSession(session);
|
|
3951
4561
|
const notificationStore = this.notificationsForScope(eventScope);
|
|
3952
4562
|
const notify = (message, type) => {
|
|
4563
|
+
const operation = McpAuthFlowService.current(session);
|
|
4564
|
+
if (operation !== undefined) {
|
|
4565
|
+
operation.notify(message);
|
|
4566
|
+
return;
|
|
4567
|
+
}
|
|
3953
4568
|
if (generation === undefined) {
|
|
3954
4569
|
this.events.publish(session.sessionId, {
|
|
3955
4570
|
type: "command.output",
|
|
@@ -3973,13 +4588,16 @@ export class PiSessionService {
|
|
|
3973
4588
|
if (property === "theme")
|
|
3974
4589
|
return plainTextTheme;
|
|
3975
4590
|
if (property === "confirm") {
|
|
3976
|
-
return (title, message, opts) =>
|
|
4591
|
+
return (title, message, opts) => McpAuthFlowService.current(session)?.confirm(title, message, opts)
|
|
4592
|
+
?? this.openExtensionDialog(session, { kind: "confirm", title, message }, opts, startupDialogs.signal);
|
|
3977
4593
|
}
|
|
3978
4594
|
if (property === "select") {
|
|
3979
|
-
return (title, options, opts) =>
|
|
4595
|
+
return (title, options, opts) => McpAuthFlowService.current(session)?.select(title, options, opts)
|
|
4596
|
+
?? this.openExtensionDialog(session, { kind: "select", title, options }, opts, startupDialogs.signal);
|
|
3980
4597
|
}
|
|
3981
4598
|
if (property === "input") {
|
|
3982
|
-
return (title, placeholder, opts) =>
|
|
4599
|
+
return (title, placeholder, opts) => McpAuthFlowService.current(session)?.input(title, placeholder, opts)
|
|
4600
|
+
?? this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts, startupDialogs.signal);
|
|
3983
4601
|
}
|
|
3984
4602
|
const value = Reflect.get(target, property, receiver);
|
|
3985
4603
|
return value;
|
|
@@ -4119,14 +4737,287 @@ export class PiSessionService {
|
|
|
4119
4737
|
this.logger.info({ sessionId: session.sessionId, error: error instanceof Error ? error.message : String(error) }, "failed to apply deferred session name");
|
|
4120
4738
|
}
|
|
4121
4739
|
}
|
|
4122
|
-
|
|
4123
|
-
this.
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4740
|
+
this.restoreSubsessionReceipts(session);
|
|
4741
|
+
this.scheduleSubsessionDelivery(session);
|
|
4742
|
+
}
|
|
4743
|
+
async deliverAskOutcome(session, outcome) {
|
|
4744
|
+
// Pi persists/displays the structured message and wakes an idle agent, or
|
|
4745
|
+
// queues it behind in-flight work. Ordinary entry-mutation protection still
|
|
4746
|
+
// applies, but there is no answer-specific stale/duplicate gate.
|
|
4747
|
+
await this.runSessionEntryMutation(session, this.t("server.main.07f784061918", "deliver answers to your questions"), () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: true, deliverAs: "followUp" }));
|
|
4748
|
+
this.publishStatus(session);
|
|
4749
|
+
return this.statusFromSession(session);
|
|
4750
|
+
}
|
|
4751
|
+
async joinSubsessions(parentSessionId, parentSessionFile, toolCallId, managementContext) {
|
|
4752
|
+
const scope = eventScopeFromManagementContext(managementContext);
|
|
4753
|
+
const subsessions = await this.listSubsessions(parentSessionId, parentSessionFile, managementContext);
|
|
4754
|
+
this.reconcileSubsessionCompletions(parentSessionId, parentSessionFile, scope);
|
|
4755
|
+
const completions = this.subsessionInbox(parentSessionId, parentSessionFile, scope).pending();
|
|
4756
|
+
this.receiveSubsessionCompletions(parentSessionId, parentSessionFile, completions, toolCallId, scope);
|
|
4757
|
+
return { subsessions, completions };
|
|
4758
|
+
}
|
|
4759
|
+
subsessionInbox(parentSessionId, parentSessionFile, scope = NORMAL_SESSION_EVENT_SCOPE) {
|
|
4760
|
+
const file = nonEmptyString(parentSessionFile) ?? nonEmptyString(this.active.get(activeSessionKey(parentSessionId, scope))?.runtime.session.sessionFile);
|
|
4761
|
+
const key = subsessionHydratedParentKey(activeSessionKey(parentSessionId, scope), file);
|
|
4762
|
+
let inbox = this.subsessionInboxes.get(key);
|
|
4763
|
+
if (inbox === undefined) {
|
|
4764
|
+
inbox = new SubsessionCompletionInbox();
|
|
4765
|
+
this.subsessionInboxes.set(key, inbox);
|
|
4766
|
+
}
|
|
4767
|
+
return inbox;
|
|
4768
|
+
}
|
|
4769
|
+
restoreSubsessionReceipts(session, releaseClaims = true) {
|
|
4770
|
+
this.subsessionInbox(session.sessionId, session.sessionFile, this.eventScopeForSession(session)).restoreReceipts(session.sessionManager.getBranch().filter((entry) => this.subsessionReceiptMatches(session, entry)), releaseClaims);
|
|
4771
|
+
}
|
|
4772
|
+
subsessionCompletionForOutput(parentId, parentFile, child, messages, scope) {
|
|
4773
|
+
const branch = persistedSubsessionCompletions(child.sessionManager.getBranch()).filter(({ sessionId }) => sessionId === child.sessionId);
|
|
4774
|
+
// A child can be rewound independently of its parent. Never acknowledge an
|
|
4775
|
+
// abandoned branch's newer result while returning the current branch's text.
|
|
4776
|
+
const completion = branch.at(-1) ?? (child.sessionManager.appendCustomEntry === undefined
|
|
4777
|
+
? this.subsessionInbox(parentId, parentFile, scope).latest(child.sessionId) : undefined);
|
|
4778
|
+
return completion?.finalText === finalAssistantText(messages) ? completion : undefined;
|
|
4779
|
+
}
|
|
4780
|
+
receiveSubsessionCompletions(parentId, parentFile, completions, toolCallId, scope) {
|
|
4781
|
+
if (completions.length === 0)
|
|
4782
|
+
return;
|
|
4783
|
+
const parent = this.active.get(activeSessionKey(parentId, scope))?.runtime.session;
|
|
4784
|
+
if (parent === undefined || (parentFile !== undefined && !sessionFileMatches(parent, parentFile))) {
|
|
4785
|
+
throw new UserFacingError("session.parentReceiptUnavailable", "Parent session {parentSessionId} is not available to receive subsession results", { parentSessionId: parentId });
|
|
4786
|
+
}
|
|
4787
|
+
const inbox = this.subsessionInbox(parentId, parentFile, scope);
|
|
4788
|
+
const completionIds = completions.map(({ completionId }) => completionId).filter((id) => !inbox.hasReceived(id));
|
|
4789
|
+
if (completionIds.length === 0)
|
|
4790
|
+
return;
|
|
4791
|
+
// Persist before removing unread results. A storage failure leaves the
|
|
4792
|
+
// answer unread and fails the tool instead of losing the notification.
|
|
4793
|
+
this.assertTreeNavigationInactive(parent, this.t("session.action.receiveSubsessions", "receive subsession results"));
|
|
4794
|
+
parent.sessionManager.appendCustomEntry?.(SUBSESSION_RECEIPT_ENTRY_TYPE, { receiptOwner: this.subsessionReceiptOwner(parent), completionIds, ...(toolCallId === undefined ? {} : { toolCallId }) });
|
|
4795
|
+
inbox.receive(completionIds);
|
|
4796
|
+
}
|
|
4797
|
+
reconcileSubsessionCompletions(parentId, parentFile, scope) {
|
|
4798
|
+
for (const childId of this.subsessionChildren.get(activeSessionKey(parentId, scope)) ?? []) {
|
|
4799
|
+
if (!this.subsessionLinkBelongsToParent(parentId, nonEmptyString(parentFile), childId, scope))
|
|
4800
|
+
continue;
|
|
4801
|
+
const link = this.subsessionLinks.get(activeSessionKey(childId, scope));
|
|
4802
|
+
const child = link === undefined ? undefined : this.activeChildForSubsessionLink(link)?.runtime.session;
|
|
4803
|
+
if (child !== undefined)
|
|
4804
|
+
this.reconcileSubsessionCompletion(child);
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
reconcileSubsessionCompletion(session) {
|
|
4808
|
+
this.updateSubsessionTracking(session);
|
|
4809
|
+
if (!this.hasActiveWork(session) && this.subsessionNotifyArmed.get(activeSessionKey(session.sessionId, this.eventScopeForSession(session))) === true) {
|
|
4810
|
+
throw new UserFacingError("session.subsessionCompletionPersistFailed", "Could not record completion of subsession {sessionId}; its result remains unread. Retry after fixing session storage.", { sessionId: session.sessionId });
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
async prepareSubsessionDelivery(parentId, parentFile, managementContext) {
|
|
4814
|
+
const scope = eventScopeFromManagementContext(managementContext);
|
|
4815
|
+
await this.hydrateSubsessionsForParent(parentId, nonEmptyString(parentFile), managementContext);
|
|
4816
|
+
const parent = this.active.get(activeSessionKey(parentId, scope))?.runtime.session;
|
|
4817
|
+
if (parent === undefined || (parentFile !== undefined && !sessionFileMatches(parent, parentFile)))
|
|
4818
|
+
return undefined;
|
|
4819
|
+
// Tool receipts are intents until the successful (possibly nested) result
|
|
4820
|
+
// is persisted. Reconcile those intents without releasing in-flight drafts.
|
|
4821
|
+
this.restoreSubsessionReceipts(parent, false);
|
|
4822
|
+
this.reconcileSubsessionCompletions(parentId, parentFile, scope);
|
|
4823
|
+
const completions = this.subsessionInbox(parentId, parentFile, scope).claim();
|
|
4824
|
+
if (completions.length === 0)
|
|
4825
|
+
return undefined;
|
|
4826
|
+
return {
|
|
4827
|
+
type: "custom_message", customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, display: true,
|
|
4828
|
+
content: renderSubsessionCompletions(completions, this.workingSubsessionIds(parentId, scope)),
|
|
4829
|
+
details: { receiptOwner: this.subsessionReceiptOwner(parent), sessionId: completions[0]?.sessionId, completionIds: completions.map(({ completionId }) => completionId) },
|
|
4830
|
+
};
|
|
4831
|
+
}
|
|
4832
|
+
async deliverPendingSubsessionCompletions(session) {
|
|
4833
|
+
if (this.hasActiveWork(session) || !this.isCurrentActiveSession(session) || this.subsessionStoppedParents.has(session))
|
|
4834
|
+
return;
|
|
4835
|
+
const inbox = this.subsessionInbox(session.sessionId, session.sessionFile, this.eventScopeForSession(session));
|
|
4836
|
+
const completions = inbox.claim();
|
|
4837
|
+
if (completions.length === 0)
|
|
4838
|
+
return;
|
|
4839
|
+
const completionIds = completions.map(({ completionId }) => completionId);
|
|
4840
|
+
this.subsessionDeliveringParents.add(session);
|
|
4841
|
+
let delivered = false;
|
|
4842
|
+
try {
|
|
4843
|
+
await this.runSessionEntryMutation(session, this.t("session.action.deliverSubsessions", "deliver subsession results"), () => session.sendCustomMessage({
|
|
4844
|
+
customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, display: true,
|
|
4845
|
+
content: renderSubsessionCompletions(completions, this.workingSubsessionIds(session.sessionId, this.eventScopeForSession(session))),
|
|
4846
|
+
details: { receiptOwner: this.subsessionReceiptOwner(session), sessionId: completions[0]?.sessionId, completionIds },
|
|
4847
|
+
}, { triggerTurn: true }));
|
|
4848
|
+
inbox.receive(completionIds);
|
|
4849
|
+
delivered = true;
|
|
4850
|
+
}
|
|
4851
|
+
catch (error) {
|
|
4852
|
+
this.subsessionStoppedParents.add(session);
|
|
4853
|
+
throw error;
|
|
4854
|
+
}
|
|
4855
|
+
finally {
|
|
4856
|
+
inbox.release(completionIds);
|
|
4857
|
+
this.subsessionDeliveringParents.delete(session);
|
|
4858
|
+
if (delivered)
|
|
4859
|
+
this.scheduleSubsessionDelivery(session);
|
|
4860
|
+
this.publishStatus(session);
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
scheduleSubsessionDelivery(session) {
|
|
4864
|
+
if (this.subsessionDeliveryScheduled.has(session) || this.subsessionInbox(session.sessionId, session.sessionFile, this.eventScopeForSession(session)).pending().length === 0)
|
|
4865
|
+
return;
|
|
4866
|
+
this.subsessionDeliveryScheduled.add(session);
|
|
4867
|
+
// Settlement can precede the host prompt-mutation receipt's finally block.
|
|
4868
|
+
// Recheck on the next tick, closing the yield/child-completion race without
|
|
4869
|
+
// restarting a stopped run or putting rendered answers in a follow-up queue.
|
|
4870
|
+
setTimeout(() => {
|
|
4871
|
+
this.subsessionDeliveryScheduled.delete(session);
|
|
4872
|
+
void this.deliverPendingSubsessionCompletions(session).catch((error) => {
|
|
4873
|
+
this.logSubsessionNotificationFailure(session.sessionId, "inbox", error);
|
|
4127
4874
|
});
|
|
4875
|
+
}, 0);
|
|
4876
|
+
}
|
|
4877
|
+
assertMcpModeAllowed(managementContext) {
|
|
4878
|
+
if (VERSION !== "1.3.0")
|
|
4879
|
+
throw new UserFacingError("mcpAuth.sdkUnsupported", "Dedicated MCP authentication requires Pi 1.3.0");
|
|
4880
|
+
if (managementContext !== undefined && !managementUsesNormalPermissions(managementContext, { allowPrivileged: this.managementAllowPrivileged })) {
|
|
4881
|
+
throw new UserFacingError("mcpAuth.restrictedManagement", "MCP authentication is unavailable in restricted management mode");
|
|
4128
4882
|
}
|
|
4129
4883
|
}
|
|
4884
|
+
async mcpAuthSession(ref, managementContext) {
|
|
4885
|
+
this.assertMcpModeAllowed(managementContext);
|
|
4886
|
+
await this.assertWritable(ref, managementContext);
|
|
4887
|
+
const session = await this.getOrOpen(ref, managementContext);
|
|
4888
|
+
await this.assertWritable(ref, managementContext);
|
|
4889
|
+
const projectPath = await this.projectPathForCwd?.(ref.cwd);
|
|
4890
|
+
const owner = {
|
|
4891
|
+
scope: eventScopeFromManagementContext(managementContext), sessionId: ref.id, cwd: canonicalizeStoredCwd(ref.cwd),
|
|
4892
|
+
...(projectPath === undefined ? {} : { projectPath: canonicalizeStoredCwd(projectPath) }),
|
|
4893
|
+
};
|
|
4894
|
+
await this.assertMcpAuthIdentity(ref, session, owner);
|
|
4895
|
+
if (!session.extensionRunner.getRegisteredCommands().some((command) => command.invocationName === "mcp" && command.sourceInfo?.path === "builtin:mcp")) {
|
|
4896
|
+
throw new UserFacingError("mcpAuth.builtinUnavailable", "Pi's built-in MCP command is not available for this session");
|
|
4897
|
+
}
|
|
4898
|
+
if (this.hasActiveWork(session))
|
|
4899
|
+
throw new UserFacingError("mcpAuth.waitForIdle", "Wait for current session activity before authenticating MCP");
|
|
4900
|
+
this.mcpAuthOwners.set(session, owner);
|
|
4901
|
+
return { session, owner };
|
|
4902
|
+
}
|
|
4903
|
+
async assertMcpAuthIdentity(ref, session, owner) {
|
|
4904
|
+
this.assertCurrentExtensionSession(session);
|
|
4905
|
+
if (session.sessionId !== ref.id || !cwdPathsEqual(ref.cwd, session.sessionManager.getCwd()) || this.eventScopeForSession(session) !== owner.scope) {
|
|
4906
|
+
throw new UserFacingError("mcpAuth.workspaceChanged", "MCP authentication session workspace changed");
|
|
4907
|
+
}
|
|
4908
|
+
await this.assertMcpProjectOwner(owner);
|
|
4909
|
+
this.assertCurrentExtensionSession(session);
|
|
4910
|
+
if (session.sessionId !== ref.id || !cwdPathsEqual(ref.cwd, session.sessionManager.getCwd()) || this.eventScopeForSession(session) !== owner.scope) {
|
|
4911
|
+
throw new UserFacingError("mcpAuth.workspaceChanged", "MCP authentication session workspace changed");
|
|
4912
|
+
}
|
|
4913
|
+
if (session.settingsManager.isProjectTrusted?.() !== true)
|
|
4914
|
+
throw new UserFacingError("mcpAuth.trustRequired", "MCP authentication requires a trusted workspace");
|
|
4915
|
+
}
|
|
4916
|
+
async authorizedMcpFlowOwner(flowId, managementContext) {
|
|
4917
|
+
const owner = this.mcpAuthFlows.owner(flowId, eventScopeFromManagementContext(managementContext));
|
|
4918
|
+
this.assertMcpModeAllowed(managementContext);
|
|
4919
|
+
// Polling never constructs a runtime. Revalidate the captured project even
|
|
4920
|
+
// after the command ends, rather than trusting the browser's current cwd.
|
|
4921
|
+
await this.assertMcpProjectOwner(owner);
|
|
4922
|
+
return owner;
|
|
4923
|
+
}
|
|
4924
|
+
async assertMcpProjectOwner(owner) {
|
|
4925
|
+
const projectPath = await this.projectPathForCwd?.(owner.cwd);
|
|
4926
|
+
if ((projectPath === undefined) !== (owner.projectPath === undefined)
|
|
4927
|
+
|| (projectPath !== undefined && owner.projectPath !== undefined && !cwdPathsEqual(projectPath, owner.projectPath))) {
|
|
4928
|
+
throw new UserFacingError("mcpAuth.flowNotFound", "MCP authentication flow not found");
|
|
4929
|
+
}
|
|
4930
|
+
if (owner.scope === NORMAL_SESSION_EVENT_SCOPE)
|
|
4931
|
+
await this.normalModelRuntimeForCwd(owner.cwd);
|
|
4932
|
+
}
|
|
4933
|
+
async mcpAuthFlow(flowId, managementContext) {
|
|
4934
|
+
return this.mcpAuthFlows.get(flowId, await this.authorizedMcpFlowOwner(flowId, managementContext));
|
|
4935
|
+
}
|
|
4936
|
+
async respondToMcpAuthFlow(flowId, requestId, value, managementContext) {
|
|
4937
|
+
return this.mcpAuthFlows.respond(flowId, requestId, value, await this.authorizedMcpFlowOwner(flowId, managementContext));
|
|
4938
|
+
}
|
|
4939
|
+
async cancelMcpAuthFlow(flowId, managementContext) {
|
|
4940
|
+
return this.mcpAuthFlows.cancel(flowId, await this.authorizedMcpFlowOwner(flowId, managementContext));
|
|
4941
|
+
}
|
|
4942
|
+
async discoverMcpAuthServers(ref, session, owner, managementContext) {
|
|
4943
|
+
return this.mcpAuthFlows.discover({
|
|
4944
|
+
owner, identity: session,
|
|
4945
|
+
isCurrent: () => this.isCurrentActiveSession(session) && cwdPathsEqual(ref.cwd, session.sessionManager.getCwd()) && session.settingsManager.isProjectTrusted?.() === true,
|
|
4946
|
+
run: async () => {
|
|
4947
|
+
// The SDK has no public MCP readiness promise. In RPC mode this supported
|
|
4948
|
+
// status command awaits startup without reconnecting or touching tokens.
|
|
4949
|
+
await session.prompt("/mcp", { expandPromptTemplates: true });
|
|
4950
|
+
await this.assertWritable(ref, managementContext);
|
|
4951
|
+
await this.assertMcpAuthIdentity(ref, session, owner);
|
|
4952
|
+
const command = session.extensionRunner.getRegisteredCommands().find((candidate) => candidate.invocationName === "mcp" && candidate.sourceInfo?.path === "builtin:mcp");
|
|
4953
|
+
if (!command)
|
|
4954
|
+
throw new UserFacingError("mcpAuth.builtinUnavailable", "Pi's built-in MCP command is not available for this session");
|
|
4955
|
+
const completions = await command.getArgumentCompletions?.("login ") ?? [];
|
|
4956
|
+
await this.assertWritable(ref, managementContext);
|
|
4957
|
+
await this.assertMcpAuthIdentity(ref, session, owner);
|
|
4958
|
+
if (completions.length > 200)
|
|
4959
|
+
throw new UserFacingError("mcpAuth.tooManyServers", "Too many MCP authentication servers");
|
|
4960
|
+
const servers = [];
|
|
4961
|
+
for (const item of completions) {
|
|
4962
|
+
const name = item.value.startsWith("login ") ? item.value.slice(6) : "";
|
|
4963
|
+
// Commands split on whitespace. Never forward an arbitrary suffix or
|
|
4964
|
+
// a name that could change the builtin command's argument structure.
|
|
4965
|
+
if (name.length === 0 || name.length > 256 || /\s/u.test(name) || Array.from(name).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127))
|
|
4966
|
+
continue;
|
|
4967
|
+
if ((item.description?.length ?? 0) > 32 * 1024)
|
|
4968
|
+
throw new UserFacingError("mcpAuth.descriptionTooLarge", "MCP server description is too large");
|
|
4969
|
+
if (!servers.some((server) => server.name === name))
|
|
4970
|
+
servers.push({ name, ...(item.description === undefined ? {} : { description: item.description }) });
|
|
4971
|
+
}
|
|
4972
|
+
return servers;
|
|
4973
|
+
},
|
|
4974
|
+
});
|
|
4975
|
+
}
|
|
4976
|
+
async mcpAuthServers(ref, managementContext) {
|
|
4977
|
+
const { session, owner } = await this.mcpAuthSession(ref, managementContext);
|
|
4978
|
+
if (this.hasActiveWork(session))
|
|
4979
|
+
throw new UserFacingError("mcpAuth.waitForIdleListing", "Wait for current session activity before listing MCP servers");
|
|
4980
|
+
return this.runSessionEntryMutation(session, "list MCP authentication servers", async () => ({ servers: await this.discoverMcpAuthServers(ref, session, owner, managementContext) }));
|
|
4981
|
+
}
|
|
4982
|
+
async startMcpAuth(ref, mode, serverName, managementContext) {
|
|
4983
|
+
const { session, owner } = await this.mcpAuthSession(ref, managementContext);
|
|
4984
|
+
if (this.hasActiveWork(session))
|
|
4985
|
+
throw new UserFacingError("mcpAuth.waitForIdle", "Wait for current session activity before authenticating MCP");
|
|
4986
|
+
const servers = await this.runSessionEntryMutation(session, "discover MCP authentication servers", () => this.discoverMcpAuthServers(ref, session, owner, managementContext));
|
|
4987
|
+
if (serverName !== undefined && !servers.some((server) => server.name === serverName))
|
|
4988
|
+
throw new UserFacingError("mcpAuth.ineligibleServer", "Unknown or ineligible MCP authentication server");
|
|
4989
|
+
await this.assertMcpAuthIdentity(ref, session, owner);
|
|
4990
|
+
if (this.hasActiveWork(session))
|
|
4991
|
+
throw new UserFacingError("mcpAuth.waitForIdle", "Wait for current session activity before authenticating MCP");
|
|
4992
|
+
return this.mcpAuthFlows.start({
|
|
4993
|
+
owner, mode,
|
|
4994
|
+
identity: session,
|
|
4995
|
+
isCurrent: () => this.isCurrentActiveSession(session) && session.sessionId === ref.id && cwdPathsEqual(ref.cwd, session.sessionManager.getCwd()) && session.settingsManager.isProjectTrusted?.() === true,
|
|
4996
|
+
// This is an awaited extension command, not a model prompt submission.
|
|
4997
|
+
// Avoid prompt echoes/activity and the fire-and-forget command receipt.
|
|
4998
|
+
run: () => this.runSessionEntryMutation(session, "authenticate MCP", async () => {
|
|
4999
|
+
const operation = McpAuthFlowService.current(session);
|
|
5000
|
+
if (!operation)
|
|
5001
|
+
throw new UserFacingError("mcpAuth.sessionNotCurrent", "MCP authentication session is no longer current");
|
|
5002
|
+
if (servers.length === 0) {
|
|
5003
|
+
operation.notify(this.t("mcpAuth.noEligibleServers", "No eligible MCP servers are available for this action."));
|
|
5004
|
+
return;
|
|
5005
|
+
}
|
|
5006
|
+
const selected = serverName ?? await operation.selectServer(servers);
|
|
5007
|
+
if (selected === undefined || operation.isCancelled())
|
|
5008
|
+
return;
|
|
5009
|
+
await this.assertWritable(ref, managementContext);
|
|
5010
|
+
await this.assertMcpAuthIdentity(ref, session, owner);
|
|
5011
|
+
const fresh = await this.discoverMcpAuthServers(ref, session, owner, managementContext);
|
|
5012
|
+
if (!fresh.some((server) => server.name === selected))
|
|
5013
|
+
throw new UserFacingError("mcpAuth.serverNoLongerEligible", "MCP authentication server is no longer eligible");
|
|
5014
|
+
if (operation.isCancelled())
|
|
5015
|
+
return;
|
|
5016
|
+
operation.setServer(selected);
|
|
5017
|
+
await session.prompt(`/mcp ${mode} ${selected}`, { expandPromptTemplates: true });
|
|
5018
|
+
}),
|
|
5019
|
+
});
|
|
5020
|
+
}
|
|
4130
5021
|
}
|
|
4131
5022
|
function previewResponseFromPlan(plan) {
|
|
4132
5023
|
return {
|
|
@@ -4153,13 +5044,13 @@ function bulkRefToLookup(ref) {
|
|
|
4153
5044
|
return ref;
|
|
4154
5045
|
}
|
|
4155
5046
|
function findArchivedRecordForBulkRef(records, ref) {
|
|
4156
|
-
return records.find((record) => record.cwd === ref.cwd &&
|
|
5047
|
+
return records.find((record) => record.cwd === ref.cwd && record.sessionId === ref.id);
|
|
4157
5048
|
}
|
|
4158
5049
|
function findListedSessionForBulkRef(context, ref) {
|
|
4159
|
-
return
|
|
5050
|
+
return findSessionByExactId(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
4160
5051
|
}
|
|
4161
|
-
function
|
|
4162
|
-
return sessions.find((session) => session.id === sessionId)
|
|
5052
|
+
function findSessionByExactId(sessions, sessionId) {
|
|
5053
|
+
return sessions.find((session) => session.id === sessionId);
|
|
4163
5054
|
}
|
|
4164
5055
|
function uniqueStrings(values) {
|
|
4165
5056
|
return [...new Set(values)];
|
|
@@ -4196,12 +5087,27 @@ function notificationIdentityForSession(session) {
|
|
|
4196
5087
|
cwd: canonicalizeStoredCwd(session.sessionManager.getCwd()),
|
|
4197
5088
|
};
|
|
4198
5089
|
}
|
|
5090
|
+
function clientSessionFromActiveSession(session) {
|
|
5091
|
+
const header = session.sessionManager.getHeader?.();
|
|
5092
|
+
const created = header?.timestamp ?? new Date().toISOString();
|
|
5093
|
+
const metadata = readSessionUiMetadata(session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch());
|
|
5094
|
+
return {
|
|
5095
|
+
id: session.sessionId, path: session.sessionFile ?? "", cwd: session.sessionManager.getCwd(),
|
|
5096
|
+
persisted: sessionFileExists(session.sessionFile), created, modified: created,
|
|
5097
|
+
messageCount: transcriptMessageCount(session.sessionManager.getBranch()), firstMessage: "",
|
|
5098
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
5099
|
+
...(session.sessionName === undefined ? {} : { name: session.sessionName }),
|
|
5100
|
+
...(header?.parentSession === undefined ? {} : { parentSessionPath: header.parentSession }),
|
|
5101
|
+
};
|
|
5102
|
+
}
|
|
4199
5103
|
function clientSessionFromListEntry(session) {
|
|
4200
5104
|
return {
|
|
4201
5105
|
id: session.id,
|
|
4202
5106
|
path: session.path,
|
|
4203
5107
|
cwd: session.cwd,
|
|
4204
5108
|
persisted: true,
|
|
5109
|
+
...(session.agentSubsession === true ? { agentSubsession: true } : {}),
|
|
5110
|
+
...(session.metadata === undefined ? {} : { metadata: requireSessionUiMetadata(session.metadata) }),
|
|
4205
5111
|
...(session.name === undefined ? {} : { name: session.name }),
|
|
4206
5112
|
created: session.created.toISOString(),
|
|
4207
5113
|
modified: session.modified.toISOString(),
|
|
@@ -4319,37 +5225,6 @@ function persistedChildSubsessionLinkData(parentSessionId, childSessionId) {
|
|
|
4319
5225
|
spawnedSessionId: childSessionId,
|
|
4320
5226
|
};
|
|
4321
5227
|
}
|
|
4322
|
-
function parsePersistedParentSubsessionLink(entry) {
|
|
4323
|
-
if (!isRecord(entry) || entry["type"] !== "custom" || entry["customType"] !== SUBSESSION_LINK_CUSTOM_TYPE)
|
|
4324
|
-
return undefined;
|
|
4325
|
-
const data = entry["data"];
|
|
4326
|
-
if (!isRecord(data))
|
|
4327
|
-
return undefined;
|
|
4328
|
-
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
4329
|
-
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
4330
|
-
if (spawnedBySessionId === undefined || spawnedBySessionId === "" || spawnedSessionId === undefined || spawnedSessionId === "")
|
|
4331
|
-
return undefined;
|
|
4332
|
-
const spawnedSessionFile = getString(data, "spawnedSessionFile");
|
|
4333
|
-
const cwd = getString(data, "cwd");
|
|
4334
|
-
return {
|
|
4335
|
-
spawnedBySessionId,
|
|
4336
|
-
spawnedSessionId,
|
|
4337
|
-
...(spawnedSessionFile === undefined || spawnedSessionFile === "" ? {} : { spawnedSessionFile }),
|
|
4338
|
-
...(cwd === undefined || cwd === "" ? {} : { cwd }),
|
|
4339
|
-
};
|
|
4340
|
-
}
|
|
4341
|
-
function parsePersistedChildSubsessionLink(entry) {
|
|
4342
|
-
if (!isRecord(entry) || entry["type"] !== "custom" || entry["customType"] !== SUBSESSION_CHILD_LINK_CUSTOM_TYPE)
|
|
4343
|
-
return undefined;
|
|
4344
|
-
const data = entry["data"];
|
|
4345
|
-
if (!isRecord(data))
|
|
4346
|
-
return undefined;
|
|
4347
|
-
const spawnedBySessionId = getString(data, "spawnedBySessionId");
|
|
4348
|
-
const spawnedSessionId = getString(data, "spawnedSessionId");
|
|
4349
|
-
if (spawnedBySessionId === undefined || spawnedBySessionId === "" || spawnedSessionId === undefined || spawnedSessionId === "")
|
|
4350
|
-
return undefined;
|
|
4351
|
-
return { spawnedBySessionId, spawnedSessionId };
|
|
4352
|
-
}
|
|
4353
5228
|
function nonEmptyString(value) {
|
|
4354
5229
|
return value === undefined || value === "" ? undefined : value;
|
|
4355
5230
|
}
|
|
@@ -4408,12 +5283,37 @@ function clearParentSessionHeader(sessionManager) {
|
|
|
4408
5283
|
function clearSessionQueue(session) {
|
|
4409
5284
|
session.clearQueue();
|
|
4410
5285
|
}
|
|
4411
|
-
function queuedMessagesFromSession(session, extraQueuedMessages = []) {
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
5286
|
+
function queuedMessagesFromSession(session, extraQueuedMessages = [], inspectSdk = true) {
|
|
5287
|
+
const texts = inspectSdk
|
|
5288
|
+
? { steer: mutableTextMirror(session.getSteeringMessages()), followUp: mutableTextMirror(session.getFollowUpMessages()) }
|
|
5289
|
+
: { steer: publicQueueTexts(() => session.getSteeringMessages()), followUp: publicQueueTexts(() => session.getFollowUpMessages()) };
|
|
5290
|
+
const attachmentCounts = { steer: [], followUp: [] };
|
|
5291
|
+
if (inspectSdk && (texts.steer.length > 0 || texts.followUp.length > 0 || extraQueuedMessages.length > 0)) {
|
|
5292
|
+
const queues = snapshotAgentQueues(mutableQueueAgent(session.agent));
|
|
5293
|
+
for (const kind of ["steer", "followUp"]) {
|
|
5294
|
+
attachmentCounts[kind] = queuedMessageAttachmentCounts(queues[kind], texts[kind]);
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
return ["steer", "followUp"].flatMap((kind) => [
|
|
5298
|
+
...texts[kind].map((text, index) => ({ kind, text, ...queuedAttachmentMetadata(attachmentCounts[kind][index] ?? 0) })),
|
|
5299
|
+
...extraQueuedMessages.filter((message) => message.kind === kind).map((prompt) => ({
|
|
5300
|
+
kind, text: prompt.text,
|
|
5301
|
+
...queuedAttachmentMetadata(prompt.message === undefined ? prompt.images?.length ?? 0 : queuedMessageAttachmentCount(prompt.message)),
|
|
5302
|
+
})),
|
|
5303
|
+
]);
|
|
5304
|
+
}
|
|
5305
|
+
/** Degraded status still uses public text getters where their values are usable. */
|
|
5306
|
+
function publicQueueTexts(read) {
|
|
5307
|
+
try {
|
|
5308
|
+
const value = read();
|
|
5309
|
+
return Array.isArray(value) ? value.filter((text) => typeof text === "string") : [];
|
|
5310
|
+
}
|
|
5311
|
+
catch {
|
|
5312
|
+
return [];
|
|
5313
|
+
} // The compatibility guard already records the original failure.
|
|
5314
|
+
}
|
|
5315
|
+
function queuedAttachmentMetadata(attachmentCount) {
|
|
5316
|
+
return attachmentCount > 0 ? { hasAttachments: true, attachmentCount } : {};
|
|
4417
5317
|
}
|
|
4418
5318
|
function userTextMessage(text) {
|
|
4419
5319
|
return { role: "user", content: text };
|
|
@@ -4469,6 +5369,55 @@ function activeEditableTreeTargetParentId(manager, targetId, activeLeafId) {
|
|
|
4469
5369
|
return undefined;
|
|
4470
5370
|
return parentId === null || typeof parentId === "string" ? parentId : undefined;
|
|
4471
5371
|
}
|
|
5372
|
+
/**
|
|
5373
|
+
* Pi edits custom messages from their parent. For a retained checkpoint, remap
|
|
5374
|
+
* that single leaf movement instead, before Pi rebuilds context/restores tools
|
|
5375
|
+
* and emits session_tree. Restore the manager methods before extension handlers
|
|
5376
|
+
* run: their appended history must not be overwritten by a post-event repair.
|
|
5377
|
+
* The caller holds the tree-navigation gate and excludes branch summarization.
|
|
5378
|
+
*/
|
|
5379
|
+
async function navigateTreeRetainingCustomMessage(manager, targetId, parentId, navigate) {
|
|
5380
|
+
if (parentId !== null && typeof parentId !== "string")
|
|
5381
|
+
throw new UserFacingError("session.checkpointParent", "This checkpoint has no valid history parent");
|
|
5382
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- Restore exact identities; calls below explicitly bind the manager.
|
|
5383
|
+
const { branch, resetLeaf } = manager;
|
|
5384
|
+
let pending = true;
|
|
5385
|
+
const restore = () => {
|
|
5386
|
+
if (!pending)
|
|
5387
|
+
return;
|
|
5388
|
+
pending = false;
|
|
5389
|
+
manager.branch = branch;
|
|
5390
|
+
manager.resetLeaf = resetLeaf;
|
|
5391
|
+
};
|
|
5392
|
+
try {
|
|
5393
|
+
manager.branch = (entryId) => {
|
|
5394
|
+
if (entryId === parentId) {
|
|
5395
|
+
restore();
|
|
5396
|
+
branch.call(manager, targetId);
|
|
5397
|
+
}
|
|
5398
|
+
else
|
|
5399
|
+
branch.call(manager, entryId);
|
|
5400
|
+
};
|
|
5401
|
+
manager.resetLeaf = () => {
|
|
5402
|
+
if (parentId === null) {
|
|
5403
|
+
restore();
|
|
5404
|
+
branch.call(manager, targetId);
|
|
5405
|
+
}
|
|
5406
|
+
else
|
|
5407
|
+
resetLeaf.call(manager);
|
|
5408
|
+
};
|
|
5409
|
+
const result = await navigate();
|
|
5410
|
+
if (result.cancelled)
|
|
5411
|
+
return result;
|
|
5412
|
+
// The retained notice is context, not text to restore into the editor.
|
|
5413
|
+
const { editorText, ...retainedResult } = result;
|
|
5414
|
+
void editorText;
|
|
5415
|
+
return retainedResult;
|
|
5416
|
+
}
|
|
5417
|
+
finally {
|
|
5418
|
+
restore();
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
4472
5421
|
function setSessionTreeLeaf(manager, leafId) {
|
|
4473
5422
|
if (leafId === null)
|
|
4474
5423
|
manager.resetLeaf();
|
|
@@ -4482,16 +5431,6 @@ function transcriptBranchIncludesEntry(entries, entryId) {
|
|
|
4482
5431
|
const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
|
|
4483
5432
|
/** custom entry type used to mark a child as created by spawn_subsession. */
|
|
4484
5433
|
const SUBSESSION_CHILD_LINK_CUSTOM_TYPE = "pi-web.subsession.spawned";
|
|
4485
|
-
/** customType marking a parent-facing subsession-completion notice. */
|
|
4486
|
-
const SUBSESSION_NOTIFICATION_CUSTOM_TYPE = "subsession.completion";
|
|
4487
|
-
const SUBSESSION_NOTIFICATION_MAX_OUTPUT_CHARS = 2000;
|
|
4488
|
-
/** Avoid duplicating a partial result in context when deliberate inspection can return the full output. */
|
|
4489
|
-
function formatSubsessionNotificationOutput(childSessionId, text) {
|
|
4490
|
-
if (text.length > SUBSESSION_NOTIFICATION_MAX_OUTPUT_CHARS) {
|
|
4491
|
-
return `Output from subsession ${childSessionId} was too long for this completion notice and was omitted. Call check_subsession with sessionId "${childSessionId}" to retrieve the final output.`;
|
|
4492
|
-
}
|
|
4493
|
-
return `--- SUBSESSION OUTPUT: ${childSessionId} ---\n${text === "" ? "(no output)" : text}`;
|
|
4494
|
-
}
|
|
4495
5434
|
/** Most recent assistant text from a history message list, or "" if none. */
|
|
4496
5435
|
function finalAssistantText(messages) {
|
|
4497
5436
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -4533,4 +5472,20 @@ async function resolveManagementAuditIdentity(cwd, context, resolveProjectId, lo
|
|
|
4533
5472
|
projectId,
|
|
4534
5473
|
};
|
|
4535
5474
|
}
|
|
5475
|
+
function transcriptReturnsFinalOutput(messages, view, finalText) {
|
|
5476
|
+
if (finalText === "")
|
|
5477
|
+
return false;
|
|
5478
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
5479
|
+
const text = finalAssistantText([messages[index]]);
|
|
5480
|
+
if (text === "")
|
|
5481
|
+
continue;
|
|
5482
|
+
const entry = view.entries.find((candidate) => candidate.index === index && candidate.role === "assistant");
|
|
5483
|
+
if (entry === undefined || text !== finalText)
|
|
5484
|
+
return false;
|
|
5485
|
+
const texts = entry.parts.filter((part) => part.kind === "text");
|
|
5486
|
+
return texts.every((part) => part.truncated === undefined)
|
|
5487
|
+
&& texts.map((part) => part.text).join("\n").trim() === finalText.trim();
|
|
5488
|
+
}
|
|
5489
|
+
return false;
|
|
5490
|
+
}
|
|
4536
5491
|
//# sourceMappingURL=piSessionService.js.map
|