@chainingintention/pi-web-cn 1.202610.4 → 2.202610.0

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