@chainingintention/pi-web-cn 1.202610.1 → 1.202610.3

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