@chainingintention/pi-web-cn 1.202610.1 → 1.202610.2
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 +60 -106
- package/dist/.plugins-ready +1 -0
- package/dist/cli.js +161 -45
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/index-CFhaGb5m.js +4357 -0
- package/dist/client/assets/vendor-editor-core-YWog6mAR.js +10 -0
- package/dist/client/assets/vendor-editor-languages-DOCw3vc9.js +28 -0
- package/dist/client/index.html +5 -5
- package/dist/config.js +30 -6
- package/dist/config.js.map +1 -1
- package/dist/docker/piWebDockerCommandPlan.js +13 -4
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
- package/dist/nativeServices/installedServiceDefinitions.js +23 -10
- package/dist/nativeServices/installedServiceDefinitions.js.map +1 -1
- package/dist/nativeServices/serviceAction.js +78 -20
- package/dist/nativeServices/serviceAction.js.map +1 -1
- package/dist/nativeServices/serviceDoctor.js +339 -67
- package/dist/nativeServices/serviceDoctor.js.map +1 -1
- package/dist/nativeServices/servicePlan.js +2 -2
- package/dist/nativeServices/servicePlan.js.map +1 -1
- package/dist/pi-packages/captains-log/README.md +11 -0
- package/dist/pi-packages/captains-log/dist/browser/channelProtocol.js +29 -0
- package/dist/pi-packages/captains-log/dist/browser/index.js +146 -0
- package/dist/pi-packages/captains-log/dist/browser/markdown.js +67 -0
- package/dist/pi-packages/captains-log/dist/browser/panel.js +151 -0
- package/dist/pi-packages/captains-log/dist/browser/protocol.js +26 -0
- package/dist/pi-packages/captains-log/dist/companion.js +126 -0
- package/dist/pi-packages/captains-log/dist/roundtrip.js +62 -0
- package/dist/pi-packages/captains-log/dist/server.js +210 -0
- package/dist/pi-packages/captains-log/dist/store.js +75 -0
- package/dist/pi-packages/captains-log/docs/usage.md +29 -0
- package/dist/pi-packages/captains-log/package.json +35 -0
- package/dist/pi-packages/captains-log/tsconfig.json +21 -0
- package/dist/pi-packages/captains-log/vite.config.mjs +28 -0
- package/dist/pi-packages/relays/pi-web-plugin.js +9 -9
- package/dist/pi-packages/relays/prompts/relay.md +1 -1
- package/dist/pi-packages/relays/relaysPanelElement.js +8 -8
- package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +21 -13
- package/dist/pi-web-plugins/files/browser/assets/files-icon-DZObYhxb.svg +3 -0
- package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +456 -0
- package/dist/pi-web-plugins/files/package.json +15 -0
- package/dist/pi-web-plugins/git/browser/git-panel.js +55 -32
- package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +2 -2
- package/dist/pi-web-plugins/git/git-backend.js +19 -26
- package/dist/pi-web-plugins/git/gitStatusCache.js +48 -0
- package/dist/pi-web-plugins/git/package.json +1 -0
- package/dist/pi-web-plugins/git/server-plugin.js +11 -10
- package/dist/pi-web-plugins/info/infoInternals.js +4 -1
- package/dist/pi-web-plugins/info/package.json +1 -1
- package/dist/pi-web-plugins/info/pi-web-plugin.js +8 -8
- package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +3590 -0
- package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +6 -0
- package/dist/pi-web-plugins/mermaid/package.json +13 -0
- package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +210 -0
- package/dist/pi-web-plugins/terminal/package.json +16 -0
- package/dist/pi-web-plugins/terminal/server-plugin.js +365 -0
- package/dist/pi-web-plugins/terminal/terminalService.js +435 -0
- package/dist/pi-web-plugins/updates/package.json +1 -1
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +69 -68
- package/dist/pi-web-plugins/workspace-tasks/package.json +1 -1
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +3 -3
- package/dist/piWebVersionReport.js +47 -24
- package/dist/piWebVersionReport.js.map +1 -1
- package/dist/plugin-api.d.ts +260 -25
- package/dist/plugin-api.js +1 -0
- package/dist/pluginRecoveryCli.js +3 -2
- package/dist/pluginRecoveryCli.js.map +1 -1
- package/dist/server/activity/workspaceActivityService.js +5 -5
- package/dist/server/activity/workspaceActivityService.js.map +1 -1
- package/dist/server/app.js +71 -44
- package/dist/server/app.js.map +1 -1
- package/dist/server/audit/managementAuditStore.js +1 -1
- package/dist/server/browserMessageProjection.js +0 -2
- package/dist/server/browserMessageProjection.js.map +1 -1
- package/dist/server/configRoutes.js +1 -2
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/knownPiPackages.js +12 -0
- package/dist/server/knownPiPackages.js.map +1 -0
- package/dist/server/machines/machineClient.js +2 -2
- package/dist/server/machines/machineClient.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +120 -21
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +80 -22
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineService.js +22 -0
- package/dist/server/machines/machineService.js.map +1 -1
- package/dist/server/managementEmbed.js +1 -1
- package/dist/server/managementEmbed.js.map +1 -1
- package/dist/server/managementPrivileges.js +7 -1
- package/dist/server/managementPrivileges.js.map +1 -1
- package/dist/server/managementTerminalProxyRoutes.js +124 -0
- package/dist/server/managementTerminalProxyRoutes.js.map +1 -0
- package/dist/server/normalAuth.js +5 -1
- package/dist/server/normalAuth.js.map +1 -1
- package/dist/server/notices/serverNoticeStore.js +50 -6
- package/dist/server/notices/serverNoticeStore.js.map +1 -1
- package/dist/server/piPackageService.js +5 -5
- package/dist/server/piPackageService.js.map +1 -1
- package/dist/server/piWebPluginCatalog.js +211 -34
- package/dist/server/piWebPluginCatalog.js.map +1 -1
- package/dist/server/piWebPluginLifecycle.js +29 -3
- package/dist/server/piWebPluginLifecycle.js.map +1 -1
- package/dist/server/piWebPluginService.js +176 -291
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebStatus.js +45 -55
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/pluginCallbackDrain.js +35 -0
- package/dist/server/pluginCallbackDrain.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +84 -0
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js +234 -0
- package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +63 -0
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -0
- package/dist/server/plugins/pluginBackendProxyRoutes.js +7 -4
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
- package/dist/server/plugins/pluginBackendRegistry.js +773 -0
- package/dist/server/plugins/pluginBackendRegistry.js.map +1 -0
- package/dist/server/plugins/serverPluginPiSessionEventsCapability.js +34 -0
- package/dist/server/plugins/serverPluginPiSessionEventsCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginPiSessionsCapability.js +185 -0
- package/dist/server/plugins/serverPluginPiSessionsCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +1049 -107
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
- package/dist/server/plugins/serverPluginWorkspacesCapability.js +114 -0
- package/dist/server/plugins/serverPluginWorkspacesCapability.js.map +1 -0
- package/dist/server/projectTrustRoutes.js +86 -0
- package/dist/server/projectTrustRoutes.js.map +1 -0
- package/dist/server/projects/projectLifecycleService.js +84 -0
- package/dist/server/projects/projectLifecycleService.js.map +1 -0
- package/dist/server/projects/projectService.js +2 -0
- package/dist/server/projects/projectService.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +35 -41
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessiond/agentHttpDispatcher.js +108 -0
- package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -0
- package/dist/server/sessiond/pluginBackendChannelRoutes.js +397 -0
- package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -0
- package/dist/server/sessiond/pluginBackendRoutes.js +19 -9
- package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
- package/dist/server/sessiond/projectMutationRoutes.js +38 -0
- package/dist/server/sessiond/projectMutationRoutes.js.map +1 -0
- package/dist/server/sessiond/sessionDaemonShutdown.js +36 -0
- package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -0
- package/dist/server/sessiond/sessionDaemonStartup.js +30 -4
- package/dist/server/sessiond/sessionDaemonStartup.js.map +1 -1
- package/dist/server/sessiond/sessionProxyRoutes.js +47 -15
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond/sessionServiceDependencies.js +4 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
- package/dist/server/sessiond/sessiondStateOwnership.js +174 -39
- package/dist/server/sessiond/sessiondStateOwnership.js.map +1 -1
- package/dist/server/sessiond/workspaceRemovalRoutes.js +1 -1
- package/dist/server/sessiond/workspaceRemovalRoutes.js.map +1 -1
- package/dist/server/sessiond.js +226 -56
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/askUserTool.js +0 -3
- package/dist/server/sessions/askUserTool.js.map +1 -1
- package/dist/server/sessions/attachmentService.js +0 -1
- package/dist/server/sessions/attachmentService.js.map +1 -1
- package/dist/server/sessions/authService.js +8 -1
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/builtinCommands.js +2 -2
- package/dist/server/sessions/builtinCommands.js.map +1 -1
- package/dist/server/sessions/clientSessionPreview.js +14 -0
- package/dist/server/sessions/clientSessionPreview.js.map +1 -0
- package/dist/server/sessions/dockerEnvironmentFacts.js +82 -9
- package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -1
- package/dist/server/sessions/managementAgentTools.js +11 -6
- package/dist/server/sessions/managementAgentTools.js.map +1 -1
- package/dist/server/sessions/managementPermissionSystem.js +16 -4
- package/dist/server/sessions/managementPermissionSystem.js.map +1 -1
- package/dist/server/sessions/messagePaging.js +2 -2
- package/dist/server/sessions/messagePaging.js.map +1 -1
- package/dist/server/sessions/modelCatalogRefresher.js +4 -3
- package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
- package/dist/server/sessions/oauthLoginFlowService.js +2 -5
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/pendingAskStore.js +0 -3
- package/dist/server/sessions/pendingAskStore.js.map +1 -1
- package/dist/server/sessions/piSessionEventConnections.js +65 -0
- package/dist/server/sessions/piSessionEventConnections.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +56 -3
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +774 -448
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/projectAuthService.js +29 -2
- package/dist/server/sessions/projectAuthService.js.map +1 -1
- package/dist/server/sessions/projectSettingsScope.js +12 -4
- package/dist/server/sessions/projectSettingsScope.js.map +1 -1
- package/dist/server/sessions/projectTrust.js +119 -0
- package/dist/server/sessions/projectTrust.js.map +1 -0
- package/dist/server/sessions/sessionActivityMarker.js +105 -0
- package/dist/server/sessions/sessionActivityMarker.js.map +1 -0
- package/dist/server/sessions/sessionArchiveMapping.js +2 -8
- package/dist/server/sessions/sessionArchiveMapping.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +12 -10
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionEnvironmentFacts.js +5 -4
- package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -1
- package/dist/server/sessions/sessionModelScope.js +42 -5
- package/dist/server/sessions/sessionModelScope.js.map +1 -1
- package/dist/server/sessions/sessionNameGenerator.js +6 -6
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +63 -15
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionTreeProjection.js +29 -29
- package/dist/server/sessions/sessionTreeProjection.js.map +1 -1
- package/dist/server/sessions/sessionUiEvents.js +6 -7
- package/dist/server/sessions/sessionUiEvents.js.map +1 -1
- package/dist/server/sessions/sessionUnreadStore.js +26 -1
- package/dist/server/sessions/sessionUnreadStore.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +3 -3
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +5 -5
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/sessions/spawnTargetResolver.js +1 -1
- package/dist/server/sessions/transcriptMessages.js +54 -0
- package/dist/server/sessions/transcriptMessages.js.map +1 -0
- package/dist/server/status/machineStatusService.js +3 -1
- package/dist/server/status/machineStatusService.js.map +1 -1
- package/dist/server/terminals/{terminalService.js → managementCommandRunService.js} +79 -107
- package/dist/server/terminals/managementCommandRunService.js.map +1 -0
- package/dist/server/terminals/managementTerminalRoutes.js +91 -0
- package/dist/server/terminals/managementTerminalRoutes.js.map +1 -0
- package/dist/server/terminals/requiredTerminalService.js +103 -0
- package/dist/server/terminals/requiredTerminalService.js.map +1 -0
- package/dist/server/webSocketBridge.js +339 -0
- package/dist/server/webSocketBridge.js.map +1 -1
- package/dist/server/workbench/gatewayIntegration.js +4 -1
- package/dist/server/workbench/gatewayIntegration.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +13 -12
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/fileContentService.js +9 -7
- package/dist/server/workspaces/fileContentService.js.map +1 -1
- package/dist/server/workspaces/fileOperationService.js +4 -3
- package/dist/server/workspaces/fileOperationService.js.map +1 -1
- package/dist/server/workspaces/projectWorkspaceCwds.js +0 -5
- package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -1
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +36 -12
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceCatalog.js +6 -4
- package/dist/server/workspaces/workspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceDeletionRoutes.js +5 -57
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
- package/dist/server/workspaces/workspaceProviderRegistry.js +75 -24
- package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
- package/dist/server/workspaces/workspaceRemovalService.js +25 -3
- package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
- package/dist/server-plugin-api.d.ts +274 -44
- package/dist/server-plugin-api.js +319 -2
- package/dist/server-plugin-api.js.map +1 -1
- package/dist/serverPluginRecovery.js +4 -0
- package/dist/serverPluginRecovery.js.map +1 -1
- package/dist/sessiond/activeAgentProfile.js +27 -2
- package/dist/sessiond/activeAgentProfile.js.map +1 -1
- package/dist/sessiond/config.js +4 -3
- package/dist/sessiond/config.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +29 -8
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/activeAgentProfile.js +2 -34
- package/dist/shared/activeAgentProfile.js.map +1 -1
- package/dist/shared/activity.js +0 -3
- package/dist/shared/activity.js.map +1 -1
- package/dist/shared/apiTypes.js +7 -2
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +5 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +49 -9
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/machinePluginIds.js +30 -7
- package/dist/shared/machinePluginIds.js.map +1 -1
- package/dist/shared/machineStatus.js +35 -1
- package/dist/shared/machineStatus.js.map +1 -1
- package/dist/shared/piWebStatusParsing.js +29 -0
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/dist/shared/pluginApiTypes.d.ts +24 -9
- package/dist/shared/pluginApiTypes.js +2 -0
- package/dist/shared/pluginApiTypes.js.map +1 -0
- package/dist/shared/pluginBackendProtocol.js +163 -2
- package/dist/shared/pluginBackendProtocol.js.map +1 -1
- package/dist/shared/pluginIds.js +10 -1
- package/dist/shared/pluginIds.js.map +1 -1
- package/dist/shared/requiredTerminalPlugin.js +6 -0
- package/dist/shared/requiredTerminalPlugin.js.map +1 -0
- package/dist/shared/serverNoticeContract.js +88 -0
- package/dist/shared/serverNoticeContract.js.map +1 -0
- package/dist/shared/sessionDefaults.js +41 -0
- package/dist/shared/sessionDefaults.js.map +1 -0
- package/dist/shared/workspaceFiles.js.map +1 -1
- package/docs/config.md +270 -323
- package/docs/plugins.md +171 -956
- package/examples/session-bridge-plugin/README.md +20 -0
- package/examples/session-bridge-plugin/docs/usage.md +54 -0
- package/examples/session-bridge-plugin/package.json +26 -0
- package/examples/session-bridge-plugin/src/browser/index.ts +87 -0
- package/examples/session-bridge-plugin/src/browser/protocol.ts +24 -0
- package/examples/session-bridge-plugin/src/companion.ts +65 -0
- package/examples/session-bridge-plugin/src/reviewRun.ts +39 -0
- package/examples/session-bridge-plugin/src/server.ts +85 -0
- package/examples/session-bridge-plugin/src/store.ts +80 -0
- package/examples/session-bridge-plugin/tsconfig.json +21 -0
- package/examples/workspace-provider-plugin/README.md +39 -0
- package/examples/workspace-provider-plugin/package.json +25 -0
- package/examples/workspace-provider-plugin/src/browser/index.ts +69 -0
- package/examples/workspace-provider-plugin/src/server.ts +68 -0
- package/examples/workspace-provider-plugin/tsconfig.json +21 -0
- package/package.json +22 -13
- package/skills/README.md +27 -2
- package/skills/relay-runner/SKILL.md +21 -13
- package/dist/client/assets/CodeViewer-CZHnKRwQ.js +0 -4
- package/dist/client/assets/TerminalPanel-B_EaRPHY.js +0 -187
- package/dist/client/assets/index-Bv1v-Jz7.js +0 -4717
- package/dist/client/assets/vendor-editor-core-vUi74DnD.js +0 -12
- package/dist/client/assets/vendor-editor-languages-DoaXUodB.js +0 -46
- package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +0 -1
- package/dist/client/assets/vendor-terminal-BrP-ENHg.css +0 -1
- package/dist/client/assets/vendor-terminal-D8k4UKM2.js +0 -35
- package/dist/server/git/gitService.js +0 -316
- package/dist/server/git/gitService.js.map +0 -1
- package/dist/server/gitRoutes.js +0 -23
- package/dist/server/gitRoutes.js.map +0 -1
- package/dist/server/terminalProxyRoutes.js +0 -181
- package/dist/server/terminalProxyRoutes.js.map +0 -1
- package/dist/server/terminals/terminalRoutes.js +0 -164
- package/dist/server/terminals/terminalRoutes.js.map +0 -1
- package/dist/server/terminals/terminalService.js.map +0 -1
- package/dist/server/terminals/terminalSize.js +0 -17
- package/dist/server/terminals/terminalSize.js.map +0 -1
- package/dist/server/workspaces/gitWorktreeDiscovery.js +0 -43
- package/dist/server/workspaces/gitWorktreeDiscovery.js.map +0 -1
- package/dist/server/workspaces/workspaceService.js +0 -52
- package/dist/server/workspaces/workspaceService.js.map +0 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { clientSessionFirstMessagePreview } from "./clientSessionPreview.js";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEventBus, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, ProjectTrustStore, hasTrustRequiringProjectResources, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
6
7
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
7
8
|
import { eventScopeFromManagementContext, managementContextKey, NORMAL_SESSION_EVENT_SCOPE } from "../realtime/sessionEventScope.js";
|
|
8
9
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
9
10
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
11
|
+
import { SessionActivityMarker } from "./sessionActivityMarker.js";
|
|
10
12
|
import { projectSessionTree } from "./sessionTreeProjection.js";
|
|
11
13
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
12
14
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
@@ -16,17 +18,20 @@ import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachm
|
|
|
16
18
|
import { loadEffectiveProjectAttachmentsConfig } from "../workspaces/projectPiWebConfig.js";
|
|
17
19
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
18
20
|
import { ASK_USER_ANSWERS_CUSTOM_TYPE, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
21
|
+
import { PiSessionEventConnections } from "./piSessionEventConnections.js";
|
|
22
|
+
import { resolveWebProjectTrusted } from "./projectTrust.js";
|
|
19
23
|
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
|
20
24
|
import { SessionPinStore } from "./sessionPinStore.js";
|
|
21
25
|
import { SessionActivityCoordinator } from "./sessionActivityCoordinator.js";
|
|
22
26
|
import { searchSessionContent } from "./sessionContentSearch.js";
|
|
23
|
-
import { createLocalOnlyModelRuntime } from "./authService.js";
|
|
27
|
+
import { createLocalOnlyModelRuntime, modelRuntimeAuthPath } from "./authService.js";
|
|
24
28
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
25
29
|
import { clearSessionFileParent, readSessionHeaderSummary } from "./sessionFileHeader.js";
|
|
26
30
|
import { isManagedAgentsFile, workbenchAccessHandle } from "../managementEmbed.js";
|
|
27
|
-
import { managementPrivilegeRequested, managementPrivileges } from "../managementPrivileges.js";
|
|
31
|
+
import { managementPrivilegeRequested, managementPrivileges, managementUsesNormalPermissions } from "../managementPrivileges.js";
|
|
28
32
|
import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS, piWebDataDir } from "../../config.js";
|
|
29
33
|
import { createScopedSettingsManager, resolveSettingsScopeDirectory } from "./projectSettingsScope.js";
|
|
34
|
+
import { parseSessionDefaults, parseSessionDefaultsUpdate } from "../../shared/sessionDefaults.js";
|
|
30
35
|
import { createAskUserToolDefinition } from "./askUserTool.js";
|
|
31
36
|
import { PendingAskStore, renderAskUserAnswersText } from "./pendingAskStore.js";
|
|
32
37
|
import { PendingExtensionDialogStore } from "./pendingExtensionDialogStore.js";
|
|
@@ -41,8 +46,9 @@ import { SessionNotificationStore } from "./sessionNotificationStore.js";
|
|
|
41
46
|
import { plainTextTheme } from "./plainTextTheme.js";
|
|
42
47
|
import { deduplicateBundledRelaySkills, ensureManagedRelaySkills, MANAGED_RELAY_SKILL_NAMES } from "./relaySkill.js";
|
|
43
48
|
import { SessionUnreadStore } from "./sessionUnreadStore.js";
|
|
44
|
-
import { archiveCandidateFromActiveSession, archiveCandidateFromArchivedRecord, archiveCandidateFromListEntry, archiveInputFromActiveSession, archiveInputFromCandidate, archiveInputFromListEntry, clientSessionFromArchivedRecord,
|
|
45
|
-
import {
|
|
49
|
+
import { archiveCandidateFromActiveSession, archiveCandidateFromArchivedRecord, archiveCandidateFromListEntry, archiveInputFromActiveSession, archiveInputFromCandidate, archiveInputFromListEntry, clientSessionFromArchivedRecord, } from "./sessionArchiveMapping.js";
|
|
50
|
+
import { getBoolean, getProperty, getString, toClientEvent } from "./sessionUiEvents.js";
|
|
51
|
+
import { annotateAssistantThinkingLevel, historyMessagesFromEntries } from "./transcriptMessages.js";
|
|
46
52
|
import { createWorkbenchToolDefinitions } from "../workbench/workbenchTools.js";
|
|
47
53
|
import { applyEnabledModelToggle, catalogWithEnabledFirst, modelScopeId, persistedEnabledModelPatterns, resolveEnabledModelIds, resolveSessionModelOptions, scopedModelsFromEnabledIds } from "./sessionModelScope.js";
|
|
48
54
|
const noopLogger = { info() { } };
|
|
@@ -65,24 +71,16 @@ const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
|
|
|
65
71
|
const MAX_PENDING_UNREAD_MUTATIONS = SESSION_UNREAD_LIMIT + 1;
|
|
66
72
|
const IDLE_SESSION_FILE_RESOLUTION_THROTTLE_MS = 30_000;
|
|
67
73
|
function auditVisibleAssistantMessage(message) {
|
|
68
|
-
if (!
|
|
74
|
+
if (!isRecord(message) || Array.isArray(message))
|
|
69
75
|
return message;
|
|
70
|
-
const
|
|
71
|
-
const content = record["content"];
|
|
76
|
+
const content = message["content"];
|
|
72
77
|
if (!Array.isArray(content))
|
|
73
78
|
return message;
|
|
74
79
|
return {
|
|
75
|
-
...
|
|
76
|
-
content: content.filter(part => (
|
|
77
|
-
|| part === null
|
|
78
|
-
|| Array.isArray(part)
|
|
79
|
-
|| !isAuditRecord(part)
|
|
80
|
-
|| part["type"] !== "thinking")),
|
|
80
|
+
...message,
|
|
81
|
+
content: content.filter(part => !isRecord(part) || Array.isArray(part) || part["type"] !== "thinking"),
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
|
-
function isAuditRecord(value) {
|
|
84
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
85
|
-
}
|
|
86
84
|
function noop() {
|
|
87
85
|
// Intentionally empty default unsubscribe callback.
|
|
88
86
|
}
|
|
@@ -143,9 +141,9 @@ function sessionTreeNavigationOptions(request) {
|
|
|
143
141
|
case "custom": {
|
|
144
142
|
const customInstructions = request.summary.instructions.trim();
|
|
145
143
|
if (customInstructions === "")
|
|
146
|
-
throw new Error("
|
|
144
|
+
throw new Error("自定义分支汇总要求不能为空");
|
|
147
145
|
if (customInstructions.length > SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH) {
|
|
148
|
-
throw new Error(
|
|
146
|
+
throw new Error(`自定义分支汇总要求不能超过 ${String(SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH)} 个字符`);
|
|
149
147
|
}
|
|
150
148
|
return { summarize: true, customInstructions };
|
|
151
149
|
}
|
|
@@ -381,67 +379,108 @@ export function piWebResourceLoaderOptions(appendSystemPromptSections) {
|
|
|
381
379
|
return undefined;
|
|
382
380
|
return { appendSystemPromptOverride: (base) => [...base, ...appendSystemPromptSections] };
|
|
383
381
|
}
|
|
384
|
-
function
|
|
385
|
-
|
|
382
|
+
async function createNormalPermissionSessionServices(options) {
|
|
383
|
+
const { cwd, agentDir, settingsManager, projectTrustRequiring } = options;
|
|
384
|
+
const projectTrustDiagnostics = [];
|
|
385
|
+
const services = await createAgentSessionServices({
|
|
386
|
+
cwd,
|
|
387
|
+
agentDir,
|
|
388
|
+
modelRuntime: options.modelRuntime,
|
|
389
|
+
settingsManager,
|
|
390
|
+
resourceLoaderOptions: {
|
|
391
|
+
...piWebResourceLoaderOptions(options.appendSystemPromptSections),
|
|
392
|
+
eventBus: options.eventBus,
|
|
393
|
+
...(options.agentsFilesOverride === undefined ? {} : { agentsFilesOverride: options.agentsFilesOverride }),
|
|
394
|
+
skillsOverride: (base) => deduplicateBundledRelaySkills(cwd, base),
|
|
395
|
+
},
|
|
396
|
+
...(projectTrustRequiring ? {
|
|
397
|
+
resourceLoaderReloadOptions: {
|
|
398
|
+
resolveProjectTrust: async ({ extensionsResult }) => resolveWebProjectTrusted({
|
|
399
|
+
cwd,
|
|
400
|
+
trustStore: new ProjectTrustStore(agentDir),
|
|
401
|
+
settingsManager,
|
|
402
|
+
extensionsResult,
|
|
403
|
+
onExtensionError: (message) => projectTrustDiagnostics.push({ type: "warning", message }),
|
|
404
|
+
}),
|
|
405
|
+
},
|
|
406
|
+
} : {}),
|
|
407
|
+
});
|
|
408
|
+
return { services, projectTrustDiagnostics };
|
|
409
|
+
}
|
|
410
|
+
function createDefaultRuntimeFactory(sessionEvents, normalModelRuntimeForCwd, managementModelRuntime, resolveSettingsScope, sessionManagers, spawn, subsessions, askUser, workbench, logger = noopLogger, managementAudit, managementProjectIdForCwd, appendSystemPromptSections = [], managementAllowPrivileged = false) {
|
|
411
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
386
412
|
const scopedModelRuntime = managementContext === undefined ? await normalModelRuntimeForCwd(cwd) : await managementModelRuntime();
|
|
387
413
|
if (managementContext !== undefined) {
|
|
388
|
-
return createManagementRuntimeFactory(scopedModelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd, managementAllowPrivileged, appendSystemPromptSections)({
|
|
414
|
+
return createManagementRuntimeFactory(scopedModelRuntime, resolveSettingsScope, sessionManagers, spawn, subsessions, askUser, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd, managementAllowPrivileged, appendSystemPromptSections)({
|
|
389
415
|
cwd,
|
|
390
416
|
agentDir,
|
|
391
417
|
sessionManager,
|
|
392
|
-
delegationToolsEnabled,
|
|
418
|
+
...(delegationToolsEnabled === undefined ? {} : { delegationToolsEnabled }),
|
|
393
419
|
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
394
420
|
...(initialModel === undefined ? {} : { initialModel }),
|
|
395
421
|
...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
|
|
396
422
|
});
|
|
397
423
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
424
|
+
// Resource loaders may read process.env; wait for managed overrides to be restored.
|
|
425
|
+
return withRuntimeCreationEnvironment({}, async () => {
|
|
426
|
+
const projectTrustRequiring = hasTrustRequiringProjectResources(cwd);
|
|
427
|
+
const settingsManager = await createScopedSettingsManager({
|
|
428
|
+
cwd,
|
|
429
|
+
scopeDirectory: await resolveSettingsScope(cwd, "normal"),
|
|
430
|
+
globalAgentDir: agentDir,
|
|
431
|
+
mode: "normal",
|
|
432
|
+
projectTrusted: !projectTrustRequiring,
|
|
433
|
+
});
|
|
434
|
+
// Only normal runtimes register a host connection; privileged management
|
|
435
|
+
// packages keep their own event bus without exposing a normal-mode session.
|
|
436
|
+
const eventBus = createEventBus();
|
|
437
|
+
const { services, projectTrustDiagnostics } = await createNormalPermissionSessionServices({
|
|
438
|
+
cwd,
|
|
439
|
+
agentDir,
|
|
440
|
+
modelRuntime: scopedModelRuntime,
|
|
441
|
+
settingsManager,
|
|
442
|
+
projectTrustRequiring,
|
|
443
|
+
appendSystemPromptSections,
|
|
444
|
+
eventBus,
|
|
445
|
+
});
|
|
446
|
+
const modelOptions = await resolveSessionModelOptions({
|
|
447
|
+
services,
|
|
448
|
+
hasExistingSession: sessionManager.buildSessionContext().messages.length > 0,
|
|
449
|
+
...(initialModel === undefined ? {} : { initialModel }),
|
|
450
|
+
...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
|
|
451
|
+
});
|
|
452
|
+
services.diagnostics.push(...modelOptions.diagnostics);
|
|
453
|
+
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
454
|
+
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
455
|
+
const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions, askUser);
|
|
456
|
+
const result = await createAgentSessionFromServices({
|
|
457
|
+
services,
|
|
458
|
+
sessionManager,
|
|
459
|
+
customTools,
|
|
460
|
+
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
461
|
+
...(modelOptions.model === undefined ? {} : { model: modelOptions.model }),
|
|
462
|
+
...(modelOptions.thinkingLevel === undefined ? {} : { thinkingLevel: modelOptions.thinkingLevel }),
|
|
463
|
+
...(modelOptions.scopedModels.length === 0 ? {} : { scopedModels: modelOptions.scopedModels }),
|
|
464
|
+
});
|
|
465
|
+
sessionEvents.register(result.session, eventBus);
|
|
466
|
+
const reload = result.session.reload.bind(result.session);
|
|
467
|
+
result.session.reload = (options) => withRuntimeCreationEnvironment({}, () => reload(options));
|
|
468
|
+
return { ...result, services, diagnostics: [...projectTrustDiagnostics, ...services.diagnostics] };
|
|
431
469
|
});
|
|
432
|
-
return { ...result, services, diagnostics: services.diagnostics };
|
|
433
470
|
};
|
|
434
471
|
}
|
|
435
|
-
function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd, managementAllowPrivileged = false, appendSystemPromptSections = []) {
|
|
436
|
-
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled
|
|
472
|
+
function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sessionManagers, spawn, subsessions, askUser, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd, managementAllowPrivileged = false, appendSystemPromptSections = []) {
|
|
473
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
474
|
+
const normalPermissions = managementUsesNormalPermissions(managementContext, { allowPrivileged: managementAllowPrivileged });
|
|
437
475
|
const privileges = managementPrivileges(managementContext, { allowPrivileged: managementAllowPrivileged });
|
|
438
|
-
const
|
|
439
|
-
|
|
476
|
+
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
477
|
+
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
478
|
+
if (!normalPermissions)
|
|
440
479
|
await ensureManagedRelaySkills(cwd);
|
|
441
480
|
const auditIdentity = await resolveManagementAuditIdentity(cwd, managementContext, managementProjectIdForCwd, logger);
|
|
442
481
|
const accessHandle = workbenchAccessHandle(managementContext);
|
|
443
|
-
const state = workbench === undefined ? undefined : await workbench.accessStates.prepare(accessHandle, workbench.client);
|
|
444
|
-
const receipt = state === undefined
|
|
482
|
+
const state = normalPermissions || workbench === undefined ? undefined : await workbench.accessStates.prepare(accessHandle, workbench.client);
|
|
483
|
+
const receipt = state === undefined ? undefined : await workbench?.skills.synchronize(cwd, state, {
|
|
445
484
|
...(managementAudit === undefined ? {} : { recorder: managementAudit }),
|
|
446
485
|
...auditIdentity,
|
|
447
486
|
sessionId: sessionManager.getSessionId(),
|
|
@@ -467,13 +506,18 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
|
|
|
467
506
|
const scopedSubsessions = subsessions === undefined
|
|
468
507
|
? undefined
|
|
469
508
|
: {
|
|
470
|
-
...subsessions,
|
|
471
509
|
spawn: (input) => subsessions.spawn({ ...input, managementContext }),
|
|
510
|
+
list: (parentSessionId, parentSessionFile) => subsessions.list(parentSessionId, parentSessionFile, managementContext),
|
|
511
|
+
check: (parentSessionId, sessionId, parentSessionFile) => subsessions.check(parentSessionId, sessionId, parentSessionFile, managementContext),
|
|
512
|
+
read: (parentSessionId, sessionId, query, parentSessionFile) => subsessions.read(parentSessionId, sessionId, query, parentSessionFile, managementContext),
|
|
472
513
|
};
|
|
514
|
+
const scopedAskUser = askUser === undefined ? undefined : {
|
|
515
|
+
open: (input) => askUser.open({ ...input, managementContext }),
|
|
516
|
+
};
|
|
473
517
|
const controlledManagementTools = [
|
|
474
|
-
...(
|
|
475
|
-
...(
|
|
476
|
-
...(
|
|
518
|
+
...(resolvedDelegationToolsEnabled && scopedSpawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn: scopedSpawn }))] : []),
|
|
519
|
+
...(resolvedDelegationToolsEnabled && scopedSubsessions !== undefined ? createSubsessionToolDefinitions(cwd, scopedSubsessions).map((tool) => defineTool(tool)) : []),
|
|
520
|
+
...(scopedAskUser === undefined ? [] : [createAskUserToolDefinition(scopedAskUser)]),
|
|
477
521
|
...workbenchTools,
|
|
478
522
|
];
|
|
479
523
|
const controlledManagementToolNames = controlledManagementTools.map((tool) => tool.name);
|
|
@@ -485,28 +529,36 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
|
|
|
485
529
|
}, "management privileged grant ignored because managementEmbed.allowPrivileged is disabled");
|
|
486
530
|
}
|
|
487
531
|
const extraToolNames = [...controlledManagementToolNames, ...privilegedManagementToolNames(privileges)];
|
|
488
|
-
const policyAgentDir =
|
|
489
|
-
|
|
532
|
+
const policyAgentDir = normalPermissions ? undefined : await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext, extraToolNames, privileges);
|
|
533
|
+
const resourceEnvironment = normalPermissions ? {} : { [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir };
|
|
534
|
+
return withRuntimeCreationEnvironment(resourceEnvironment, async () => {
|
|
490
535
|
const settingsManager = await createScopedSettingsManager({
|
|
491
536
|
cwd,
|
|
492
537
|
scopeDirectory: await resolveSettingsScope(cwd, "management"),
|
|
493
538
|
globalAgentDir: agentDir,
|
|
494
539
|
mode: "management",
|
|
540
|
+
// Signed management entry authorizes the project without changing ordinary trust decisions.
|
|
541
|
+
projectTrusted: true,
|
|
495
542
|
});
|
|
496
|
-
const services = await
|
|
543
|
+
const { services, projectTrustDiagnostics } = normalPermissions ? await createNormalPermissionSessionServices({
|
|
497
544
|
cwd,
|
|
498
545
|
agentDir,
|
|
499
546
|
modelRuntime,
|
|
500
547
|
settingsManager,
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
548
|
+
projectTrustRequiring: false,
|
|
549
|
+
appendSystemPromptSections,
|
|
550
|
+
eventBus: createEventBus(),
|
|
551
|
+
agentsFilesOverride: (base) => filterPrivilegedManagementContextFiles(cwd, managementContext, base),
|
|
552
|
+
}) : { services: await createAgentSessionServices({
|
|
553
|
+
cwd,
|
|
554
|
+
agentDir,
|
|
555
|
+
modelRuntime,
|
|
556
|
+
settingsManager,
|
|
557
|
+
resourceLoaderOptions: {
|
|
558
|
+
agentsFilesOverride: (base) => filterManagedGlobalContextFiles(cwd, agentDir, base),
|
|
559
|
+
skillsOverride: (base) => receipt === undefined ? filterManagedProjectSkills(cwd, base) : filterManagedWorkbenchSkills(cwd, base, receipt),
|
|
560
|
+
},
|
|
561
|
+
}), projectTrustDiagnostics: [] };
|
|
510
562
|
const modelOptions = await resolveSessionModelOptions({
|
|
511
563
|
services,
|
|
512
564
|
hasExistingSession: sessionManager.buildSessionContext().messages.length > 0,
|
|
@@ -514,12 +566,8 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
|
|
|
514
566
|
...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
|
|
515
567
|
});
|
|
516
568
|
services.diagnostics.push(...modelOptions.diagnostics);
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
...createPiWebCustomToolDefinitions(cwd, delegationToolsEnabled, scopedSpawn, scopedSubsessions, askUser),
|
|
520
|
-
...workbenchTools,
|
|
521
|
-
] : [
|
|
522
|
-
...managedAgentToolDefinitions(cwd, managedToolOptions),
|
|
569
|
+
const customTools = normalPermissions ? createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, scopedSpawn, scopedSubsessions, scopedAskUser) : [
|
|
570
|
+
...managedAgentToolDefinitions(cwd, createManagedAgentToolOptions(cwd)),
|
|
523
571
|
createManagedPythonToolDefinition(cwd, managementContext, { network: privileges.network }),
|
|
524
572
|
createManagedBashToolDefinition(cwd, managementContext, { network: privileges.network, hostFilesystem: privileges.bash }),
|
|
525
573
|
...controlledManagementTools,
|
|
@@ -529,13 +577,16 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
|
|
|
529
577
|
sessionManager,
|
|
530
578
|
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
531
579
|
customTools,
|
|
532
|
-
...(
|
|
580
|
+
...(normalPermissions ? {} : { tools: managementAgentToolNames(managementContext, extraToolNames, privileges) }),
|
|
533
581
|
...(modelOptions.model === undefined ? {} : { model: modelOptions.model }),
|
|
534
582
|
...(modelOptions.thinkingLevel === undefined ? {} : { thinkingLevel: modelOptions.thinkingLevel }),
|
|
535
583
|
...(modelOptions.scopedModels.length === 0 ? {} : { scopedModels: modelOptions.scopedModels }),
|
|
536
584
|
};
|
|
537
585
|
const result = await createAgentSessionFromServices(options);
|
|
538
|
-
|
|
586
|
+
// SDK reload re-executes extensions, so it needs the same scoped environment as startup.
|
|
587
|
+
const reload = result.session.reload.bind(result.session);
|
|
588
|
+
result.session.reload = (options) => withRuntimeCreationEnvironment(resourceEnvironment, () => reload(options));
|
|
589
|
+
return { ...result, services, diagnostics: [...projectTrustDiagnostics, ...services.diagnostics] };
|
|
539
590
|
});
|
|
540
591
|
};
|
|
541
592
|
}
|
|
@@ -591,6 +642,7 @@ export class PiSessionService {
|
|
|
591
642
|
this.activities = new SessionActivityCoordinator();
|
|
592
643
|
/** Runtime-identity gate held while Pi may await abandoned-branch summarization. */
|
|
593
644
|
this.treeNavigations = new WeakSet();
|
|
645
|
+
this.unpersistedTreeBranchLeaves = new WeakMap();
|
|
594
646
|
/** Counts async operations that may append an entry before they settle. */
|
|
595
647
|
this.sessionEntryMutationCounts = new WeakMap();
|
|
596
648
|
/** Serializes enabled-model read/modify/write operations. */
|
|
@@ -626,6 +678,7 @@ export class PiSessionService {
|
|
|
626
678
|
this.scopedNotificationStores = new Map();
|
|
627
679
|
this.scopedUnreadStores = new Map();
|
|
628
680
|
this.sessionEventScopes = new WeakMap();
|
|
681
|
+
this.sessionManagementContexts = new WeakMap();
|
|
629
682
|
this.notificationGenerationBySession = new WeakMap();
|
|
630
683
|
this.idleSessionFileResolutions = new WeakMap();
|
|
631
684
|
/** The parked extension Promise resolvers behind the store's open dialogs. */
|
|
@@ -659,6 +712,10 @@ export class PiSessionService {
|
|
|
659
712
|
this.managementAudit = deps.managementAudit;
|
|
660
713
|
this.managementProjectIdForCwd = deps.managementProjectIdForCwd;
|
|
661
714
|
this.now = deps.now ?? (() => new Date());
|
|
715
|
+
this.activityMarker = new SessionActivityMarker({
|
|
716
|
+
now: () => this.now().getTime(),
|
|
717
|
+
onError: (error) => { this.logger.info({ err: error }, "Could not update advisory session activity marker"); },
|
|
718
|
+
});
|
|
662
719
|
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
663
720
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
664
721
|
this.onUnreadChanged = deps.onUnreadChanged;
|
|
@@ -672,18 +729,19 @@ export class PiSessionService {
|
|
|
672
729
|
// Subsessions are gated behind their own flag, and they
|
|
673
730
|
// also require the spawn capability (they share its project-scope resolver).
|
|
674
731
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
675
|
-
this.
|
|
732
|
+
this.sessionEvents = deps.sessionEvents ?? new PiSessionEventConnections();
|
|
733
|
+
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 : {
|
|
676
734
|
spawn: (input) => this.spawnSubsession(input),
|
|
677
|
-
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
678
|
-
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
679
|
-
read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
|
|
680
|
-
}, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined, deps.workbench, this.logger, this.managementAudit, this.managementProjectIdForCwd, deps.appendSystemPromptSections ?? [], deps.managementAllowPrivileged === true);
|
|
735
|
+
list: (parentSessionId, parentSessionFile, managementContext) => this.listSubsessions(parentSessionId, parentSessionFile, managementContext),
|
|
736
|
+
check: (parentSessionId, sessionId, parentSessionFile, managementContext) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile, managementContext),
|
|
737
|
+
read: (parentSessionId, sessionId, query, parentSessionFile, managementContext) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile, managementContext),
|
|
738
|
+
}, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input, input.managementContext) } : undefined, deps.workbench, this.logger, this.managementAudit, this.managementProjectIdForCwd, deps.appendSystemPromptSections ?? [], deps.managementAllowPrivileged === true);
|
|
681
739
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
682
740
|
this.workspaceActivity = deps.workspaceActivity;
|
|
683
741
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
684
742
|
this.commandService = new SessionCommandService((sessionId, eventScope) => this.getActiveForCommand(sessionId, eventScope), (sessionId, text, eventScope) => this.submitCommandPrompt(sessionId, text, eventScope), events, {
|
|
685
743
|
onCompactionStart: (session) => {
|
|
686
|
-
this.beginSessionEntryMutation(session, "
|
|
744
|
+
this.beginSessionEntryMutation(session, "压缩会话");
|
|
687
745
|
this.publishActivity(session, "正在压缩上下文", "active");
|
|
688
746
|
this.publishStatus(session);
|
|
689
747
|
},
|
|
@@ -702,7 +760,7 @@ export class PiSessionService {
|
|
|
702
760
|
isTreeNavigationActive: (session) => this.treeNavigations.has(session),
|
|
703
761
|
runSessionReplacement: (session, operation) => {
|
|
704
762
|
const runtime = this.activeRuntimeForSession(session);
|
|
705
|
-
return this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, ...(runtime === undefined ? {} : { runtime }) }], "
|
|
763
|
+
return this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, ...(runtime === undefined ? {} : { runtime }) }], "替换会话前请先停止当前会话活动", operation);
|
|
706
764
|
},
|
|
707
765
|
}, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
|
|
708
766
|
}
|
|
@@ -750,7 +808,6 @@ export class PiSessionService {
|
|
|
750
808
|
await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" }, scope);
|
|
751
809
|
readyDeleteRecords.push(record);
|
|
752
810
|
}
|
|
753
|
-
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
754
811
|
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
755
812
|
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
756
813
|
await this.forgetUnreadSessions(deleteRecords, scope);
|
|
@@ -777,7 +834,7 @@ export class PiSessionService {
|
|
|
777
834
|
const activeSessions = Array.from(new Set(this.active.values()));
|
|
778
835
|
for (const active of activeSessions) {
|
|
779
836
|
this.forgetUnreadActivity(active.runtime.session);
|
|
780
|
-
this.pendingAskStore.forgetSession(active.runtime.session
|
|
837
|
+
this.pendingAskStore.forgetSession(this.dialogSessionKey(active.runtime.session));
|
|
781
838
|
this.endSessionExtensionDialogs(active.runtime.session);
|
|
782
839
|
}
|
|
783
840
|
this.active.clear();
|
|
@@ -796,10 +853,15 @@ export class PiSessionService {
|
|
|
796
853
|
store.clearAll("service-dispose");
|
|
797
854
|
await Promise.all(activeSessions.map(async (active) => {
|
|
798
855
|
active.unsubscribe();
|
|
856
|
+
active.runtime.setRebindSession(undefined);
|
|
799
857
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
858
|
+
try {
|
|
859
|
+
await this.abortSessionOperations(active.runtime.session);
|
|
860
|
+
}
|
|
861
|
+
finally {
|
|
862
|
+
await active.runtime.dispose();
|
|
863
|
+
}
|
|
864
|
+
})).finally(() => this.activityMarker.dispose());
|
|
803
865
|
await this.publishUnreadMutations([]);
|
|
804
866
|
}
|
|
805
867
|
async list(cwd, managementContext) {
|
|
@@ -867,9 +929,9 @@ export class PiSessionService {
|
|
|
867
929
|
await this.publishUnreadMutations(this.unreadForScope(eventScopeFromManagementContext(managementContext)).reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
|
|
868
930
|
}
|
|
869
931
|
const archivedSessions = archivedForCwd
|
|
870
|
-
.sort(compareArchivedRecords)
|
|
871
932
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
872
|
-
.filter(isDefined)
|
|
933
|
+
.filter(isDefined)
|
|
934
|
+
.sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified));
|
|
873
935
|
return {
|
|
874
936
|
sessions: [...unarchivedSessions, ...archivedSessions],
|
|
875
937
|
entriesById: new Map(sessions.map((session) => [session.id, session])),
|
|
@@ -881,15 +943,42 @@ export class PiSessionService {
|
|
|
881
943
|
await this.normalModelRuntimeForCwd(cwd);
|
|
882
944
|
return this.startSession(cwd, options);
|
|
883
945
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
946
|
+
/** Publish a host-owned normal-mode conversation without submitting an initial prompt. */
|
|
947
|
+
async createHostedSession(cwd, signal) {
|
|
948
|
+
const created = await this.startManagedSession(cwd, signal);
|
|
949
|
+
return Object.freeze({ id: created.id });
|
|
950
|
+
}
|
|
951
|
+
/** Atomically start and prompt one visible normal-mode, non-delegating session. */
|
|
952
|
+
async startOneShotRun(cwd, text, signal) {
|
|
953
|
+
const promptText = requirePromptText(text);
|
|
954
|
+
return this.startManagedSession(cwd, signal, promptText);
|
|
955
|
+
}
|
|
956
|
+
async startManagedSession(cwd, signal, promptText) {
|
|
957
|
+
signal.throwIfAborted();
|
|
958
|
+
await this.normalModelRuntimeForCwd(cwd);
|
|
959
|
+
const active = await this.create(this.sessionManager.create(cwd), cwd, { startupIntent: "create", creationProvenance: "host-one-shot", startupSignal: signal });
|
|
960
|
+
const { session } = active.runtime;
|
|
961
|
+
try {
|
|
962
|
+
signal.throwIfAborted();
|
|
963
|
+
if (promptText !== undefined)
|
|
964
|
+
this.maybeGenerateSessionName(session, promptText);
|
|
965
|
+
const created = this.announceCreatedSession(active, cwd);
|
|
966
|
+
// Publication transfers lifetime ownership to normal hosting. Plugin
|
|
967
|
+
// cancellation after this point must not abort this or later user work.
|
|
968
|
+
const completion = promptText === undefined ? Promise.resolve() : this.submitInitialRunPrompt(session, promptText);
|
|
969
|
+
return Object.freeze({ id: created.id, completion });
|
|
970
|
+
}
|
|
971
|
+
catch (error) {
|
|
972
|
+
try {
|
|
973
|
+
await this.closeActive(session.sessionId);
|
|
974
|
+
}
|
|
975
|
+
catch (cleanupError) {
|
|
976
|
+
throw new AggregateError([error, cleanupError], `托管会话 ${session.sessionId} 启动失败后清理失败`, { cause: cleanupError });
|
|
977
|
+
}
|
|
978
|
+
throw error;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
announceCreatedSession(active, cwd, parentSession) {
|
|
893
982
|
const { session } = active.runtime;
|
|
894
983
|
const created = {
|
|
895
984
|
id: session.sessionId,
|
|
@@ -902,13 +991,22 @@ export class PiSessionService {
|
|
|
902
991
|
firstMessage: "",
|
|
903
992
|
// Include the parent so listeners can nest the new session in the tree
|
|
904
993
|
// immediately, instead of showing it flat until the next reload.
|
|
905
|
-
...(
|
|
994
|
+
...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
|
|
906
995
|
};
|
|
907
|
-
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
908
|
-
// session to their list without a manual reload.
|
|
909
996
|
this.events.publishGlobal({ type: "session.created", session: created }, active.eventScope);
|
|
910
997
|
return created;
|
|
911
998
|
}
|
|
999
|
+
async startSession(cwd, options) {
|
|
1000
|
+
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
|
|
1001
|
+
...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
|
|
1002
|
+
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
1003
|
+
...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
|
|
1004
|
+
...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
|
|
1005
|
+
...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
|
|
1006
|
+
startupIntent: "create",
|
|
1007
|
+
});
|
|
1008
|
+
return this.announceCreatedSession(active, cwd, options.parentSession);
|
|
1009
|
+
}
|
|
912
1010
|
/**
|
|
913
1011
|
* Start a new session on behalf of a LLM and deliver an initial prompt to it.
|
|
914
1012
|
* The target cwd is constrained to a workspace of the same registered project
|
|
@@ -973,9 +1071,9 @@ export class PiSessionService {
|
|
|
973
1071
|
...(parentSessionFile === undefined ? {} : { parentSessionFile }),
|
|
974
1072
|
cwd: decision.cwd,
|
|
975
1073
|
};
|
|
976
|
-
await this.registerVerifiedSubsession(link,
|
|
977
|
-
this.persistSubsessionLink(
|
|
978
|
-
this.persistSubsessionChildMarker(
|
|
1074
|
+
const scopedLink = await this.registerVerifiedSubsession(link, input.managementContext);
|
|
1075
|
+
this.persistSubsessionLink(scopedLink);
|
|
1076
|
+
this.persistSubsessionChildMarker(scopedLink);
|
|
979
1077
|
await this.prompt(created.id, input.prompt, undefined, undefined, {
|
|
980
1078
|
...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
|
|
981
1079
|
});
|
|
@@ -1051,6 +1149,23 @@ export class PiSessionService {
|
|
|
1051
1149
|
this.modelScopeMutationQueue = queued.then(() => undefined, () => undefined);
|
|
1052
1150
|
return queued;
|
|
1053
1151
|
}
|
|
1152
|
+
reloadModelScope(session) {
|
|
1153
|
+
return this.runModelScopeMutation(async () => {
|
|
1154
|
+
const context = this.modelScopeContext(session);
|
|
1155
|
+
await this.modelScopeInitializations.get(context.key);
|
|
1156
|
+
const enabledIds = await resolveEnabledModelIds({
|
|
1157
|
+
settingsManager: session.settingsManager,
|
|
1158
|
+
// Runtime-backed sessions hold the SDK ModelRuntime behind the narrow test seam.
|
|
1159
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
1160
|
+
modelRuntime: session.modelRuntime,
|
|
1161
|
+
scopedModels: [],
|
|
1162
|
+
});
|
|
1163
|
+
const revision = ++this.modelScopeRevision;
|
|
1164
|
+
this.modelScopeStates.set(context.key, { enabledIds, revision });
|
|
1165
|
+
await this.synchronizeSessionModelScope(session, session.modelRuntime.getAvailableSnapshot());
|
|
1166
|
+
this.events.publishGlobal({ type: "models.changed", revision }, this.eventScopeForSession(session));
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1054
1169
|
applyEnabledModelScope(session, available, enabledIds) {
|
|
1055
1170
|
const context = this.modelScopeContext(session);
|
|
1056
1171
|
const availableIds = available.map(modelScopeId);
|
|
@@ -1093,14 +1208,16 @@ export class PiSessionService {
|
|
|
1093
1208
|
* agent loop reports to the model as an error tool result.
|
|
1094
1209
|
*/
|
|
1095
1210
|
// 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.
|
|
1096
|
-
async openAsk(input) {
|
|
1097
|
-
const
|
|
1211
|
+
async openAsk(input, managementContext) {
|
|
1212
|
+
const scope = eventScopeFromManagementContext(managementContext);
|
|
1213
|
+
const sessionKey = scope === NORMAL_SESSION_EVENT_SCOPE ? input.sessionId : activeSessionKey(input.sessionId, scope);
|
|
1214
|
+
const result = this.pendingAskStore.open({ ...input, sessionId: sessionKey });
|
|
1098
1215
|
// A supersede closes the earlier ask, so the browsers watching it must hear
|
|
1099
1216
|
// that before they hear about its replacement.
|
|
1100
1217
|
if (result.superseded !== undefined)
|
|
1101
|
-
this.publishAskClosed(input.sessionId, result.superseded);
|
|
1102
|
-
this.events.publish(input.sessionId, { type: "ask.opened", ask: result.ask });
|
|
1103
|
-
this.publishStatusForSessionId(input.sessionId);
|
|
1218
|
+
this.publishAskClosed(input.sessionId, result.superseded, scope);
|
|
1219
|
+
this.events.publish(input.sessionId, { type: "ask.opened", ask: result.ask }, scope);
|
|
1220
|
+
this.publishStatusForSessionId(input.sessionId, scope);
|
|
1104
1221
|
return result;
|
|
1105
1222
|
}
|
|
1106
1223
|
/**
|
|
@@ -1116,8 +1233,8 @@ export class PiSessionService {
|
|
|
1116
1233
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1117
1234
|
// Checked before the store closes the ask so a refused delivery cannot
|
|
1118
1235
|
// discard answers the user already submitted.
|
|
1119
|
-
this.assertTreeNavigationInactive(session, "
|
|
1120
|
-
return this.closeAsk(session, this.pendingAskStore.submit(session
|
|
1236
|
+
this.assertTreeNavigationInactive(session, "回答问题");
|
|
1237
|
+
return this.closeAsk(session, this.pendingAskStore.submit(this.dialogSessionKey(session), askId, submission));
|
|
1121
1238
|
}
|
|
1122
1239
|
/**
|
|
1123
1240
|
* Close the open ask without answers. The model is still told, naming every
|
|
@@ -1127,8 +1244,8 @@ export class PiSessionService {
|
|
|
1127
1244
|
async cancelAsk(ref, askId, managementContext) {
|
|
1128
1245
|
await this.assertWritable(ref);
|
|
1129
1246
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1130
|
-
this.assertTreeNavigationInactive(session, "
|
|
1131
|
-
return this.closeAsk(session, this.pendingAskStore.cancel(session
|
|
1247
|
+
this.assertTreeNavigationInactive(session, "关闭问题");
|
|
1248
|
+
return this.closeAsk(session, this.pendingAskStore.cancel(this.dialogSessionKey(session), askId));
|
|
1132
1249
|
}
|
|
1133
1250
|
/**
|
|
1134
1251
|
* Publish and deliver a closed ask. A stale close is reported rather than
|
|
@@ -1140,13 +1257,13 @@ export class PiSessionService {
|
|
|
1140
1257
|
if (result.status === "stale")
|
|
1141
1258
|
return { result: "stale", sessionStatus: this.statusFromSession(session) };
|
|
1142
1259
|
const { outcome } = result;
|
|
1143
|
-
this.publishAskClosed(session.sessionId, outcome);
|
|
1144
|
-
await this.runSessionEntryMutation(session, "
|
|
1260
|
+
this.publishAskClosed(session.sessionId, outcome, this.eventScopeForSession(session));
|
|
1261
|
+
await this.runSessionEntryMutation(session, "提交问题回答", () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: true, deliverAs: "followUp" }));
|
|
1145
1262
|
this.publishStatus(session);
|
|
1146
1263
|
return { result: "closed", outcome, sessionStatus: this.statusFromSession(session) };
|
|
1147
1264
|
}
|
|
1148
|
-
publishAskClosed(sessionId, outcome) {
|
|
1149
|
-
this.events.publish(sessionId, { type: "ask.closed", askId: outcome.askId, reason: outcome.reason });
|
|
1265
|
+
publishAskClosed(sessionId, outcome, scope) {
|
|
1266
|
+
this.events.publish(sessionId, { type: "ask.closed", askId: outcome.askId, reason: outcome.reason }, scope);
|
|
1150
1267
|
}
|
|
1151
1268
|
/**
|
|
1152
1269
|
* Void the session's open ask because the user sent a chat message instead of
|
|
@@ -1155,11 +1272,11 @@ export class PiSessionService {
|
|
|
1155
1272
|
* itself triggers rather than becoming a turn of its own.
|
|
1156
1273
|
*/
|
|
1157
1274
|
async voidOpenAskForUserMessage(session) {
|
|
1158
|
-
const outcome = this.pendingAskStore.cancelOpen(session
|
|
1275
|
+
const outcome = this.pendingAskStore.cancelOpen(this.dialogSessionKey(session));
|
|
1159
1276
|
if (outcome === undefined)
|
|
1160
1277
|
return;
|
|
1161
|
-
this.publishAskClosed(session.sessionId, outcome);
|
|
1162
|
-
await this.runSessionEntryMutation(session, "
|
|
1278
|
+
this.publishAskClosed(session.sessionId, outcome, this.eventScopeForSession(session));
|
|
1279
|
+
await this.runSessionEntryMutation(session, "作废未回答问题", () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: false, deliverAs: "followUp" }));
|
|
1163
1280
|
this.publishStatus(session);
|
|
1164
1281
|
}
|
|
1165
1282
|
/**
|
|
@@ -1204,8 +1321,12 @@ export class PiSessionService {
|
|
|
1204
1321
|
* `async` so a rejected dialog becomes a rejection rather than a synchronous
|
|
1205
1322
|
* throw from a promise-returning method.
|
|
1206
1323
|
*/
|
|
1207
|
-
async openExtensionDialog(session, request, opts) {
|
|
1208
|
-
const signal = opts?.signal
|
|
1324
|
+
async openExtensionDialog(session, request, opts, startupSignal) {
|
|
1325
|
+
const signal = opts?.signal === undefined
|
|
1326
|
+
? startupSignal
|
|
1327
|
+
: startupSignal === undefined
|
|
1328
|
+
? opts.signal
|
|
1329
|
+
: AbortSignal.any([opts.signal, startupSignal]);
|
|
1209
1330
|
// A pre-aborted signal dismisses the dialog before it ever opens.
|
|
1210
1331
|
if (signal?.aborted === true)
|
|
1211
1332
|
return extensionDialogCancelValue(request.kind);
|
|
@@ -1287,18 +1408,19 @@ export class PiSessionService {
|
|
|
1287
1408
|
}, this.eventScopeForSession(session));
|
|
1288
1409
|
}
|
|
1289
1410
|
/** Summaries of the tracked subsessions spawned by `parentSessionId`. */
|
|
1290
|
-
async listSubsessions(parentSessionId, parentSessionFile) {
|
|
1411
|
+
async listSubsessions(parentSessionId, parentSessionFile, managementContext) {
|
|
1291
1412
|
const parentFile = nonEmptyString(parentSessionFile);
|
|
1292
|
-
|
|
1293
|
-
|
|
1413
|
+
const scope = eventScopeFromManagementContext(managementContext);
|
|
1414
|
+
await this.hydrateSubsessionsForParent(parentSessionId, parentFile, managementContext);
|
|
1415
|
+
const childIds = this.subsessionChildren.get(activeSessionKey(parentSessionId, scope));
|
|
1294
1416
|
if (childIds === undefined)
|
|
1295
1417
|
return [];
|
|
1296
|
-
const authorizedChildIds = [...childIds].filter((childId) => this.subsessionLinkBelongsToParent(parentSessionId, parentFile, childId));
|
|
1297
|
-
return Promise.all(authorizedChildIds.map(async (childId) => ({ sessionId: childId, ...(await this.subsessionSummaryFields(childId)) })));
|
|
1418
|
+
const authorizedChildIds = [...childIds].filter((childId) => this.subsessionLinkBelongsToParent(parentSessionId, parentFile, childId, scope));
|
|
1419
|
+
return Promise.all(authorizedChildIds.map(async (childId) => ({ sessionId: childId, ...(await this.subsessionSummaryFields(childId, scope)) })));
|
|
1298
1420
|
}
|
|
1299
1421
|
/** Status and final result of a subsession, scoped to the caller's children. */
|
|
1300
|
-
async checkSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
1301
|
-
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile);
|
|
1422
|
+
async checkSubsession(parentSessionId, sessionId, parentSessionFile, managementContext) {
|
|
1423
|
+
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile, managementContext);
|
|
1302
1424
|
const messages = historyMessages(session);
|
|
1303
1425
|
return {
|
|
1304
1426
|
sessionId,
|
|
@@ -1309,8 +1431,8 @@ export class PiSessionService {
|
|
|
1309
1431
|
};
|
|
1310
1432
|
}
|
|
1311
1433
|
/** Filtered, paginated transcript of a subsession, scoped to the caller's children. */
|
|
1312
|
-
async readSubsession(parentSessionId, sessionId, query, parentSessionFile) {
|
|
1313
|
-
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile);
|
|
1434
|
+
async readSubsession(parentSessionId, sessionId, query, parentSessionFile, managementContext) {
|
|
1435
|
+
const session = await this.openSubsession(parentSessionId, sessionId, parentSessionFile, managementContext);
|
|
1314
1436
|
const view = buildTranscriptView(historyMessages(session), query);
|
|
1315
1437
|
return {
|
|
1316
1438
|
sessionId,
|
|
@@ -1320,52 +1442,61 @@ export class PiSessionService {
|
|
|
1320
1442
|
};
|
|
1321
1443
|
}
|
|
1322
1444
|
/** Open a session after verifying it is one of the caller's tracked children. */
|
|
1323
|
-
async openSubsession(parentSessionId, sessionId, parentSessionFile) {
|
|
1445
|
+
async openSubsession(parentSessionId, sessionId, parentSessionFile, managementContext) {
|
|
1324
1446
|
const parentFile = nonEmptyString(parentSessionFile);
|
|
1325
|
-
|
|
1326
|
-
|
|
1447
|
+
const scope = eventScopeFromManagementContext(managementContext);
|
|
1448
|
+
await this.hydrateSubsessionsForParent(parentSessionId, parentFile, managementContext);
|
|
1449
|
+
if (this.subsessionParents.get(activeSessionKey(sessionId, scope)) !== parentSessionId || !this.subsessionLinkBelongsToParent(parentSessionId, parentFile, sessionId, scope)) {
|
|
1327
1450
|
throw new Error(`Session ${sessionId} is not one of your subsessions`);
|
|
1328
1451
|
}
|
|
1329
|
-
return this.getOrOpenTrackedSubsession(sessionId);
|
|
1452
|
+
return this.getOrOpenTrackedSubsession(sessionId, scope);
|
|
1330
1453
|
}
|
|
1331
|
-
subsessionLinkBelongsToParent(parentSessionId, parentSessionFile, childSessionId) {
|
|
1332
|
-
const link = this.subsessionLinks.get(childSessionId);
|
|
1454
|
+
subsessionLinkBelongsToParent(parentSessionId, parentSessionFile, childSessionId, scope) {
|
|
1455
|
+
const link = this.subsessionLinks.get(activeSessionKey(childSessionId, scope));
|
|
1333
1456
|
if (link?.parentSessionId !== parentSessionId)
|
|
1334
1457
|
return false;
|
|
1335
1458
|
return parentSessionFile === undefined || trackedLinkParentFileMatches(link, parentSessionFile);
|
|
1336
1459
|
}
|
|
1337
1460
|
activeChildForSubsessionLink(link) {
|
|
1338
|
-
|
|
1461
|
+
const active = this.activeForSessionIdAndScope(link.childSessionId, link.eventScope);
|
|
1462
|
+
return active !== undefined && activeSessionFileMatches(active, link.childSessionFile) ? active : undefined;
|
|
1339
1463
|
}
|
|
1340
1464
|
activeParentForSubsessionLink(link) {
|
|
1341
|
-
|
|
1465
|
+
const active = this.activeForSessionIdAndScope(link.parentSessionId, link.eventScope);
|
|
1466
|
+
return active !== undefined && activeSessionFileMatches(active, link.parentSessionFile) ? active : undefined;
|
|
1342
1467
|
}
|
|
1343
1468
|
subsessionLinkForActiveChild(session) {
|
|
1344
|
-
const
|
|
1345
|
-
const parentId = this.subsessionParents.get(
|
|
1346
|
-
const link = this.subsessionLinks.get(
|
|
1469
|
+
const childKey = activeSessionKey(session.sessionId, this.eventScopeForSession(session));
|
|
1470
|
+
const parentId = this.subsessionParents.get(childKey);
|
|
1471
|
+
const link = this.subsessionLinks.get(childKey);
|
|
1347
1472
|
if (parentId === undefined || link?.parentSessionId !== parentId)
|
|
1348
1473
|
return undefined;
|
|
1349
1474
|
return sessionFileMatches(session, link.childSessionFile) ? link : undefined;
|
|
1350
1475
|
}
|
|
1351
|
-
async registerVerifiedSubsession(link,
|
|
1476
|
+
async registerVerifiedSubsession(link, managementContext) {
|
|
1352
1477
|
const { childSessionId, parentSessionId } = link;
|
|
1353
|
-
const
|
|
1478
|
+
const scope = eventScopeFromManagementContext(managementContext);
|
|
1479
|
+
const childKey = activeSessionKey(childSessionId, scope);
|
|
1480
|
+
const parentKey = activeSessionKey(parentSessionId, scope);
|
|
1481
|
+
const scopedLink = { ...link, eventScope: scope, ...(managementContext === undefined ? {} : { managementContext }) };
|
|
1482
|
+
const previousParentId = this.subsessionParents.get(childKey);
|
|
1354
1483
|
if (previousParentId !== undefined && previousParentId !== parentSessionId) {
|
|
1355
|
-
const
|
|
1484
|
+
const previousParentKey = activeSessionKey(previousParentId, scope);
|
|
1485
|
+
const previousChildren = this.subsessionChildren.get(previousParentKey);
|
|
1356
1486
|
previousChildren?.delete(childSessionId);
|
|
1357
1487
|
if (previousChildren?.size === 0)
|
|
1358
|
-
this.subsessionChildren.delete(
|
|
1488
|
+
this.subsessionChildren.delete(previousParentKey);
|
|
1359
1489
|
}
|
|
1360
|
-
this.subsessionParents.set(
|
|
1361
|
-
const children = this.subsessionChildren.get(
|
|
1490
|
+
this.subsessionParents.set(childKey, parentSessionId);
|
|
1491
|
+
const children = this.subsessionChildren.get(parentKey) ?? new Set();
|
|
1362
1492
|
children.add(childSessionId);
|
|
1363
|
-
this.subsessionChildren.set(
|
|
1364
|
-
this.subsessionLinks.set(
|
|
1365
|
-
if (!this.subsessionNotifyArmed.has(
|
|
1366
|
-
this.subsessionNotifyArmed.set(
|
|
1367
|
-
const cwd = this.cwdForVerifiedSubsession(
|
|
1493
|
+
this.subsessionChildren.set(parentKey, children);
|
|
1494
|
+
this.subsessionLinks.set(childKey, scopedLink);
|
|
1495
|
+
if (!this.subsessionNotifyArmed.has(childKey))
|
|
1496
|
+
this.subsessionNotifyArmed.set(childKey, false);
|
|
1497
|
+
const cwd = this.cwdForVerifiedSubsession(scopedLink);
|
|
1368
1498
|
await this.publishUnreadMutations(this.unreadForScope(scope).excludeSession(childSessionId, cwd));
|
|
1499
|
+
return scopedLink;
|
|
1369
1500
|
}
|
|
1370
1501
|
cwdForVerifiedSubsession(link) {
|
|
1371
1502
|
const activeCwd = this.activeChildForSubsessionLink(link)?.runtime.session.sessionManager.getCwd();
|
|
@@ -1383,17 +1514,19 @@ export class PiSessionService {
|
|
|
1383
1514
|
}
|
|
1384
1515
|
throw new Error("Could not resolve cwd for verified tracked sub-session");
|
|
1385
1516
|
}
|
|
1386
|
-
unregisterSubsession(childSessionId) {
|
|
1387
|
-
const
|
|
1388
|
-
this.subsessionParents.
|
|
1389
|
-
this.
|
|
1390
|
-
this.
|
|
1517
|
+
unregisterSubsession(childSessionId, scope) {
|
|
1518
|
+
const childKey = activeSessionKey(childSessionId, scope);
|
|
1519
|
+
const parentSessionId = this.subsessionParents.get(childKey);
|
|
1520
|
+
this.subsessionParents.delete(childKey);
|
|
1521
|
+
this.subsessionLinks.delete(childKey);
|
|
1522
|
+
this.subsessionNotifyArmed.delete(childKey);
|
|
1391
1523
|
if (parentSessionId === undefined)
|
|
1392
1524
|
return;
|
|
1393
|
-
const
|
|
1525
|
+
const parentKey = activeSessionKey(parentSessionId, scope);
|
|
1526
|
+
const children = this.subsessionChildren.get(parentKey);
|
|
1394
1527
|
children?.delete(childSessionId);
|
|
1395
1528
|
if (children?.size === 0)
|
|
1396
|
-
this.subsessionChildren.delete(
|
|
1529
|
+
this.subsessionChildren.delete(parentKey);
|
|
1397
1530
|
}
|
|
1398
1531
|
persistSubsessionLink(link) {
|
|
1399
1532
|
const parent = this.activeParentForSubsessionLink(link)?.runtime.session;
|
|
@@ -1408,8 +1541,9 @@ export class PiSessionService {
|
|
|
1408
1541
|
this.logger.info({ parentSessionId: link.parentSessionId, sessionId: link.childSessionId, error: error instanceof Error ? error.message : String(error) }, "failed to persist subsession link");
|
|
1409
1542
|
}
|
|
1410
1543
|
}
|
|
1411
|
-
persistSubsessionChildMarker(
|
|
1412
|
-
const
|
|
1544
|
+
persistSubsessionChildMarker(link) {
|
|
1545
|
+
const { parentSessionId, childSessionId } = link;
|
|
1546
|
+
const child = this.activeChildForSubsessionLink(link)?.runtime.session;
|
|
1413
1547
|
if (child === undefined)
|
|
1414
1548
|
return;
|
|
1415
1549
|
if (child.sessionManager.appendCustomEntry === undefined)
|
|
@@ -1421,14 +1555,15 @@ export class PiSessionService {
|
|
|
1421
1555
|
this.logger.info({ parentSessionId, sessionId: childSessionId, error: error instanceof Error ? error.message : String(error) }, "failed to persist subsession child marker");
|
|
1422
1556
|
}
|
|
1423
1557
|
}
|
|
1424
|
-
async hydrateSubsessionsForParent(parentSessionId, parentSessionFile) {
|
|
1425
|
-
const
|
|
1558
|
+
async hydrateSubsessionsForParent(parentSessionId, parentSessionFile, managementContext) {
|
|
1559
|
+
const scope = eventScopeFromManagementContext(managementContext);
|
|
1560
|
+
const hydrationKey = subsessionHydratedParentKey(activeSessionKey(parentSessionId, scope), parentSessionFile);
|
|
1426
1561
|
if (this.subsessionHydratedParents.has(hydrationKey))
|
|
1427
1562
|
return;
|
|
1428
|
-
const activeParent = this.
|
|
1563
|
+
const activeParent = this.activeForSessionIdAndScope(parentSessionId, scope);
|
|
1429
1564
|
if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
|
|
1430
1565
|
const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
|
|
1431
|
-
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile,
|
|
1566
|
+
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, activeParent.runtime.session.sessionManager, activeParentFile, managementContext);
|
|
1432
1567
|
if (complete)
|
|
1433
1568
|
this.subsessionHydratedParents.add(hydrationKey);
|
|
1434
1569
|
return;
|
|
@@ -1444,11 +1579,11 @@ export class PiSessionService {
|
|
|
1444
1579
|
catch {
|
|
1445
1580
|
return;
|
|
1446
1581
|
}
|
|
1447
|
-
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
|
|
1582
|
+
const complete = await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile, managementContext);
|
|
1448
1583
|
if (complete)
|
|
1449
1584
|
this.subsessionHydratedParents.add(hydrationKey);
|
|
1450
1585
|
}
|
|
1451
|
-
async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile,
|
|
1586
|
+
async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile, managementContext) {
|
|
1452
1587
|
// Parent custom links are the authoritative recovery record: verify the
|
|
1453
1588
|
// exact live child file/header before tracking. Do not negatively cache a
|
|
1454
1589
|
// scan while a candidate child is temporarily unavailable.
|
|
@@ -1463,7 +1598,7 @@ export class PiSessionService {
|
|
|
1463
1598
|
complete = false;
|
|
1464
1599
|
continue;
|
|
1465
1600
|
}
|
|
1466
|
-
await this.registerVerifiedSubsession(verified,
|
|
1601
|
+
await this.registerVerifiedSubsession(verified, managementContext);
|
|
1467
1602
|
}
|
|
1468
1603
|
return complete;
|
|
1469
1604
|
}
|
|
@@ -1484,7 +1619,7 @@ export class PiSessionService {
|
|
|
1484
1619
|
const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
|
|
1485
1620
|
if (link === undefined)
|
|
1486
1621
|
return;
|
|
1487
|
-
await this.registerVerifiedSubsession(link, this.
|
|
1622
|
+
await this.registerVerifiedSubsession(link, this.sessionManagementContexts.get(session));
|
|
1488
1623
|
}
|
|
1489
1624
|
verifiedSubsessionLinkFromOpenedChild(session) {
|
|
1490
1625
|
return verifiedTrackedSubsessionLink(this.sessionManager, {
|
|
@@ -1494,8 +1629,8 @@ export class PiSessionService {
|
|
|
1494
1629
|
cwd: session.sessionManager.getCwd(),
|
|
1495
1630
|
});
|
|
1496
1631
|
}
|
|
1497
|
-
async getOrOpenTrackedSubsession(sessionId) {
|
|
1498
|
-
const link = this.subsessionLinks.get(sessionId);
|
|
1632
|
+
async getOrOpenTrackedSubsession(sessionId, scope) {
|
|
1633
|
+
const link = this.subsessionLinks.get(activeSessionKey(sessionId, scope));
|
|
1499
1634
|
if (link === undefined)
|
|
1500
1635
|
throw new Error("Session not found");
|
|
1501
1636
|
const active = this.activeChildForSubsessionLink(link);
|
|
@@ -1505,12 +1640,14 @@ export class PiSessionService {
|
|
|
1505
1640
|
if (!(await sessionFileHeaderMatches(link.childSessionFile, { sessionId, parentSessionFile: link.parentSessionFile })))
|
|
1506
1641
|
throw new Error("Session not found");
|
|
1507
1642
|
const sessionManager = this.sessionManager.open(link.childSessionFile);
|
|
1508
|
-
return (await this.create(sessionManager, link.cwd ?? sessionManager.getCwd()
|
|
1643
|
+
return (await this.create(sessionManager, link.cwd ?? sessionManager.getCwd(), {
|
|
1644
|
+
...(link.managementContext === undefined ? {} : { managementContext: link.managementContext }),
|
|
1645
|
+
})).runtime.session;
|
|
1509
1646
|
}
|
|
1510
1647
|
throw new Error("Session not found");
|
|
1511
1648
|
}
|
|
1512
|
-
async subsessionSummaryFields(childSessionId) {
|
|
1513
|
-
const link = this.subsessionLinks.get(childSessionId);
|
|
1649
|
+
async subsessionSummaryFields(childSessionId, scope) {
|
|
1650
|
+
const link = this.subsessionLinks.get(activeSessionKey(childSessionId, scope));
|
|
1514
1651
|
const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
|
|
1515
1652
|
if (active !== undefined) {
|
|
1516
1653
|
return { cwd: active.runtime.cwd, status: this.subsessionStatus(active.runtime.session) };
|
|
@@ -1529,12 +1666,12 @@ export class PiSessionService {
|
|
|
1529
1666
|
return "error";
|
|
1530
1667
|
return "idle";
|
|
1531
1668
|
}
|
|
1532
|
-
workingSubsessionIds(parentSessionId) {
|
|
1533
|
-
const childIds = this.subsessionChildren.get(parentSessionId);
|
|
1669
|
+
workingSubsessionIds(parentSessionId, scope) {
|
|
1670
|
+
const childIds = this.subsessionChildren.get(activeSessionKey(parentSessionId, scope));
|
|
1534
1671
|
if (childIds === undefined)
|
|
1535
1672
|
return [];
|
|
1536
1673
|
return [...childIds].filter((childId) => {
|
|
1537
|
-
const link = this.subsessionLinks.get(childId);
|
|
1674
|
+
const link = this.subsessionLinks.get(activeSessionKey(childId, scope));
|
|
1538
1675
|
const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
|
|
1539
1676
|
return active !== undefined && this.hasActiveWork(active.runtime.session);
|
|
1540
1677
|
});
|
|
@@ -1550,27 +1687,26 @@ export class PiSessionService {
|
|
|
1550
1687
|
if (link === undefined)
|
|
1551
1688
|
return;
|
|
1552
1689
|
const childId = link.childSessionId;
|
|
1690
|
+
const childKey = activeSessionKey(childId, link.eventScope);
|
|
1553
1691
|
if (this.hasActiveWork(session)) {
|
|
1554
|
-
this.subsessionNotifyArmed.set(
|
|
1692
|
+
this.subsessionNotifyArmed.set(childKey, true);
|
|
1555
1693
|
return;
|
|
1556
1694
|
}
|
|
1557
|
-
if (this.subsessionNotifyArmed.get(
|
|
1695
|
+
if (this.subsessionNotifyArmed.get(childKey) !== true)
|
|
1558
1696
|
return;
|
|
1559
|
-
this.subsessionNotifyArmed.set(
|
|
1697
|
+
this.subsessionNotifyArmed.set(childKey, false);
|
|
1560
1698
|
const status = this.activityForSession(session)?.phase === "error" ? "error" : "idle";
|
|
1561
1699
|
const finalText = finalAssistantText(historyMessages(session));
|
|
1562
1700
|
const outputSection = formatSubsessionNotificationOutput(childId, finalText);
|
|
1563
|
-
const workingIds = this.workingSubsessionIds(link.parentSessionId);
|
|
1701
|
+
const workingIds = this.workingSubsessionIds(link.parentSessionId, link.eventScope);
|
|
1564
1702
|
const next = workingIds.length === 0
|
|
1565
1703
|
? "No other tracked subsessions are working."
|
|
1566
1704
|
: `Still working: ${workingIds.join(", ")}. Continue working, or call yield_to_subsessions alone and last at the next join point. Further completion notices arrive automatically; do not poll.`;
|
|
1567
1705
|
const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n${outputSection}`;
|
|
1568
|
-
void this.notifyParentOfSubsession(link
|
|
1706
|
+
void this.notifyParentOfSubsession(link, text);
|
|
1569
1707
|
}
|
|
1570
|
-
async getOrOpenParentForSubsession(
|
|
1571
|
-
const
|
|
1572
|
-
if (link?.parentSessionId !== parentSessionId)
|
|
1573
|
-
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
1708
|
+
async getOrOpenParentForSubsession(link) {
|
|
1709
|
+
const { parentSessionId } = link;
|
|
1574
1710
|
const active = this.activeParentForSubsessionLink(link);
|
|
1575
1711
|
if (active !== undefined)
|
|
1576
1712
|
return active.runtime.session;
|
|
@@ -1581,7 +1717,9 @@ export class PiSessionService {
|
|
|
1581
1717
|
throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
|
|
1582
1718
|
}
|
|
1583
1719
|
const sessionManager = this.sessionManager.open(parentSessionFile);
|
|
1584
|
-
return (await this.create(sessionManager, sessionManager.getCwd()
|
|
1720
|
+
return (await this.create(sessionManager, sessionManager.getCwd(), {
|
|
1721
|
+
...(link.managementContext === undefined ? {} : { managementContext: link.managementContext }),
|
|
1722
|
+
})).runtime.session;
|
|
1585
1723
|
}
|
|
1586
1724
|
/**
|
|
1587
1725
|
* Deliver a subsession-completion notice to the parent as a system-authored
|
|
@@ -1590,9 +1728,10 @@ export class PiSessionService {
|
|
|
1590
1728
|
* queues behind in-flight work (`deliverAs: "followUp"`), preserving the
|
|
1591
1729
|
* established "queue if busy, send and act if idle" behavior.
|
|
1592
1730
|
*/
|
|
1593
|
-
async notifyParentOfSubsession(
|
|
1731
|
+
async notifyParentOfSubsession(link, text) {
|
|
1732
|
+
const { parentSessionId: parentId, childSessionId: childId } = link;
|
|
1594
1733
|
try {
|
|
1595
|
-
const session = await this.getOrOpenParentForSubsession(
|
|
1734
|
+
const session = await this.getOrOpenParentForSubsession(link);
|
|
1596
1735
|
if (this.treeNavigations.has(session)) {
|
|
1597
1736
|
const pending = this.deferredSubsessionNotifications.get(session) ?? [];
|
|
1598
1737
|
pending.push({ parentId, childId, text });
|
|
@@ -1611,6 +1750,9 @@ export class PiSessionService {
|
|
|
1611
1750
|
}
|
|
1612
1751
|
async status(ref, managementContext) {
|
|
1613
1752
|
const session = await this.sessionForStatusOrDialogClose(ref, managementContext);
|
|
1753
|
+
if (this.eventScopeForSession(session) === NORMAL_SESSION_EVENT_SCOPE) {
|
|
1754
|
+
await this.activityMarker.refresh(session.sessionFile, this.hasActiveWork(session));
|
|
1755
|
+
}
|
|
1614
1756
|
if (this.hasActiveWork(session))
|
|
1615
1757
|
return this.statusFromSession(session);
|
|
1616
1758
|
const branch = await this.readableSessionBranch(session);
|
|
@@ -1645,6 +1787,60 @@ export class PiSessionService {
|
|
|
1645
1787
|
: annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
|
|
1646
1788
|
return { seq, partial };
|
|
1647
1789
|
}
|
|
1790
|
+
async getSessionDefaults(ref, managementContext) {
|
|
1791
|
+
const session = await this.getOrOpen(ref, managementContext);
|
|
1792
|
+
const settings = await this.freshSessionDefaultsSettings(session);
|
|
1793
|
+
return parseSessionDefaults(settings.getGlobalSettings());
|
|
1794
|
+
}
|
|
1795
|
+
async setSessionDefaults(ref, defaults, managementContext) {
|
|
1796
|
+
const update = parseSessionDefaultsUpdate({ ...defaults });
|
|
1797
|
+
await this.assertWritable(ref);
|
|
1798
|
+
const session = await this.getOrOpen(ref, managementContext);
|
|
1799
|
+
return this.runModelScopeMutation(async () => {
|
|
1800
|
+
const settings = await this.freshSessionDefaultsSettings(session);
|
|
1801
|
+
if (update.provider !== undefined && update.modelId !== undefined) {
|
|
1802
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1803
|
+
const target = `${update.provider}/${update.modelId}`;
|
|
1804
|
+
const available = session.modelRuntime.getAvailableSnapshot();
|
|
1805
|
+
if (!available.some((model) => modelScopeId(model) === target))
|
|
1806
|
+
throw new Error(`未找到模型:${target}`);
|
|
1807
|
+
const enabledIds = await resolveEnabledModelIds({
|
|
1808
|
+
settingsManager: settings,
|
|
1809
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
1810
|
+
modelRuntime: session.modelRuntime,
|
|
1811
|
+
scopedModels: [],
|
|
1812
|
+
});
|
|
1813
|
+
if (enabledIds !== null && !enabledIds.includes(target))
|
|
1814
|
+
throw new Error(`模型未启用:${target}`);
|
|
1815
|
+
settings.setDefaultModelAndProvider(update.provider, update.modelId);
|
|
1816
|
+
}
|
|
1817
|
+
else if (update.thinkingLevel !== undefined) {
|
|
1818
|
+
settings.setDefaultThinkingLevel(update.thinkingLevel);
|
|
1819
|
+
}
|
|
1820
|
+
await settings.flush();
|
|
1821
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1822
|
+
return parseSessionDefaults(settings.getGlobalSettings());
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
async freshSessionDefaultsSettings(session) {
|
|
1826
|
+
const cwd = session.sessionManager.getCwd();
|
|
1827
|
+
const mode = this.eventScopeForSession(session) === NORMAL_SESSION_EVENT_SCOPE ? "normal" : "management";
|
|
1828
|
+
const settings = await createScopedSettingsManager({
|
|
1829
|
+
cwd,
|
|
1830
|
+
scopeDirectory: await this.resolveSettingsScopeDirectory(cwd, mode),
|
|
1831
|
+
globalAgentDir: this.agentDir,
|
|
1832
|
+
mode,
|
|
1833
|
+
projectTrusted: session.settingsManager.isProjectTrusted?.() ?? true,
|
|
1834
|
+
});
|
|
1835
|
+
await settings.reload();
|
|
1836
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1837
|
+
return settings;
|
|
1838
|
+
}
|
|
1839
|
+
assertDefaultsSettingsHealthy(settings) {
|
|
1840
|
+
const errors = settings.drainErrors();
|
|
1841
|
+
if (errors.length > 0)
|
|
1842
|
+
throw new Error(`会话默认值设置失败:${errors.map(({ error }) => error.message).join("; ")}`);
|
|
1843
|
+
}
|
|
1648
1844
|
async availableModels(ref, managementContext) {
|
|
1649
1845
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1650
1846
|
const models = await this.sessionModelCandidates(session);
|
|
@@ -1664,7 +1860,7 @@ export class PiSessionService {
|
|
|
1664
1860
|
const scope = this.modelScopeContext(session);
|
|
1665
1861
|
if (!scope.editable)
|
|
1666
1862
|
throw new Error("模型可用范围由此工作区的 .pi/settings.json 控制");
|
|
1667
|
-
this.assertTreeNavigationInactive(session, "
|
|
1863
|
+
this.assertTreeNavigationInactive(session, "修改已启用模型");
|
|
1668
1864
|
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1669
1865
|
const currentIds = (await this.modelScopeSnapshot(session, scope)).enabledIds;
|
|
1670
1866
|
const available = session.modelRuntime.getAvailableSnapshot();
|
|
@@ -1675,7 +1871,7 @@ export class PiSessionService {
|
|
|
1675
1871
|
if (!enabled && session.model !== undefined && modelScopeId(session.model) === targetId)
|
|
1676
1872
|
throw new Error("不能停用当前模型");
|
|
1677
1873
|
const nextIds = applyEnabledModelToggle(currentIds, availableIds, targetId, enabled);
|
|
1678
|
-
this.assertTreeNavigationInactive(session, "
|
|
1874
|
+
this.assertTreeNavigationInactive(session, "修改已启用模型");
|
|
1679
1875
|
if (nextIds !== currentIds)
|
|
1680
1876
|
this.applyEnabledModelScope(session, available, nextIds);
|
|
1681
1877
|
return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry));
|
|
@@ -1690,7 +1886,7 @@ export class PiSessionService {
|
|
|
1690
1886
|
const scope = this.modelScopeContext(session);
|
|
1691
1887
|
if (!scope.editable)
|
|
1692
1888
|
throw new Error("模型可用范围由此工作区的 .pi/settings.json 控制");
|
|
1693
|
-
this.assertTreeNavigationInactive(session, "
|
|
1889
|
+
this.assertTreeNavigationInactive(session, "修改已启用模型");
|
|
1694
1890
|
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1695
1891
|
const available = session.modelRuntime.getAvailableSnapshot();
|
|
1696
1892
|
const availableIds = available.map(modelScopeId);
|
|
@@ -1701,7 +1897,7 @@ export class PiSessionService {
|
|
|
1701
1897
|
throw new Error("当前模型不可用");
|
|
1702
1898
|
nextIds = [modelScopeId(current)];
|
|
1703
1899
|
}
|
|
1704
|
-
this.assertTreeNavigationInactive(session, "
|
|
1900
|
+
this.assertTreeNavigationInactive(session, "修改已启用模型");
|
|
1705
1901
|
this.applyEnabledModelScope(session, available, nextIds);
|
|
1706
1902
|
return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry));
|
|
1707
1903
|
});
|
|
@@ -1709,14 +1905,14 @@ export class PiSessionService {
|
|
|
1709
1905
|
async setModel(ref, provider, modelId, managementContext) {
|
|
1710
1906
|
await this.assertWritable(ref);
|
|
1711
1907
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1712
|
-
this.assertTreeNavigationInactive(session, "
|
|
1908
|
+
this.assertTreeNavigationInactive(session, "更改模型");
|
|
1713
1909
|
const candidates = await this.sessionModelCandidates(session);
|
|
1714
|
-
this.assertTreeNavigationInactive(session, "
|
|
1910
|
+
this.assertTreeNavigationInactive(session, "更改模型");
|
|
1715
1911
|
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
|
|
1716
1912
|
?? session.modelRuntime.getModel(provider, modelId);
|
|
1717
1913
|
if (model === undefined)
|
|
1718
1914
|
throw new Error(`未找到模型:${provider}/${modelId}`);
|
|
1719
|
-
await this.runSessionEntryMutation(session, "
|
|
1915
|
+
await this.runSessionEntryMutation(session, "更改模型", () => session.setModel(model));
|
|
1720
1916
|
this.publishActivity(session, `模型:${model.id}`, "idle", model.provider);
|
|
1721
1917
|
this.publishStatus(session);
|
|
1722
1918
|
return this.statusFromSession(session);
|
|
@@ -1727,7 +1923,7 @@ export class PiSessionService {
|
|
|
1727
1923
|
const result = await this.runModelScopeMutation(async () => {
|
|
1728
1924
|
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1729
1925
|
await this.synchronizeSessionModelScope(session, session.modelRuntime.getAvailableSnapshot());
|
|
1730
|
-
return this.runSessionEntryMutation(session, "
|
|
1926
|
+
return this.runSessionEntryMutation(session, "更改模型", () => session.cycleModel(direction));
|
|
1731
1927
|
});
|
|
1732
1928
|
if (result === undefined)
|
|
1733
1929
|
throw new Error(session.scopedModels.length > 0 ? "作用域内只有一个模型" : "只有一个可用模型");
|
|
@@ -1742,14 +1938,14 @@ export class PiSessionService {
|
|
|
1742
1938
|
async setThinkingLevel(ref, level, managementContext) {
|
|
1743
1939
|
await this.assertWritable(ref);
|
|
1744
1940
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1745
|
-
this.assertTreeNavigationInactive(session, "
|
|
1941
|
+
this.assertTreeNavigationInactive(session, "更改思考级别");
|
|
1746
1942
|
// pi owns the valid set; validate against the session's live levels rather
|
|
1747
1943
|
// than a hardcoded union so this stays correct if pi changes the set.
|
|
1748
1944
|
const available = session.getAvailableThinkingLevels();
|
|
1749
1945
|
const match = available.find((candidate) => candidate === level);
|
|
1750
1946
|
if (match === undefined)
|
|
1751
1947
|
throw new Error(`无效的思考级别:${level}`);
|
|
1752
|
-
await this.runSessionEntryMutation(session, "
|
|
1948
|
+
await this.runSessionEntryMutation(session, "更改思考级别", () => {
|
|
1753
1949
|
session.setThinkingLevel(match);
|
|
1754
1950
|
return Promise.resolve();
|
|
1755
1951
|
});
|
|
@@ -1760,7 +1956,7 @@ export class PiSessionService {
|
|
|
1760
1956
|
async cycleThinkingLevel(ref, managementContext) {
|
|
1761
1957
|
await this.assertWritable(ref);
|
|
1762
1958
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1763
|
-
const level = await this.runSessionEntryMutation(session, "
|
|
1959
|
+
const level = await this.runSessionEntryMutation(session, "更改思考级别", () => Promise.resolve(session.cycleThinkingLevel()));
|
|
1764
1960
|
if (level === undefined)
|
|
1765
1961
|
throw new Error("当前模型不支持思考");
|
|
1766
1962
|
this.publishActivity(session, `思考级别:${level}`, "idle");
|
|
@@ -1774,7 +1970,12 @@ export class PiSessionService {
|
|
|
1774
1970
|
commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
|
|
1775
1971
|
}
|
|
1776
1972
|
for (const template of session.promptTemplates) {
|
|
1777
|
-
commands.push({
|
|
1973
|
+
commands.push({
|
|
1974
|
+
name: template.name,
|
|
1975
|
+
...(template.description === undefined ? {} : { description: template.description }),
|
|
1976
|
+
...(template.argumentHint === undefined ? {} : { argumentHint: template.argumentHint }),
|
|
1977
|
+
source: "prompt",
|
|
1978
|
+
});
|
|
1778
1979
|
}
|
|
1779
1980
|
for (const skill of session.resourceLoader.getSkills().skills) {
|
|
1780
1981
|
commands.push({ name: `skill:${skill.name}`, ...(skill.description === undefined ? {} : { description: skill.description }), source: "skill" });
|
|
@@ -1793,7 +1994,7 @@ export class PiSessionService {
|
|
|
1793
1994
|
const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
|
|
1794
1995
|
await this.assertWritable(ref);
|
|
1795
1996
|
const session = await this.getOrOpen(ref, options?.managementContext);
|
|
1796
|
-
this.assertTreeNavigationInactive(session, "
|
|
1997
|
+
this.assertTreeNavigationInactive(session, "发送消息");
|
|
1797
1998
|
this.maybeGenerateSessionName(session, promptText);
|
|
1798
1999
|
const isQueued = session.isStreaming || session.isCompacting;
|
|
1799
2000
|
const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
|
|
@@ -1814,24 +2015,78 @@ export class PiSessionService {
|
|
|
1814
2015
|
void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
|
|
1815
2016
|
}
|
|
1816
2017
|
submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
|
|
2018
|
+
const promptPromise = this.beginPromptSubmission(session, text, behavior, images, echoUserMessage).catch(() => undefined);
|
|
2019
|
+
void promptPromise;
|
|
2020
|
+
return promptPromise;
|
|
2021
|
+
}
|
|
2022
|
+
beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1817
2023
|
this.auditUserPrompt(session, text);
|
|
1818
2024
|
this.publishActivity(session, behavior === "steer" ? "插队消息已排队" : behavior === "followUp" ? "消息已排队" : "消息已接收", "active");
|
|
1819
2025
|
const eventScope = this.eventScopeForSession(session);
|
|
1820
2026
|
if (behavior === undefined && echoUserMessage)
|
|
1821
2027
|
this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) }, eventScope);
|
|
1822
2028
|
const promptOptions = buildPromptOptions(behavior, images);
|
|
1823
|
-
const promptPromise = this.runSessionEntryMutation(session, "
|
|
2029
|
+
const promptPromise = this.runSessionEntryMutation(session, "发送消息", () => session.prompt(text, promptOptions));
|
|
2030
|
+
void promptPromise.catch((error) => {
|
|
1824
2031
|
const message = error instanceof Error ? error.message : String(error);
|
|
1825
2032
|
this.publishActivity(session, "发生错误", "error", message);
|
|
1826
2033
|
this.events.publish(session.sessionId, { type: "session.error", message }, eventScope);
|
|
1827
2034
|
});
|
|
1828
|
-
void promptPromise;
|
|
1829
2035
|
return promptPromise;
|
|
1830
2036
|
}
|
|
2037
|
+
async submitInitialRunPrompt(session, text) {
|
|
2038
|
+
let failure;
|
|
2039
|
+
let settled = false;
|
|
2040
|
+
let sawInitialUserMessage = false;
|
|
2041
|
+
const unsubscribe = session.subscribe((event) => {
|
|
2042
|
+
if (settled || !isRecord(event))
|
|
2043
|
+
return;
|
|
2044
|
+
if (event["type"] === "agent_settled") {
|
|
2045
|
+
// Freeze the initial outcome before a later user submission can change it.
|
|
2046
|
+
settled = true;
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
if (event["type"] === "auto_retry_end" && event["success"] === false && event["finalError"] === "Retry cancelled") {
|
|
2050
|
+
// Native Pi cancels retry sleep without an aborted assistant message.
|
|
2051
|
+
failure = new DOMException("初始 PI 会话运行已中止", "AbortError");
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
const message = event["message"];
|
|
2055
|
+
if (!isRecord(message))
|
|
2056
|
+
return;
|
|
2057
|
+
if (event["type"] === "message_start" && message["role"] === "user") {
|
|
2058
|
+
// Pi can drain a queued user follow-up inside the same prompt promise.
|
|
2059
|
+
// Its outcome belongs to that later submission, not this launcher.
|
|
2060
|
+
if (sawInitialUserMessage)
|
|
2061
|
+
settled = true;
|
|
2062
|
+
sawInitialUserMessage = true;
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (event["type"] !== "message_end" || message["role"] !== "assistant")
|
|
2066
|
+
return;
|
|
2067
|
+
// A successful retry supersedes a transient provider failure. Pi's
|
|
2068
|
+
// prompt promise can fulfill even when the final assistant is an error.
|
|
2069
|
+
const reason = message["stopReason"];
|
|
2070
|
+
failure = reason === "aborted"
|
|
2071
|
+
? new DOMException("初始 PI 会话运行已中止", "AbortError")
|
|
2072
|
+
: reason === "error"
|
|
2073
|
+
? new Error(typeof message["errorMessage"] === "string" ? message["errorMessage"] : "初始 PI 会话运行失败")
|
|
2074
|
+
: undefined;
|
|
2075
|
+
});
|
|
2076
|
+
try {
|
|
2077
|
+
await this.beginPromptSubmission(session, text, undefined);
|
|
2078
|
+
if (failure !== undefined)
|
|
2079
|
+
throw failure;
|
|
2080
|
+
}
|
|
2081
|
+
finally {
|
|
2082
|
+
unsubscribe();
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
1831
2085
|
enqueuePromptDuringCompaction(session, text, kind, images = [], echoUserMessage = true) {
|
|
1832
|
-
const
|
|
2086
|
+
const sessionKey = activeSessionKey(session.sessionId, this.eventScopeForSession(session));
|
|
2087
|
+
const queue = this.compactionPromptQueues.get(sessionKey) ?? [];
|
|
1833
2088
|
queue.push({ kind, text, ...(images.length > 0 ? { images } : {}), ...(echoUserMessage ? {} : { echoUserMessage: false }) });
|
|
1834
|
-
this.compactionPromptQueues.set(
|
|
2089
|
+
this.compactionPromptQueues.set(sessionKey, queue);
|
|
1835
2090
|
this.publishActivity(session, "压缩期间消息已排队", "active");
|
|
1836
2091
|
this.publishStatus(session);
|
|
1837
2092
|
}
|
|
@@ -1853,7 +2108,7 @@ export class PiSessionService {
|
|
|
1853
2108
|
await this.assertWritable(ref);
|
|
1854
2109
|
const active = await this.getActive(ref, managementContext);
|
|
1855
2110
|
const { session } = active.runtime;
|
|
1856
|
-
this.assertTreeNavigationInactive(session, "
|
|
2111
|
+
this.assertTreeNavigationInactive(session, "运行 Shell 命令");
|
|
1857
2112
|
const isExcluded = text.startsWith("!!");
|
|
1858
2113
|
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
|
1859
2114
|
if (!command)
|
|
@@ -1863,7 +2118,7 @@ export class PiSessionService {
|
|
|
1863
2118
|
const eventScope = active.eventScope;
|
|
1864
2119
|
this.publishActivity(session, "正在运行命令", "active", command);
|
|
1865
2120
|
this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded }, eventScope);
|
|
1866
|
-
void this.runSessionEntryMutation(session, "
|
|
2121
|
+
void this.runSessionEntryMutation(session, "运行 Shell 命令", () => session.executeBash(command, (chunk) => {
|
|
1867
2122
|
this.events.publish(session.sessionId, { type: "shell.chunk", chunk }, eventScope);
|
|
1868
2123
|
this.publishActivity(session, "正在运行命令", "active", command);
|
|
1869
2124
|
this.publishStatus(session);
|
|
@@ -1898,42 +2153,74 @@ export class PiSessionService {
|
|
|
1898
2153
|
}
|
|
1899
2154
|
async navigateTree(ref, request, managementContext) {
|
|
1900
2155
|
if (request.targetId.trim() === "")
|
|
1901
|
-
throw new Error("
|
|
2156
|
+
throw new Error("会话树目标条目不能为空");
|
|
1902
2157
|
if (this.isTreeExclusiveSessionIdentityActive(sessionIdFromLookup(ref))) {
|
|
1903
|
-
throw new Error("
|
|
2158
|
+
throw new Error("切换会话树前请先停止当前会话活动");
|
|
1904
2159
|
}
|
|
1905
2160
|
await this.assertWritable(ref);
|
|
1906
2161
|
const options = sessionTreeNavigationOptions(request);
|
|
1907
2162
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1908
2163
|
if (typeof session.navigateTree !== "function")
|
|
1909
|
-
throw new Error("
|
|
2164
|
+
throw new Error("当前 Pi 运行时不支持会话树导航");
|
|
1910
2165
|
if (this.hasActiveWork(session))
|
|
1911
|
-
throw new Error("
|
|
2166
|
+
throw new Error("切换会话树前请先停止当前会话活动");
|
|
1912
2167
|
// Acquire synchronously after the active-work check. No leaf-producing work
|
|
1913
2168
|
// may enter this runtime until Pi's potentially asynchronous summary settles.
|
|
1914
2169
|
this.treeNavigations.add(session);
|
|
1915
2170
|
try {
|
|
1916
|
-
|
|
1917
|
-
|
|
2171
|
+
const oldLeafId = session.sessionManager.getLeafId();
|
|
2172
|
+
if (oldLeafId !== request.expectedLeafId) {
|
|
2173
|
+
throw new Error("打开 /tree 后会话已发生变化。请重新打开 /tree 后重试。");
|
|
1918
2174
|
}
|
|
2175
|
+
const activeEditableTargetParentId = activeEditableTreeTargetParentId(session.sessionManager, request.targetId, oldLeafId);
|
|
1919
2176
|
this.publishActivity(session, options.summarize ? "正在汇总分支" : "正在切换会话树", "active");
|
|
1920
2177
|
this.publishStatus(session);
|
|
1921
|
-
|
|
2178
|
+
let result = await session.navigateTree(request.targetId, options);
|
|
2179
|
+
if (activeEditableTargetParentId !== undefined && !result.cancelled
|
|
2180
|
+
&& result.editorText === undefined && result.summaryEntry === undefined
|
|
2181
|
+
&& session.sessionManager.getLeafId() === oldLeafId) {
|
|
2182
|
+
setSessionTreeLeaf(session.sessionManager, activeEditableTargetParentId);
|
|
2183
|
+
try {
|
|
2184
|
+
result = await session.navigateTree(request.targetId, options);
|
|
2185
|
+
}
|
|
2186
|
+
catch (error) {
|
|
2187
|
+
session.sessionManager.branch(request.targetId);
|
|
2188
|
+
throw error;
|
|
2189
|
+
}
|
|
2190
|
+
if (result.cancelled)
|
|
2191
|
+
session.sessionManager.branch(request.targetId);
|
|
2192
|
+
}
|
|
1922
2193
|
if (result.cancelled) {
|
|
1923
2194
|
if (this.isCurrentActiveSession(session)) {
|
|
1924
2195
|
this.publishActivity(session, result.aborted === true ? "分支汇总已终止" : "会话树切换已取消", "idle");
|
|
1925
2196
|
}
|
|
1926
2197
|
return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
|
|
1927
2198
|
}
|
|
2199
|
+
if (session.sessionManager.getLeafId() !== oldLeafId) {
|
|
2200
|
+
// Questions belong to the branch we left. Close them without appending a
|
|
2201
|
+
// cancellation message or waking the rewound session.
|
|
2202
|
+
const outcome = this.pendingAskStore.cancelOpen(this.dialogSessionKey(session));
|
|
2203
|
+
if (outcome !== undefined)
|
|
2204
|
+
this.publishAskClosed(session.sessionId, outcome, this.eventScopeForSession(session));
|
|
2205
|
+
}
|
|
2206
|
+
if (result.summaryEntry !== undefined) {
|
|
2207
|
+
this.unpersistedTreeBranchLeaves.delete(session);
|
|
2208
|
+
}
|
|
2209
|
+
else {
|
|
2210
|
+
const selectedLeafId = session.sessionManager.getLeafId();
|
|
2211
|
+
if (selectedLeafId !== oldLeafId)
|
|
2212
|
+
this.unpersistedTreeBranchLeaves.set(session, selectedLeafId);
|
|
2213
|
+
}
|
|
1928
2214
|
if (this.isCurrentActiveSession(session))
|
|
1929
2215
|
this.publishActivity(session, "已切换会话树", "idle");
|
|
1930
2216
|
return { cancelled: false, ...(result.editorText === undefined ? {} : { editorText: result.editorText }) };
|
|
1931
2217
|
}
|
|
1932
2218
|
catch (error) {
|
|
1933
2219
|
const message = error instanceof Error ? error.message : String(error);
|
|
1934
|
-
this.
|
|
1935
|
-
|
|
1936
|
-
|
|
2220
|
+
if (this.isCurrentActiveSession(session)) {
|
|
2221
|
+
this.publishActivity(session, "会话树切换失败", "error", message);
|
|
2222
|
+
this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
|
|
2223
|
+
}
|
|
1937
2224
|
throw error;
|
|
1938
2225
|
}
|
|
1939
2226
|
finally {
|
|
@@ -1950,19 +2237,22 @@ export class PiSessionService {
|
|
|
1950
2237
|
}
|
|
1951
2238
|
async forkFromTree(ref, request, managementContext) {
|
|
1952
2239
|
if (request.entryId.trim() === "")
|
|
1953
|
-
throw new Error("
|
|
2240
|
+
throw new Error("会话树条目不能为空");
|
|
1954
2241
|
const requestedSessionId = sessionIdFromLookup(ref);
|
|
1955
2242
|
if (this.isTreeExclusiveSessionIdentityActive(requestedSessionId)) {
|
|
1956
|
-
throw new Error("
|
|
2243
|
+
throw new Error("分叉会话树前请先停止当前会话活动");
|
|
1957
2244
|
}
|
|
1958
2245
|
await this.assertWritable(ref);
|
|
1959
2246
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1960
2247
|
if (this.hasActiveWork(session))
|
|
1961
|
-
throw new Error("
|
|
2248
|
+
throw new Error("分叉会话树前请先停止当前会话活动");
|
|
2249
|
+
if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
|
|
2250
|
+
throw new Error("打开 /tree 后会话已发生变化。请重新打开 /tree 后重试。");
|
|
2251
|
+
}
|
|
1962
2252
|
const sourceSessionId = session.sessionId;
|
|
1963
2253
|
const sourceEventScope = this.eventScopeForSession(session);
|
|
1964
2254
|
const sourceCwd = session.sessionManager.getCwd();
|
|
1965
|
-
this.publishActivity(session, "
|
|
2255
|
+
this.publishActivity(session, "正在从条目分叉会话", "active");
|
|
1966
2256
|
this.publishStatus(session);
|
|
1967
2257
|
try {
|
|
1968
2258
|
const result = await this.commandService.forkEntry(session.sessionId, request.entryId, sourceEventScope, {
|
|
@@ -1971,10 +2261,10 @@ export class PiSessionService {
|
|
|
1971
2261
|
if (result.type === "unsupported")
|
|
1972
2262
|
throw new Error(result.message);
|
|
1973
2263
|
if (result.type !== "done")
|
|
1974
|
-
throw new Error("
|
|
2264
|
+
throw new Error("会话分叉不可用");
|
|
1975
2265
|
if (result.session === undefined) {
|
|
1976
2266
|
if (this.isCurrentActiveSession(session)) {
|
|
1977
|
-
this.publishActivity(session, "
|
|
2267
|
+
this.publishActivity(session, "分叉已取消", "idle");
|
|
1978
2268
|
this.publishStatus(session);
|
|
1979
2269
|
}
|
|
1980
2270
|
return { cancelled: true };
|
|
@@ -1995,7 +2285,7 @@ export class PiSessionService {
|
|
|
1995
2285
|
catch (error) {
|
|
1996
2286
|
const message = error instanceof Error ? error.message : String(error);
|
|
1997
2287
|
if (this.isCurrentActiveSession(session)) {
|
|
1998
|
-
this.publishActivity(session, "
|
|
2288
|
+
this.publishActivity(session, "分叉失败", "error", message);
|
|
1999
2289
|
this.events.publish(session.sessionId, { type: "session.error", message }, sourceEventScope);
|
|
2000
2290
|
this.publishStatus(session);
|
|
2001
2291
|
}
|
|
@@ -2154,8 +2444,6 @@ export class PiSessionService {
|
|
|
2154
2444
|
if (this.archiveStore.deleteArchived === undefined)
|
|
2155
2445
|
throw new Error("归档存储不支持删除");
|
|
2156
2446
|
await this.closeActiveInScope(record.sessionId, eventScopeFromManagementContext(managementContext));
|
|
2157
|
-
if (record.archivePath === undefined)
|
|
2158
|
-
await this.ensureArchivedRecordMoved(record);
|
|
2159
2447
|
await this.archiveStore.deleteArchived(record.sessionId);
|
|
2160
2448
|
await this.forgetUnreadSessions([record], eventScopeFromManagementContext(managementContext));
|
|
2161
2449
|
}
|
|
@@ -2189,12 +2477,7 @@ export class PiSessionService {
|
|
|
2189
2477
|
failures.push({ sessionId: item.record.sessionId, error: errorMessage(error) });
|
|
2190
2478
|
}
|
|
2191
2479
|
}
|
|
2192
|
-
const
|
|
2193
|
-
failures.push(...moveFailures);
|
|
2194
|
-
const moveFailureIds = new Set(moveFailures.map((failure) => failure.sessionId));
|
|
2195
|
-
const deleteIds = readyRecords
|
|
2196
|
-
.map((record) => record.sessionId)
|
|
2197
|
-
.filter((sessionId) => !moveFailureIds.has(sessionId));
|
|
2480
|
+
const deleteIds = readyRecords.map((record) => record.sessionId);
|
|
2198
2481
|
let deletedSessionIds = [];
|
|
2199
2482
|
try {
|
|
2200
2483
|
deletedSessionIds = await this.archiveStoreDeleteArchivedMany(deleteIds);
|
|
@@ -2234,6 +2517,7 @@ export class PiSessionService {
|
|
|
2234
2517
|
...(managementContext === undefined ? {} : { managementContext }),
|
|
2235
2518
|
...(candidateGeneration === undefined ? {} : { notificationGeneration: candidateGeneration }),
|
|
2236
2519
|
});
|
|
2520
|
+
await this.reloadModelScope(reopened.runtime.session);
|
|
2237
2521
|
if (candidateGeneration !== undefined) {
|
|
2238
2522
|
this.publishNotificationMutations(this.notificationsForSession(session).commitReplacement(candidateGeneration));
|
|
2239
2523
|
}
|
|
@@ -2249,22 +2533,22 @@ export class PiSessionService {
|
|
|
2249
2533
|
this.publishStatus(reopenedSession);
|
|
2250
2534
|
}
|
|
2251
2535
|
async detachParent(ref, managementContext) {
|
|
2536
|
+
await this.assertWritable(ref);
|
|
2252
2537
|
const session = await this.getOrOpen(ref, managementContext);
|
|
2253
2538
|
const sessionFile = session.sessionFile;
|
|
2254
2539
|
if (sessionFile === undefined || sessionFile === "")
|
|
2255
2540
|
throw new Error("会话尚未持久化");
|
|
2256
2541
|
clearSessionFileParent(sessionFile);
|
|
2257
|
-
this.sessionManager.invalidateSessionFile
|
|
2542
|
+
this.sessionManager.invalidateSessionFile(sessionFile);
|
|
2258
2543
|
clearParentSessionHeader(session.sessionManager);
|
|
2259
|
-
this.unregisterSubsession(session.sessionId);
|
|
2544
|
+
this.unregisterSubsession(session.sessionId, this.eventScopeForSession(session));
|
|
2260
2545
|
await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }], this.eventScopeForSession(session));
|
|
2261
2546
|
}
|
|
2262
2547
|
async abort(ref, managementContext) {
|
|
2263
2548
|
const active = this.activeForLookup(ref, managementContext);
|
|
2264
2549
|
if (active === undefined)
|
|
2265
2550
|
return;
|
|
2266
|
-
|
|
2267
|
-
this.clearCompactionPromptQueue(sessionId);
|
|
2551
|
+
this.clearCompactionPromptQueue(active.runtime.session);
|
|
2268
2552
|
clearSessionQueue(active.runtime.session);
|
|
2269
2553
|
this.abortRunScopedExtensionDialogs(active.runtime.session);
|
|
2270
2554
|
try {
|
|
@@ -2283,25 +2567,15 @@ export class PiSessionService {
|
|
|
2283
2567
|
async clearQueue(ref, managementContext) {
|
|
2284
2568
|
await this.assertWritable(ref);
|
|
2285
2569
|
const session = await this.getOrOpen(ref, managementContext);
|
|
2286
|
-
this.clearCompactionPromptQueue(session
|
|
2570
|
+
this.clearCompactionPromptQueue(session);
|
|
2287
2571
|
clearSessionQueue(session);
|
|
2288
2572
|
this.publishStatus(session);
|
|
2289
2573
|
return this.statusFromSession(session);
|
|
2290
2574
|
}
|
|
2291
2575
|
async stop(ref, managementContext) {
|
|
2292
|
-
|
|
2293
|
-
if (
|
|
2294
|
-
|
|
2295
|
-
if (startup !== undefined) {
|
|
2296
|
-
this.endSessionExtensionDialogs(startup);
|
|
2297
|
-
const pendingOpens = this.pendingSessionOpenPromises(startup.sessionId);
|
|
2298
|
-
if (pendingOpens.length > 0)
|
|
2299
|
-
await Promise.allSettled(pendingOpens);
|
|
2300
|
-
active = this.activeForLookup(ref, managementContext);
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
if (active !== undefined) {
|
|
2304
|
-
await this.closeActiveSession(active);
|
|
2576
|
+
const session = this.activeForLookup(ref, managementContext)?.runtime.session ?? this.startupSessionForLookup(ref, managementContext);
|
|
2577
|
+
if (session !== undefined) {
|
|
2578
|
+
await this.closeActive(session.sessionId, CLEAR_RUNTIME_NOTIFICATIONS, eventScopeFromManagementContext(managementContext));
|
|
2305
2579
|
return;
|
|
2306
2580
|
}
|
|
2307
2581
|
if (isPiSessionRef(ref)) {
|
|
@@ -2319,10 +2593,11 @@ export class PiSessionService {
|
|
|
2319
2593
|
async archiveStoreArchiveMany(inputs) {
|
|
2320
2594
|
if (inputs.length === 0)
|
|
2321
2595
|
return [];
|
|
2596
|
+
const oldestFirst = [...inputs].sort((a, b) => Date.parse(a.modified) - Date.parse(b.modified));
|
|
2322
2597
|
if (this.archiveStore.archiveMany !== undefined)
|
|
2323
|
-
return this.archiveStore.archiveMany(
|
|
2598
|
+
return this.archiveStore.archiveMany(oldestFirst);
|
|
2324
2599
|
const records = [];
|
|
2325
|
-
for (const input of
|
|
2600
|
+
for (const input of oldestFirst)
|
|
2326
2601
|
records.push(await this.archiveStore.archive(input));
|
|
2327
2602
|
return records;
|
|
2328
2603
|
}
|
|
@@ -2337,38 +2612,16 @@ export class PiSessionService {
|
|
|
2337
2612
|
await this.archiveStore.deleteArchived(sessionId);
|
|
2338
2613
|
return [...sessionIds];
|
|
2339
2614
|
}
|
|
2340
|
-
async moveLegacyArchivedRecordsForDelete(records) {
|
|
2341
|
-
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
2342
|
-
if (legacyRecords.length === 0)
|
|
2343
|
-
return [];
|
|
2344
|
-
let sessionsByCwd;
|
|
2345
|
-
try {
|
|
2346
|
-
sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
2347
|
-
}
|
|
2348
|
-
catch (error) {
|
|
2349
|
-
return legacyRecords.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
2350
|
-
}
|
|
2351
|
-
const moveInputs = legacyRecords
|
|
2352
|
-
.map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
|
|
2353
|
-
.filter(isDefined)
|
|
2354
|
-
.map(archiveInputFromListEntry);
|
|
2355
|
-
if (moveInputs.length === 0)
|
|
2356
|
-
return [];
|
|
2357
|
-
try {
|
|
2358
|
-
await this.archiveStoreArchiveMany(moveInputs);
|
|
2359
|
-
return [];
|
|
2360
|
-
}
|
|
2361
|
-
catch (error) {
|
|
2362
|
-
const failedIds = new Set(moveInputs.map((input) => input.sessionId));
|
|
2363
|
-
return legacyRecords
|
|
2364
|
-
.filter((record) => failedIds.has(record.sessionId))
|
|
2365
|
-
.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
2615
|
async cleanupPlan(request) {
|
|
2369
|
-
const [sessions, archivedRecords] = await Promise.all([
|
|
2616
|
+
const [sessions, archivedRecords, scopedSessions] = await Promise.all([
|
|
2617
|
+
this.sessionManager.listAll(),
|
|
2618
|
+
this.archiveStore.list(),
|
|
2619
|
+
request.thresholds.archiveIdleDays === undefined
|
|
2620
|
+
? Promise.resolve(new Map())
|
|
2621
|
+
: this.listSessionsByCwd(request.projectCwds ?? []),
|
|
2622
|
+
]);
|
|
2370
2623
|
return planSessionCleanup({
|
|
2371
|
-
sessions,
|
|
2624
|
+
sessions: [...sessions, ...[...scopedSessions.values()].flat()],
|
|
2372
2625
|
archivedRecords,
|
|
2373
2626
|
activeSessions: this.cleanupActiveSessionStatuses(),
|
|
2374
2627
|
thresholds: request.thresholds,
|
|
@@ -2404,24 +2657,6 @@ export class PiSessionService {
|
|
|
2404
2657
|
return record;
|
|
2405
2658
|
}
|
|
2406
2659
|
}
|
|
2407
|
-
async ensureArchivedRecordMoved(record) {
|
|
2408
|
-
const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
|
|
2409
|
-
if (session === undefined)
|
|
2410
|
-
return record;
|
|
2411
|
-
const [moved] = await this.archiveStoreArchiveMany([archiveInputFromListEntry(session)]);
|
|
2412
|
-
return moved ?? record;
|
|
2413
|
-
}
|
|
2414
|
-
async ensureArchivedRecordsMoved(records) {
|
|
2415
|
-
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
2416
|
-
if (legacyRecords.length === 0)
|
|
2417
|
-
return;
|
|
2418
|
-
const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
2419
|
-
const moveInputs = legacyRecords
|
|
2420
|
-
.map((record) => sessionsByCwd.get(record.cwd)?.find((candidate) => candidate.id === record.sessionId))
|
|
2421
|
-
.filter(isDefined)
|
|
2422
|
-
.map(archiveInputFromListEntry);
|
|
2423
|
-
await this.archiveStoreArchiveMany(moveInputs);
|
|
2424
|
-
}
|
|
2425
2660
|
async archiveInputForSession(session) {
|
|
2426
2661
|
const cwd = session.sessionManager.getCwd();
|
|
2427
2662
|
const sessionFile = session.sessionFile;
|
|
@@ -2487,7 +2722,7 @@ export class PiSessionService {
|
|
|
2487
2722
|
if (startup.session.sessionId === sessionId && startup.eventScope === eventScope)
|
|
2488
2723
|
this.endSessionExtensionDialogs(startup.session);
|
|
2489
2724
|
}
|
|
2490
|
-
const pendingOpens = this.pendingSessionOpenPromises(sessionId);
|
|
2725
|
+
const pendingOpens = this.pendingSessionOpenPromises(sessionId, eventScope);
|
|
2491
2726
|
if (pendingOpens.length > 0)
|
|
2492
2727
|
await Promise.allSettled(pendingOpens);
|
|
2493
2728
|
const activeSessions = this.activeSessionsForId(sessionId).filter((active) => active.eventScope === eventScope);
|
|
@@ -2498,13 +2733,7 @@ export class PiSessionService {
|
|
|
2498
2733
|
await Promise.all(activeSessions.map((active) => this.closeActiveSession(active, notificationPolicy)));
|
|
2499
2734
|
}
|
|
2500
2735
|
async closeActiveInScope(sessionId, eventScope, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
|
|
2501
|
-
|
|
2502
|
-
if (active !== undefined) {
|
|
2503
|
-
await this.closeActiveSession(active, notificationPolicy);
|
|
2504
|
-
}
|
|
2505
|
-
else if (notificationPolicy.kind === "clear") {
|
|
2506
|
-
this.publishNotificationMutations(this.notificationsForScope(eventScope).clearSession(sessionId, notificationPolicy.reason));
|
|
2507
|
-
}
|
|
2736
|
+
await this.closeActive(sessionId, notificationPolicy, eventScope);
|
|
2508
2737
|
}
|
|
2509
2738
|
async closeActiveSession(active, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
|
|
2510
2739
|
const sessionId = active.runtime.session.sessionId;
|
|
@@ -2517,18 +2746,18 @@ export class PiSessionService {
|
|
|
2517
2746
|
this.publishNotificationMutations(mutations);
|
|
2518
2747
|
}
|
|
2519
2748
|
this.forgetUnreadActivity(active.runtime.session);
|
|
2520
|
-
this.pendingAskStore.forgetSession(
|
|
2749
|
+
this.pendingAskStore.forgetSession(this.dialogSessionKey(active.runtime.session));
|
|
2521
2750
|
this.endSessionExtensionDialogs(active.runtime.session);
|
|
2522
2751
|
this.active.delete(activeSessionKey(sessionId, active.eventScope));
|
|
2523
2752
|
this.activities.delete(activeSessionKey(sessionId, active.eventScope));
|
|
2524
2753
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
|
|
2525
2754
|
this.clearAuthLossWarningsForSession(sessionId);
|
|
2526
|
-
this.clearCompactionPromptQueue(
|
|
2755
|
+
this.clearCompactionPromptQueue(active.runtime.session);
|
|
2527
2756
|
// Disarm subsession notification before teardown so the abort below cannot
|
|
2528
2757
|
// emit a "stopped working" event that notifies the parent (e.g. on archive).
|
|
2529
2758
|
// The parent/children link is kept so the parent can still see the child.
|
|
2530
2759
|
if (this.subsessionLinkForActiveChild(active.runtime.session) !== undefined)
|
|
2531
|
-
this.subsessionNotifyArmed.delete(sessionId);
|
|
2760
|
+
this.subsessionNotifyArmed.delete(activeSessionKey(sessionId, active.eventScope));
|
|
2532
2761
|
clearSessionQueue(active.runtime.session);
|
|
2533
2762
|
active.unsubscribe();
|
|
2534
2763
|
active.runtime.setRebindSession(undefined);
|
|
@@ -2536,6 +2765,8 @@ export class PiSessionService {
|
|
|
2536
2765
|
await this.abortSessionOperations(active.runtime.session);
|
|
2537
2766
|
}
|
|
2538
2767
|
finally {
|
|
2768
|
+
if (active.eventScope === NORMAL_SESSION_EVENT_SCOPE)
|
|
2769
|
+
await this.activityMarker.release(active.runtime.session.sessionFile);
|
|
2539
2770
|
await active.runtime.dispose();
|
|
2540
2771
|
}
|
|
2541
2772
|
}
|
|
@@ -2594,7 +2825,9 @@ export class PiSessionService {
|
|
|
2594
2825
|
const leafId = session.sessionManager.getLeafId();
|
|
2595
2826
|
const latestEntry = session.sessionManager.getEntries?.().at(-1);
|
|
2596
2827
|
// Unsummarized tree navigation changes only the SDK's in-memory leaf.
|
|
2597
|
-
if (
|
|
2828
|
+
if (!this.unpersistedTreeBranchLeaves.has(session)
|
|
2829
|
+
&& isRecord(latestEntry) && typeof latestEntry["id"] === "string"
|
|
2830
|
+
&& latestEntry["id"] !== leafId)
|
|
2598
2831
|
return session.sessionManager.getBranch();
|
|
2599
2832
|
const sessionFile = session.sessionFile ?? session.sessionManager.getSessionFile();
|
|
2600
2833
|
const resolvedPath = sessionFile === undefined ? await this.idleSessionFilePath(session) : undefined;
|
|
@@ -2606,19 +2839,37 @@ export class PiSessionService {
|
|
|
2606
2839
|
const snapshot = await this.sessionManager.readBranch(path);
|
|
2607
2840
|
if (snapshot === undefined)
|
|
2608
2841
|
return session.sessionManager.getBranch();
|
|
2609
|
-
|
|
2842
|
+
if (this.hasActiveWork(session) || session.sessionManager.getLeafId() !== leafId)
|
|
2843
|
+
return session.sessionManager.getBranch();
|
|
2844
|
+
const selectedLeafId = this.unpersistedTreeBranchLeaves.get(session);
|
|
2845
|
+
if (selectedLeafId !== undefined) {
|
|
2846
|
+
const runtimeLeafId = session.sessionManager.getLeafId();
|
|
2847
|
+
const selectedBranchIsAnchored = runtimeLeafId !== null
|
|
2848
|
+
&& runtimeLeafId !== selectedLeafId && transcriptBranchIncludesEntry(snapshot, runtimeLeafId);
|
|
2849
|
+
if (!selectedBranchIsAnchored)
|
|
2850
|
+
return session.sessionManager.getBranch();
|
|
2851
|
+
this.unpersistedTreeBranchLeaves.delete(session);
|
|
2852
|
+
}
|
|
2853
|
+
return snapshot;
|
|
2610
2854
|
}
|
|
2611
2855
|
async idleSessionFilePath(session) {
|
|
2612
2856
|
const cached = this.idleSessionFileResolutions.get(session);
|
|
2613
2857
|
const at = this.now().getTime();
|
|
2614
2858
|
if (cached !== undefined && at - cached.at < IDLE_SESSION_FILE_RESOLUTION_THROTTLE_MS)
|
|
2615
2859
|
return cached.path;
|
|
2616
|
-
const match = await this.sessionManager.resolveSessionFile
|
|
2860
|
+
const match = await this.sessionManager.resolveSessionFile(session.sessionManager.getCwd(), session.sessionId);
|
|
2617
2861
|
this.idleSessionFileResolutions.set(session, { at, path: match?.path });
|
|
2618
2862
|
return match?.path;
|
|
2619
2863
|
}
|
|
2620
2864
|
async getActive(ref, managementContext) {
|
|
2621
2865
|
const active = this.activeForLookup(ref, managementContext);
|
|
2866
|
+
const resolvedRef = active === undefined ? isPiSessionRef(ref) ? ref : undefined
|
|
2867
|
+
: { id: active.runtime.session.sessionId, cwd: active.runtime.cwd };
|
|
2868
|
+
const pending = resolvedRef === undefined ? undefined : this.pendingSessionOpens.get(JSON.stringify([
|
|
2869
|
+
canonicalizeStoredCwd(resolvedRef.cwd), resolvedRef.id, managementContextKey(managementContext),
|
|
2870
|
+
]));
|
|
2871
|
+
if (pending !== undefined)
|
|
2872
|
+
return pending.promise;
|
|
2622
2873
|
if (active !== undefined)
|
|
2623
2874
|
return active;
|
|
2624
2875
|
const archived = await this.getArchived(ref);
|
|
@@ -2627,10 +2878,8 @@ export class PiSessionService {
|
|
|
2627
2878
|
return this.openPending(archived.sessionId, archived.cwd, managementContext, () => this.sessionManager.open(archivePath), { notifications: "disabled" });
|
|
2628
2879
|
}
|
|
2629
2880
|
const match = isPiSessionRef(ref)
|
|
2630
|
-
? this.sessionManager.resolveSessionFile
|
|
2631
|
-
|
|
2632
|
-
: await this.sessionManager.resolveSessionFile(ref.cwd, ref.id)
|
|
2633
|
-
: findSessionByIdOrPrefix(await this.sessionManager.listAll?.() ?? [], ref);
|
|
2881
|
+
? await this.sessionManager.resolveSessionFile(ref.cwd, ref.id)
|
|
2882
|
+
: findSessionByIdOrPrefix(await this.sessionManager.listAll(), ref);
|
|
2634
2883
|
if (!match)
|
|
2635
2884
|
throw new Error("未找到会话");
|
|
2636
2885
|
return this.openPending(match.id, match.cwd, managementContext, () => this.sessionManager.open(match.path));
|
|
@@ -2640,8 +2889,12 @@ export class PiSessionService {
|
|
|
2640
2889
|
const existing = this.pendingSessionOpens.get(key);
|
|
2641
2890
|
if (existing !== undefined)
|
|
2642
2891
|
return existing.promise;
|
|
2892
|
+
const active = this.activeForLookup({ id: sessionId, cwd }, managementContext);
|
|
2893
|
+
if (active !== undefined)
|
|
2894
|
+
return Promise.resolve(active);
|
|
2643
2895
|
const pending = {
|
|
2644
2896
|
sessionId,
|
|
2897
|
+
eventScope: eventScopeFromManagementContext(managementContext),
|
|
2645
2898
|
promise: this.create(openSessionManager(), cwd, {
|
|
2646
2899
|
...options,
|
|
2647
2900
|
...(managementContext === undefined ? {} : { managementContext }),
|
|
@@ -2654,9 +2907,9 @@ export class PiSessionService {
|
|
|
2654
2907
|
this.pendingSessionOpens.set(key, pending);
|
|
2655
2908
|
return pending.promise;
|
|
2656
2909
|
}
|
|
2657
|
-
pendingSessionOpenPromises(sessionId) {
|
|
2910
|
+
pendingSessionOpenPromises(sessionId, eventScope) {
|
|
2658
2911
|
return [...this.pendingSessionOpens.values()]
|
|
2659
|
-
.filter((pending) => sessionId === undefined || pending.sessionId === sessionId)
|
|
2912
|
+
.filter((pending) => (sessionId === undefined || pending.sessionId === sessionId) && (eventScope === undefined || pending.eventScope === eventScope))
|
|
2660
2913
|
.map((pending) => pending.promise);
|
|
2661
2914
|
}
|
|
2662
2915
|
async getArchived(ref) {
|
|
@@ -2705,6 +2958,21 @@ export class PiSessionService {
|
|
|
2705
2958
|
throw new Error(`Session not active: ${sessionId}`);
|
|
2706
2959
|
return this.submitPrompt(active.runtime.session, text, undefined, [], false);
|
|
2707
2960
|
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Attach to one already-hosted normal-mode session. Management scopes are
|
|
2963
|
+
* not searched: host session events are a normal-mode capability.
|
|
2964
|
+
*/
|
|
2965
|
+
connectSessionEvents(ref, lifetime) {
|
|
2966
|
+
const active = this.activeForSessionIdAndScope(ref.id, NORMAL_SESSION_EVENT_SCOPE);
|
|
2967
|
+
if (active?.runtime.session.sessionId !== ref.id
|
|
2968
|
+
|| !cwdPathsEqual(active.runtime.session.sessionManager.getCwd(), ref.cwd)) {
|
|
2969
|
+
throw new Error("Selected session is not hosted in this workspace on this machine");
|
|
2970
|
+
}
|
|
2971
|
+
if (this.startupSessions.has(activeSessionKey(ref.id, NORMAL_SESSION_EVENT_SCOPE))) {
|
|
2972
|
+
throw new Error("Selected session is still initializing its extensions");
|
|
2973
|
+
}
|
|
2974
|
+
return this.sessionEvents.connect(active.runtime.session, lifetime);
|
|
2975
|
+
}
|
|
2708
2976
|
activeForSessionId(sessionId) {
|
|
2709
2977
|
return this.activeSessionsForId(sessionId)[0];
|
|
2710
2978
|
}
|
|
@@ -2716,17 +2984,25 @@ export class PiSessionService {
|
|
|
2716
2984
|
activeSessionsForId(sessionId) {
|
|
2717
2985
|
return [...new Set(this.active.values())].filter((active) => active.runtime.session.sessionId === sessionId);
|
|
2718
2986
|
}
|
|
2719
|
-
|
|
2987
|
+
create(sessionManager, cwd, options = {}) {
|
|
2720
2988
|
const startup = this.startupProgress(sessionManager, options.startupIntent ?? "open", options.startupToken, eventScopeFromManagementContext(options.managementContext));
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2989
|
+
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionManager.getSessionId(), managementContextKey(options.managementContext)]);
|
|
2990
|
+
const pending = {
|
|
2991
|
+
sessionId: sessionManager.getSessionId(),
|
|
2992
|
+
eventScope: eventScopeFromManagementContext(options.managementContext),
|
|
2993
|
+
promise: this.createSessionRuntime(sessionManager, cwd, options, startup),
|
|
2994
|
+
};
|
|
2995
|
+
pending.promise = pending.promise.finally(() => {
|
|
2725
2996
|
startup.end();
|
|
2726
|
-
|
|
2997
|
+
if (this.pendingSessionOpens.get(key) === pending)
|
|
2998
|
+
this.pendingSessionOpens.delete(key);
|
|
2999
|
+
});
|
|
3000
|
+
this.pendingSessionOpens.set(key, pending);
|
|
3001
|
+
return pending.promise;
|
|
2727
3002
|
}
|
|
2728
|
-
async bindSessionExtensions(session, generation, owner) {
|
|
2729
|
-
const
|
|
3003
|
+
async bindSessionExtensions(session, generation, owner, startupSignal) {
|
|
3004
|
+
const startupDialogs = { signal: startupSignal };
|
|
3005
|
+
const uiContext = this.sessionUiContext(session, generation, startupDialogs);
|
|
2730
3006
|
const key = activeSessionKey(session.sessionId, owner.eventScope);
|
|
2731
3007
|
this.startupSessions.set(key, { session, ...owner });
|
|
2732
3008
|
try {
|
|
@@ -2741,6 +3017,9 @@ export class PiSessionService {
|
|
|
2741
3017
|
});
|
|
2742
3018
|
}
|
|
2743
3019
|
finally {
|
|
3020
|
+
// A captured startup UI context must not carry plugin cancellation into
|
|
3021
|
+
// later user turns after startup has finished.
|
|
3022
|
+
startupDialogs.signal = undefined;
|
|
2744
3023
|
if (this.startupSessions.get(key)?.session === session)
|
|
2745
3024
|
this.startupSessions.delete(key);
|
|
2746
3025
|
}
|
|
@@ -2749,50 +3028,66 @@ export class PiSessionService {
|
|
|
2749
3028
|
this.sessionEventScopes.set(session, active.eventScope);
|
|
2750
3029
|
this.runtimeBySession.set(session, active.runtime);
|
|
2751
3030
|
active.unsubscribe();
|
|
2752
|
-
for (const [
|
|
3031
|
+
for (const [sessionKey, candidate] of this.active.entries()) {
|
|
2753
3032
|
if (candidate === active) {
|
|
2754
|
-
this.active.delete(
|
|
2755
|
-
if (
|
|
2756
|
-
this.
|
|
3033
|
+
this.active.delete(sessionKey);
|
|
3034
|
+
if (sessionKey !== activeSessionKey(session.sessionId, active.eventScope))
|
|
3035
|
+
this.clearCompactionPromptQueueKey(sessionKey);
|
|
2757
3036
|
}
|
|
2758
3037
|
}
|
|
2759
|
-
active.
|
|
3038
|
+
if (active.eventScope === NORMAL_SESSION_EVENT_SCOPE && active.runtime.session !== session) {
|
|
3039
|
+
this.sessionEvents.close(active.runtime.session);
|
|
3040
|
+
}
|
|
3041
|
+
const unsubscribe = session.subscribe((event) => {
|
|
2760
3042
|
this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel), active.eventScope);
|
|
2761
3043
|
this.auditToolExecutionEvent(active, event);
|
|
2762
3044
|
this.auditAssistantResponseEvent(active, event);
|
|
2763
3045
|
this.publishActivityForEvent(session, event);
|
|
2764
3046
|
const eventType = getString(event, "type");
|
|
3047
|
+
if (eventType === "message_start" && isRecord(event) && getString(event["message"], "role") === "user") {
|
|
3048
|
+
void this.voidOpenAskForUserMessage(session).catch((error) => {
|
|
3049
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3050
|
+
this.publishActivity(session, "扩展错误", "error", message);
|
|
3051
|
+
this.events.publish(session.sessionId, { type: "session.error", message }, active.eventScope);
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
2765
3054
|
if (eventType === "agent_end")
|
|
2766
3055
|
this.abortRunScopedExtensionDialogs(session);
|
|
2767
3056
|
if (eventType === "compaction_end")
|
|
2768
|
-
this.scheduleCompactionQueueDrain(session
|
|
3057
|
+
this.scheduleCompactionQueueDrain(session);
|
|
2769
3058
|
if (eventType === "agent_start" || eventType === "agent_end")
|
|
2770
|
-
this.scheduleCompactionQueueDrain(session
|
|
3059
|
+
this.scheduleCompactionQueueDrain(session);
|
|
2771
3060
|
this.publishStatus(session);
|
|
2772
3061
|
this.updateSubsessionTracking(session);
|
|
2773
3062
|
});
|
|
3063
|
+
active.unsubscribe = () => {
|
|
3064
|
+
if (active.eventScope === NORMAL_SESSION_EVENT_SCOPE)
|
|
3065
|
+
this.sessionEvents.close(session);
|
|
3066
|
+
unsubscribe();
|
|
3067
|
+
};
|
|
2774
3068
|
this.active.set(activeSessionKey(session.sessionId, active.eventScope), active);
|
|
2775
3069
|
}
|
|
2776
|
-
scheduleCompactionQueueDrain(
|
|
2777
|
-
|
|
3070
|
+
scheduleCompactionQueueDrain(session, delayMs = 0) {
|
|
3071
|
+
const sessionKey = activeSessionKey(session.sessionId, this.eventScopeForSession(session));
|
|
3072
|
+
if (!this.isCurrentActiveSession(session)
|
|
3073
|
+
|| !this.compactionPromptQueues.has(sessionKey) || this.compactionDrainTimers.has(sessionKey))
|
|
2778
3074
|
return;
|
|
2779
3075
|
const timer = setTimeout(() => {
|
|
2780
|
-
this.compactionDrainTimers.delete(
|
|
2781
|
-
this.drainCompactionPromptQueue(
|
|
3076
|
+
this.compactionDrainTimers.delete(sessionKey);
|
|
3077
|
+
this.drainCompactionPromptQueue(session);
|
|
2782
3078
|
}, delayMs);
|
|
2783
|
-
this.compactionDrainTimers.set(
|
|
3079
|
+
this.compactionDrainTimers.set(sessionKey, timer);
|
|
2784
3080
|
}
|
|
2785
|
-
drainCompactionPromptQueue(
|
|
2786
|
-
|
|
2787
|
-
if (active === undefined)
|
|
3081
|
+
drainCompactionPromptQueue(session) {
|
|
3082
|
+
if (!this.isCurrentActiveSession(session))
|
|
2788
3083
|
return;
|
|
2789
|
-
const
|
|
3084
|
+
const sessionKey = activeSessionKey(session.sessionId, this.eventScopeForSession(session));
|
|
2790
3085
|
if (session.isCompacting) {
|
|
2791
|
-
this.scheduleCompactionQueueDrain(
|
|
3086
|
+
this.scheduleCompactionQueueDrain(session, 100);
|
|
2792
3087
|
return;
|
|
2793
3088
|
}
|
|
2794
3089
|
if (session.isStreaming) {
|
|
2795
|
-
const queued = this.takeCompactionPromptQueue(
|
|
3090
|
+
const queued = this.takeCompactionPromptQueue(sessionKey);
|
|
2796
3091
|
if (queued.length === 0)
|
|
2797
3092
|
return;
|
|
2798
3093
|
this.publishStatus(session);
|
|
@@ -2800,12 +3095,12 @@ export class PiSessionService {
|
|
|
2800
3095
|
void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images, prompt.echoUserMessage ?? true);
|
|
2801
3096
|
return;
|
|
2802
3097
|
}
|
|
2803
|
-
const prompt = this.shiftCompactionPrompt(
|
|
3098
|
+
const prompt = this.shiftCompactionPrompt(sessionKey);
|
|
2804
3099
|
if (prompt === undefined)
|
|
2805
3100
|
return;
|
|
2806
3101
|
this.publishStatus(session);
|
|
2807
3102
|
const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images, prompt.echoUserMessage ?? true);
|
|
2808
|
-
void submitted.finally(() => { this.scheduleCompactionQueueDrain(
|
|
3103
|
+
void submitted.finally(() => { this.scheduleCompactionQueueDrain(session); });
|
|
2809
3104
|
}
|
|
2810
3105
|
takeCompactionPromptQueue(sessionId) {
|
|
2811
3106
|
const queued = this.compactionPromptQueues.get(sessionId) ?? [];
|
|
@@ -2819,12 +3114,15 @@ export class PiSessionService {
|
|
|
2819
3114
|
this.compactionPromptQueues.delete(sessionId);
|
|
2820
3115
|
return prompt;
|
|
2821
3116
|
}
|
|
2822
|
-
clearCompactionPromptQueue(
|
|
2823
|
-
this.
|
|
2824
|
-
|
|
3117
|
+
clearCompactionPromptQueue(session) {
|
|
3118
|
+
this.clearCompactionPromptQueueKey(activeSessionKey(session.sessionId, this.eventScopeForSession(session)));
|
|
3119
|
+
}
|
|
3120
|
+
clearCompactionPromptQueueKey(sessionKey) {
|
|
3121
|
+
this.compactionPromptQueues.delete(sessionKey);
|
|
3122
|
+
const timer = this.compactionDrainTimers.get(sessionKey);
|
|
2825
3123
|
if (timer !== undefined) {
|
|
2826
3124
|
clearTimeout(timer);
|
|
2827
|
-
this.compactionDrainTimers.delete(
|
|
3125
|
+
this.compactionDrainTimers.delete(sessionKey);
|
|
2828
3126
|
}
|
|
2829
3127
|
}
|
|
2830
3128
|
clearCompactionDrainTimers() {
|
|
@@ -2913,6 +3211,7 @@ export class PiSessionService {
|
|
|
2913
3211
|
// the session still reports active work transiently, so the event-driven
|
|
2914
3212
|
// latch may not fire. The heartbeat re-checks once the session settles.
|
|
2915
3213
|
this.updateSubsessionTracking(session);
|
|
3214
|
+
void this.refreshActivityMarker(session);
|
|
2916
3215
|
const activity = this.activities.get(activeSessionKey(session.sessionId, active.eventScope));
|
|
2917
3216
|
if (!this.hasActiveWork(session)) {
|
|
2918
3217
|
if (activity?.phase === "active")
|
|
@@ -2945,7 +3244,7 @@ export class PiSessionService {
|
|
|
2945
3244
|
return this.treeNavigations.has(session)
|
|
2946
3245
|
|| this.isSessionEntryMutationActive(session)
|
|
2947
3246
|
|| this.isTreeExclusiveOperationActive(session)
|
|
2948
|
-
|| sessionHasActiveWork(session, this.compactionQueuedMessages(session
|
|
3247
|
+
|| sessionHasActiveWork(session, this.compactionQueuedMessages(session).length);
|
|
2949
3248
|
}
|
|
2950
3249
|
async runTreeExclusiveOperation(targets, activeError, operation) {
|
|
2951
3250
|
const sessionIds = new Set();
|
|
@@ -3005,7 +3304,7 @@ export class PiSessionService {
|
|
|
3005
3304
|
}
|
|
3006
3305
|
assertTreeNavigationInactive(session, action) {
|
|
3007
3306
|
if (this.treeNavigations.has(session))
|
|
3008
|
-
throw new Error(
|
|
3307
|
+
throw new Error(`会话树导航期间无法${action}`);
|
|
3009
3308
|
}
|
|
3010
3309
|
async runSessionEntryMutation(session, action, operation) {
|
|
3011
3310
|
this.beginSessionEntryMutation(session, action);
|
|
@@ -3139,16 +3438,16 @@ export class PiSessionService {
|
|
|
3139
3438
|
const message = getProperty(event, "message");
|
|
3140
3439
|
if (getString(message, "role") !== "assistant")
|
|
3141
3440
|
return;
|
|
3142
|
-
this.
|
|
3441
|
+
this.recordManagementMessageAudit(active, "assistant_response", auditVisibleAssistantMessage(message));
|
|
3143
3442
|
}
|
|
3144
3443
|
auditUserPrompt(session, text) {
|
|
3145
3444
|
const active = [...new Set(this.active.values())]
|
|
3146
3445
|
.find(candidate => candidate.runtime.session === session);
|
|
3147
3446
|
if (active === undefined)
|
|
3148
3447
|
return;
|
|
3149
|
-
this.
|
|
3448
|
+
this.recordManagementMessageAudit(active, "user_prompt", text);
|
|
3150
3449
|
}
|
|
3151
|
-
|
|
3450
|
+
recordManagementMessageAudit(active, action, content) {
|
|
3152
3451
|
const identity = active.managementAuditIdentity;
|
|
3153
3452
|
if (identity === undefined || this.managementAudit === undefined)
|
|
3154
3453
|
return;
|
|
@@ -3167,7 +3466,7 @@ export class PiSessionService {
|
|
|
3167
3466
|
sessionId: active.runtime.session.sessionId,
|
|
3168
3467
|
action,
|
|
3169
3468
|
error: error instanceof Error ? error.message : String(error),
|
|
3170
|
-
}, "failed to enqueue management-mode
|
|
3469
|
+
}, "failed to enqueue management-mode message audit");
|
|
3171
3470
|
}
|
|
3172
3471
|
}
|
|
3173
3472
|
/**
|
|
@@ -3229,7 +3528,20 @@ export class PiSessionService {
|
|
|
3229
3528
|
this.events.publishGlobal({ type: "activity.update", activity }, eventScope);
|
|
3230
3529
|
this.observeUnreadActivityState(session);
|
|
3231
3530
|
}
|
|
3531
|
+
async refreshActivityMarker(session) {
|
|
3532
|
+
const eventScope = this.eventScopeForSession(session);
|
|
3533
|
+
if (eventScope !== NORMAL_SESSION_EVENT_SCOPE
|
|
3534
|
+
|| this.activeForSessionIdAndScope(session.sessionId, eventScope)?.runtime.session !== session)
|
|
3535
|
+
return;
|
|
3536
|
+
const previous = this.activityMarker.isActiveElsewhere(session.sessionFile);
|
|
3537
|
+
await this.activityMarker.refresh(session.sessionFile, this.hasActiveWork(session));
|
|
3538
|
+
if (this.activeForSessionIdAndScope(session.sessionId, eventScope)?.runtime.session === session
|
|
3539
|
+
&& previous !== this.activityMarker.isActiveElsewhere(session.sessionFile)) {
|
|
3540
|
+
this.publishStatus(session);
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3232
3543
|
publishStatus(session) {
|
|
3544
|
+
void this.refreshActivityMarker(session);
|
|
3233
3545
|
const status = this.statusFromSession(session);
|
|
3234
3546
|
this.clearStaleActiveActivity(session);
|
|
3235
3547
|
const eventScope = this.eventScopeForSession(session);
|
|
@@ -3271,7 +3583,7 @@ export class PiSessionService {
|
|
|
3271
3583
|
const eventScope = this.eventScopeForSession(session);
|
|
3272
3584
|
return eventScope === NORMAL_SESSION_EVENT_SCOPE ? session.sessionId : activeSessionKey(session.sessionId, eventScope);
|
|
3273
3585
|
}
|
|
3274
|
-
publishStatusForSessionId(sessionId) {
|
|
3586
|
+
publishStatusForSessionId(sessionId, scope) {
|
|
3275
3587
|
const sessions = new Set();
|
|
3276
3588
|
for (const active of this.activeSessionsForId(sessionId))
|
|
3277
3589
|
sessions.add(active.runtime.session);
|
|
@@ -3279,15 +3591,17 @@ export class PiSessionService {
|
|
|
3279
3591
|
if (startup.session.sessionId === sessionId)
|
|
3280
3592
|
sessions.add(startup.session);
|
|
3281
3593
|
}
|
|
3282
|
-
for (const session of sessions)
|
|
3283
|
-
this.
|
|
3594
|
+
for (const session of sessions) {
|
|
3595
|
+
if (scope === undefined || this.eventScopeForSession(session) === scope)
|
|
3596
|
+
this.publishStatus(session);
|
|
3597
|
+
}
|
|
3284
3598
|
}
|
|
3285
3599
|
statusFromSession(session, messageCount = session.messages.length) {
|
|
3286
3600
|
const stats = session.getSessionStats();
|
|
3287
3601
|
const model = session.model === undefined ? undefined : modelToClientModel(session.model);
|
|
3288
3602
|
const contextUsage = session.getContextUsage();
|
|
3289
3603
|
const warnings = this.warningsForSession(session);
|
|
3290
|
-
const pendingAsk = this.pendingAskStore.pendingAsk(session
|
|
3604
|
+
const pendingAsk = this.pendingAskStore.pendingAsk(this.dialogSessionKey(session));
|
|
3291
3605
|
const pendingDialogs = this.pendingExtensionDialogStore.pendingDialogs(this.dialogSessionKey(session));
|
|
3292
3606
|
return {
|
|
3293
3607
|
sessionId: session.sessionId,
|
|
@@ -3298,7 +3612,7 @@ export class PiSessionService {
|
|
|
3298
3612
|
isCompacting: session.isCompacting,
|
|
3299
3613
|
isBashRunning: session.isBashRunning,
|
|
3300
3614
|
pendingMessageCount: this.pendingMessageCount(session),
|
|
3301
|
-
queuedMessages: queuedMessagesFromSession(session, this.compactionQueuedMessages(session
|
|
3615
|
+
queuedMessages: queuedMessagesFromSession(session, this.compactionQueuedMessages(session)),
|
|
3302
3616
|
messageCount,
|
|
3303
3617
|
tokens: stats.tokens,
|
|
3304
3618
|
cost: stats.cost,
|
|
@@ -3317,19 +3631,28 @@ export class PiSessionService {
|
|
|
3317
3631
|
warningsForSession(session) {
|
|
3318
3632
|
const runtime = [...this.active.values()].find((active) => active.runtime.session === session)?.runtime;
|
|
3319
3633
|
const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
|
|
3320
|
-
|
|
3634
|
+
if (this.eventScopeForSession(session) === NORMAL_SESSION_EVENT_SCOPE
|
|
3635
|
+
&& this.activityMarker.isActiveElsewhere(session.sessionFile)) {
|
|
3636
|
+
warnings.push({
|
|
3637
|
+
severity: "info",
|
|
3638
|
+
message: "此会话最近在另一个 PI WEB 实例中活动。请避免在两个实例中同时操作此会话。",
|
|
3639
|
+
source: "PI-WEB",
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
const authPath = modelRuntimeAuthPath(session.modelRuntime);
|
|
3643
|
+
const anthropic = authPath === undefined ? undefined : anthropicSubscriptionWarning(session, authPath);
|
|
3321
3644
|
if (anthropic !== undefined)
|
|
3322
3645
|
warnings.push(anthropic);
|
|
3323
3646
|
return warnings;
|
|
3324
3647
|
}
|
|
3325
3648
|
pendingMessageCount(session) {
|
|
3326
|
-
return session.pendingMessageCount + this.compactionQueuedMessages(session
|
|
3649
|
+
return session.pendingMessageCount + this.compactionQueuedMessages(session).length;
|
|
3327
3650
|
}
|
|
3328
|
-
compactionQueuedMessages(
|
|
3329
|
-
return this.compactionPromptQueues.get(sessionId) ?? [];
|
|
3651
|
+
compactionQueuedMessages(session) {
|
|
3652
|
+
return this.compactionPromptQueues.get(activeSessionKey(session.sessionId, this.eventScopeForSession(session))) ?? [];
|
|
3330
3653
|
}
|
|
3331
3654
|
hasQueuedMessageText(session, text) {
|
|
3332
|
-
return queuedMessagesFromSession(session, this.compactionQueuedMessages(session
|
|
3655
|
+
return queuedMessagesFromSession(session, this.compactionQueuedMessages(session)).some((message) => message.text === text);
|
|
3333
3656
|
}
|
|
3334
3657
|
notificationsForScope(scope) {
|
|
3335
3658
|
if (scope === NORMAL_SESSION_EVENT_SCOPE)
|
|
@@ -3366,6 +3689,9 @@ export class PiSessionService {
|
|
|
3366
3689
|
await this.publishUnreadMutations([]);
|
|
3367
3690
|
return this.unreadForScope(eventScope).durableCatalogSnapshot();
|
|
3368
3691
|
}
|
|
3692
|
+
async reconcileUnreadWorkspaces(cwds) {
|
|
3693
|
+
await this.publishUnreadMutations(this.unreadStore.reconcileWorkspaces(cwds));
|
|
3694
|
+
}
|
|
3369
3695
|
async acknowledgeUnread(sessionId, request, context) {
|
|
3370
3696
|
const store = this.unreadForScope(eventScopeFromManagementContext(context));
|
|
3371
3697
|
const result = store.acknowledge(sessionId, {
|
|
@@ -3389,7 +3715,7 @@ export class PiSessionService {
|
|
|
3389
3715
|
return result.snapshot;
|
|
3390
3716
|
}
|
|
3391
3717
|
async deliverSubsessionNotification(session, notification) {
|
|
3392
|
-
await this.runSessionEntryMutation(session, "
|
|
3718
|
+
await this.runSessionEntryMutation(session, "发送子会话通知", () => session.sendCustomMessage({ customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, content: notification.text, display: true, details: { sessionId: notification.childId } }, { triggerTurn: true, deliverAs: "followUp" }));
|
|
3393
3719
|
this.publishStatus(session);
|
|
3394
3720
|
}
|
|
3395
3721
|
logSubsessionNotificationFailure(parentId, childId, error) {
|
|
@@ -3410,6 +3736,7 @@ export class PiSessionService {
|
|
|
3410
3736
|
this.replaceSessionNotificationContext(session, candidateGeneration);
|
|
3411
3737
|
},
|
|
3412
3738
|
});
|
|
3739
|
+
await this.reloadModelScope(session);
|
|
3413
3740
|
if (candidateGeneration !== undefined) {
|
|
3414
3741
|
this.publishNotificationMutations(this.notificationsForSession(session).commitReplacement(candidateGeneration));
|
|
3415
3742
|
}
|
|
@@ -3423,44 +3750,34 @@ export class PiSessionService {
|
|
|
3423
3750
|
}
|
|
3424
3751
|
const message = error instanceof Error ? error.message : String(error);
|
|
3425
3752
|
this.publishActivity(session, "重新加载失败", "error", message);
|
|
3426
|
-
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
3753
|
+
this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
|
|
3427
3754
|
this.publishStatus(session);
|
|
3428
3755
|
throw error;
|
|
3429
3756
|
}
|
|
3430
3757
|
});
|
|
3431
3758
|
}
|
|
3432
|
-
async dismissWarning(ref, dismissId) {
|
|
3433
|
-
const session = await this.getOrOpen(ref);
|
|
3759
|
+
async dismissWarning(ref, dismissId, managementContext) {
|
|
3760
|
+
const session = await this.getOrOpen(ref, managementContext);
|
|
3434
3761
|
dismissSessionWarning(session, dismissId);
|
|
3435
3762
|
this.publishStatus(session);
|
|
3436
3763
|
return this.statusFromSession(session);
|
|
3437
3764
|
}
|
|
3438
3765
|
openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
|
|
3439
|
-
|
|
3440
|
-
if (active !== undefined)
|
|
3441
|
-
return Promise.resolve(active);
|
|
3442
|
-
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
|
|
3443
|
-
const existing = this.pendingSessionOpens.get(key);
|
|
3444
|
-
if (existing !== undefined)
|
|
3445
|
-
return existing.promise;
|
|
3446
|
-
const pending = {
|
|
3447
|
-
sessionId,
|
|
3448
|
-
promise: this.create(openSessionManager(), cwd, options),
|
|
3449
|
-
};
|
|
3450
|
-
pending.promise = pending.promise.finally(() => {
|
|
3451
|
-
if (this.pendingSessionOpens.get(key) === pending)
|
|
3452
|
-
this.pendingSessionOpens.delete(key);
|
|
3453
|
-
});
|
|
3454
|
-
this.pendingSessionOpens.set(key, pending);
|
|
3455
|
-
return pending.promise;
|
|
3766
|
+
return this.openPending(sessionId, cwd, options.managementContext, openSessionManager, options);
|
|
3456
3767
|
}
|
|
3457
3768
|
isCurrentActiveSession(session) {
|
|
3458
3769
|
return [...this.active.values()].some((active) => active.runtime.session === session);
|
|
3459
3770
|
}
|
|
3460
3771
|
async createSessionRuntime(sessionManager, cwd, options, startup) {
|
|
3461
3772
|
startup.report(STARTUP_PHASE_RUNTIME);
|
|
3462
|
-
|
|
3463
|
-
|
|
3773
|
+
options.startupSignal?.throwIfAborted();
|
|
3774
|
+
// Ordinary local sessions (undefined provenance) keep the current rule.
|
|
3775
|
+
// Only an explicit host one-shot forces delegation off; do not treat every
|
|
3776
|
+
// defined provenance as disabled the way upstream does.
|
|
3777
|
+
const delegationToolsEnabled = options.creationProvenance === "host-one-shot"
|
|
3778
|
+
? false
|
|
3779
|
+
: options.creationProvenance !== "tracked-subsession"
|
|
3780
|
+
&& await sessionAllowsDelegationTools(sessionManager, this.sessionManager);
|
|
3464
3781
|
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
3465
3782
|
cwd,
|
|
3466
3783
|
agentDir: this.agentDir,
|
|
@@ -3481,6 +3798,13 @@ export class PiSessionService {
|
|
|
3481
3798
|
};
|
|
3482
3799
|
let boundSession = runtime.session;
|
|
3483
3800
|
this.sessionEventScopes.set(boundSession, active.eventScope);
|
|
3801
|
+
if (options.managementContext !== undefined)
|
|
3802
|
+
this.sessionManagementContexts.set(boundSession, options.managementContext);
|
|
3803
|
+
const startupSignal = options.startupSignal;
|
|
3804
|
+
const cancelStartup = () => { this.endSessionExtensionDialogs(boundSession); };
|
|
3805
|
+
if (startupSignal !== undefined && !startupSignal.aborted) {
|
|
3806
|
+
startupSignal.addEventListener("abort", cancelStartup, { once: true });
|
|
3807
|
+
}
|
|
3484
3808
|
const notificationStore = this.notificationsForScope(active.eventScope);
|
|
3485
3809
|
let notificationGeneration = options.notificationGeneration;
|
|
3486
3810
|
let notificationOwnership = options.notifications === "disabled"
|
|
@@ -3504,17 +3828,23 @@ export class PiSessionService {
|
|
|
3504
3828
|
if (notificationGeneration !== undefined)
|
|
3505
3829
|
this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
|
|
3506
3830
|
try {
|
|
3831
|
+
startupSignal?.throwIfAborted();
|
|
3507
3832
|
if (options.creationProvenance === "tracked-subsession") {
|
|
3508
3833
|
await this.publishUnreadMutations(this.unreadForScope(active.eventScope).excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
|
|
3509
3834
|
}
|
|
3510
3835
|
else {
|
|
3511
3836
|
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
3512
3837
|
}
|
|
3838
|
+
startupSignal?.throwIfAborted();
|
|
3513
3839
|
startup.report(STARTUP_PHASE_EXTENSIONS);
|
|
3514
|
-
|
|
3840
|
+
// Startup hooks emit through the same scoped stream as hosted turns.
|
|
3515
3841
|
this.bindRuntime(active);
|
|
3842
|
+
await this.bindSessionExtensions(runtime.session, notificationGeneration, active, startupSignal);
|
|
3843
|
+
startupSignal?.throwIfAborted();
|
|
3516
3844
|
runtime.setRebindSession(async (session) => {
|
|
3517
3845
|
this.sessionEventScopes.set(session, active.eventScope);
|
|
3846
|
+
if (options.managementContext !== undefined)
|
|
3847
|
+
this.sessionManagementContexts.set(session, options.managementContext);
|
|
3518
3848
|
const priorGeneration = notificationGeneration;
|
|
3519
3849
|
let candidateGeneration;
|
|
3520
3850
|
try {
|
|
@@ -3545,6 +3875,7 @@ export class PiSessionService {
|
|
|
3545
3875
|
throw error;
|
|
3546
3876
|
}
|
|
3547
3877
|
});
|
|
3878
|
+
startupSignal?.throwIfAborted();
|
|
3548
3879
|
this.active.set(activeSessionKey(runtime.session.sessionId, active.eventScope), active);
|
|
3549
3880
|
if (notificationOwnership === "replacement" && notificationGeneration !== undefined) {
|
|
3550
3881
|
this.publishNotificationMutations(notificationStore.commitReplacement(notificationGeneration));
|
|
@@ -3572,7 +3903,7 @@ export class PiSessionService {
|
|
|
3572
3903
|
this.active.delete(sessionKey);
|
|
3573
3904
|
this.activities.delete(sessionKey);
|
|
3574
3905
|
this.clearAuthLossWarningsForSession(runtime.session.sessionId);
|
|
3575
|
-
this.clearCompactionPromptQueue(runtime.session
|
|
3906
|
+
this.clearCompactionPromptQueue(runtime.session);
|
|
3576
3907
|
removedActive = true;
|
|
3577
3908
|
}
|
|
3578
3909
|
if (removedActive) {
|
|
@@ -3586,11 +3917,14 @@ export class PiSessionService {
|
|
|
3586
3917
|
}
|
|
3587
3918
|
throw error;
|
|
3588
3919
|
}
|
|
3920
|
+
finally {
|
|
3921
|
+
startupSignal?.removeEventListener("abort", cancelStartup);
|
|
3922
|
+
}
|
|
3589
3923
|
}
|
|
3590
3924
|
replaceSessionNotificationContext(session, generation) {
|
|
3591
3925
|
session.extensionRunner.setUIContext(this.sessionUiContext(session, generation), "rpc");
|
|
3592
3926
|
}
|
|
3593
|
-
sessionUiContext(session, generation) {
|
|
3927
|
+
sessionUiContext(session, generation, startupDialogs = {}) {
|
|
3594
3928
|
const baseUiContext = session.extensionRunner.getUIContext();
|
|
3595
3929
|
const eventScope = this.eventScopeForSession(session);
|
|
3596
3930
|
const notificationStore = this.notificationsForScope(eventScope);
|
|
@@ -3605,14 +3939,6 @@ export class PiSessionService {
|
|
|
3605
3939
|
}
|
|
3606
3940
|
const added = notificationStore.addNotification(generation, message, type);
|
|
3607
3941
|
this.publishNotificationMutations(added.mutations);
|
|
3608
|
-
if (added.notification === undefined)
|
|
3609
|
-
return;
|
|
3610
|
-
this.events.publish(session.sessionId, {
|
|
3611
|
-
type: "command.output",
|
|
3612
|
-
level: type === "error" ? "error" : "info",
|
|
3613
|
-
message,
|
|
3614
|
-
notificationId: added.notification.id,
|
|
3615
|
-
}, eventScope);
|
|
3616
3942
|
};
|
|
3617
3943
|
// PI WEB owns the browser-facing dialog, notification, and text-formatting
|
|
3618
3944
|
// boundaries: the three dialog primitives park daemon-held Promises that
|
|
@@ -3626,13 +3952,13 @@ export class PiSessionService {
|
|
|
3626
3952
|
if (property === "theme")
|
|
3627
3953
|
return plainTextTheme;
|
|
3628
3954
|
if (property === "confirm") {
|
|
3629
|
-
return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts);
|
|
3955
|
+
return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts, startupDialogs.signal);
|
|
3630
3956
|
}
|
|
3631
3957
|
if (property === "select") {
|
|
3632
|
-
return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts);
|
|
3958
|
+
return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts, startupDialogs.signal);
|
|
3633
3959
|
}
|
|
3634
3960
|
if (property === "input") {
|
|
3635
|
-
return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts);
|
|
3961
|
+
return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts, startupDialogs.signal);
|
|
3636
3962
|
}
|
|
3637
3963
|
const value = Reflect.get(target, property, receiver);
|
|
3638
3964
|
return value;
|
|
@@ -3673,11 +3999,12 @@ export class PiSessionService {
|
|
|
3673
3999
|
}
|
|
3674
4000
|
publishUnreadMutations(mutations) {
|
|
3675
4001
|
if (mutations.length > 0) {
|
|
3676
|
-
const scopes = new
|
|
4002
|
+
const scopes = new Map();
|
|
3677
4003
|
for (const mutation of mutations) {
|
|
3678
|
-
|
|
4004
|
+
const scope = mutation.eventScope ?? NORMAL_SESSION_EVENT_SCOPE;
|
|
4005
|
+
scopes.set(scope, scopes.get(scope) === true || mutation.event.unread !== null);
|
|
3679
4006
|
}
|
|
3680
|
-
scopes.forEach((scope) => this.onUnreadChanged?.(scope));
|
|
4007
|
+
scopes.forEach((hasNewCompletion, scope) => this.onUnreadChanged?.(scope, hasNewCompletion));
|
|
3681
4008
|
}
|
|
3682
4009
|
this.enqueueUnreadMutations(mutations);
|
|
3683
4010
|
this.unreadPublicationFlushRequested = true;
|
|
@@ -3858,7 +4185,7 @@ function clientSessionFromListEntry(session) {
|
|
|
3858
4185
|
created: session.created.toISOString(),
|
|
3859
4186
|
modified: session.modified.toISOString(),
|
|
3860
4187
|
messageCount: session.messageCount,
|
|
3861
|
-
firstMessage: session.firstMessage,
|
|
4188
|
+
firstMessage: clientSessionFirstMessagePreview(session.firstMessage),
|
|
3862
4189
|
...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
|
|
3863
4190
|
};
|
|
3864
4191
|
}
|
|
@@ -4092,37 +4419,12 @@ function buildPromptOptions(behavior, images) {
|
|
|
4092
4419
|
options.images = images;
|
|
4093
4420
|
return Object.keys(options).length > 0 ? options : undefined;
|
|
4094
4421
|
}
|
|
4095
|
-
function stringValue(value) {
|
|
4096
|
-
return typeof value === "string" ? value : "";
|
|
4097
|
-
}
|
|
4098
4422
|
function historyMessages(session) {
|
|
4099
4423
|
return historyMessagesFromManager(session.sessionManager);
|
|
4100
4424
|
}
|
|
4101
4425
|
function historyMessagesFromManager(sessionManager) {
|
|
4102
4426
|
return historyMessagesFromEntries(sessionManager.getBranch());
|
|
4103
4427
|
}
|
|
4104
|
-
function historyMessagesFromEntries(entries) {
|
|
4105
|
-
const messages = [];
|
|
4106
|
-
let thinkingLevel;
|
|
4107
|
-
for (const entry of entries) {
|
|
4108
|
-
if (!isRecord(entry))
|
|
4109
|
-
continue;
|
|
4110
|
-
if (entry["type"] === "message")
|
|
4111
|
-
messages.push(annotateAssistantThinkingLevel(entry["message"], thinkingLevel));
|
|
4112
|
-
else if (entry["type"] === "thinking_level_change") {
|
|
4113
|
-
const level = getString(entry, "thinkingLevel");
|
|
4114
|
-
if (level !== undefined)
|
|
4115
|
-
thinkingLevel = level;
|
|
4116
|
-
}
|
|
4117
|
-
else if (entry["type"] === "custom_message" && entry["display"] === true)
|
|
4118
|
-
messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
|
|
4119
|
-
else if (entry["type"] === "compaction")
|
|
4120
|
-
messages.push({ role: "system", source: "compaction", content: `Compacted history:\n\n${stringValue(entry["summary"])}` });
|
|
4121
|
-
else if (entry["type"] === "branch_summary")
|
|
4122
|
-
messages.push({ role: "system", source: "branch_summary", content: `Branch summary:\n\n${stringValue(entry["summary"])}` });
|
|
4123
|
-
}
|
|
4124
|
-
return messages;
|
|
4125
|
-
}
|
|
4126
4428
|
function transcriptMessageCount(entries) {
|
|
4127
4429
|
let count = 0;
|
|
4128
4430
|
for (const entry of entries) {
|
|
@@ -4131,6 +4433,30 @@ function transcriptMessageCount(entries) {
|
|
|
4131
4433
|
}
|
|
4132
4434
|
return count;
|
|
4133
4435
|
}
|
|
4436
|
+
function activeEditableTreeTargetParentId(manager, targetId, activeLeafId) {
|
|
4437
|
+
if (activeLeafId !== targetId)
|
|
4438
|
+
return undefined;
|
|
4439
|
+
const entry = manager.getBranch().at(-1);
|
|
4440
|
+
if (!isRecord(entry) || entry["id"] !== targetId)
|
|
4441
|
+
return undefined;
|
|
4442
|
+
const isUserMessage = entry["type"] === "message"
|
|
4443
|
+
&& isRecord(entry["message"]) && entry["message"]["role"] === "user";
|
|
4444
|
+
if (!isUserMessage && entry["type"] !== "custom_message")
|
|
4445
|
+
return undefined;
|
|
4446
|
+
const parentId = entry["parentId"];
|
|
4447
|
+
if (parentId === targetId)
|
|
4448
|
+
return undefined;
|
|
4449
|
+
return parentId === null || typeof parentId === "string" ? parentId : undefined;
|
|
4450
|
+
}
|
|
4451
|
+
function setSessionTreeLeaf(manager, leafId) {
|
|
4452
|
+
if (leafId === null)
|
|
4453
|
+
manager.resetLeaf();
|
|
4454
|
+
else
|
|
4455
|
+
manager.branch(leafId);
|
|
4456
|
+
}
|
|
4457
|
+
function transcriptBranchIncludesEntry(entries, entryId) {
|
|
4458
|
+
return entries.some((entry) => isRecord(entry) && entry["id"] === entryId);
|
|
4459
|
+
}
|
|
4134
4460
|
/** custom entry type used to persist parent -> child subsession links outside LLM context. */
|
|
4135
4461
|
const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
|
|
4136
4462
|
/** custom entry type used to mark a child as created by spawn_subsession. */
|