@chainingintention/pi-web-cn 1.202610.0 → 1.202610.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +2 -2
  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 +785 -450
  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
- import { workbenchAccessHandle } from "../managementEmbed.js";
27
- import { managementPrivilegeRequested, managementPrivileges } from "../managementPrivileges.js";
30
+ import { isManagedAgentsFile, workbenchAccessHandle } from "../managementEmbed.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
  }
@@ -178,6 +176,16 @@ function parsePromptStreamingBehavior(value) {
178
176
  throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
179
177
  }
180
178
  const GLOBAL_AGENT_CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
179
+ export function filterPrivilegedManagementContextFiles(cwd, context, base) {
180
+ const injectedInstructions = context.projects.flatMap((project) => [
181
+ ...(project.agentsInstructions === undefined ? [] : [project.agentsInstructions]),
182
+ ...(project.agentsInstructionsReplaceIfMatches ?? []),
183
+ ]).map((content) => content.trim());
184
+ return {
185
+ agentsFiles: base.agentsFiles.filter((file) => !isManagedAgentsFile(file.content)
186
+ && !(basename(file.path) === "AGENTS.md" && pathInsideOrEqual(dirname(file.path), cwd) && injectedInstructions.includes(file.content.trim()))),
187
+ };
188
+ }
181
189
  export function filterManagedGlobalContextFiles(cwd, agentDir, base) {
182
190
  return {
183
191
  agentsFiles: base.agentsFiles.filter((file) => {
@@ -371,67 +379,108 @@ export function piWebResourceLoaderOptions(appendSystemPromptSections) {
371
379
  return undefined;
372
380
  return { appendSystemPromptOverride: (base) => [...base, ...appendSystemPromptSections] };
373
381
  }
374
- function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRuntime, resolveSettingsScope, spawn, subsessions, askUser, workbench, logger = noopLogger, managementAudit, managementProjectIdForCwd, appendSystemPromptSections = [], managementAllowPrivileged = false) {
375
- 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 }) => {
376
412
  const scopedModelRuntime = managementContext === undefined ? await normalModelRuntimeForCwd(cwd) : await managementModelRuntime();
377
413
  if (managementContext !== undefined) {
378
- 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)({
379
415
  cwd,
380
416
  agentDir,
381
417
  sessionManager,
382
- delegationToolsEnabled,
418
+ ...(delegationToolsEnabled === undefined ? {} : { delegationToolsEnabled }),
383
419
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
384
420
  ...(initialModel === undefined ? {} : { initialModel }),
385
421
  ...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
386
422
  });
387
423
  }
388
- const settingsManager = await createScopedSettingsManager({
389
- cwd,
390
- scopeDirectory: await resolveSettingsScope(cwd, "normal"),
391
- globalAgentDir: agentDir,
392
- mode: "normal",
393
- });
394
- const resourceLoaderOptions = {
395
- ...piWebResourceLoaderOptions(appendSystemPromptSections),
396
- skillsOverride: (base) => deduplicateBundledRelaySkills(cwd, base),
397
- };
398
- const services = await createAgentSessionServices({
399
- cwd,
400
- agentDir,
401
- modelRuntime: scopedModelRuntime,
402
- settingsManager,
403
- resourceLoaderOptions,
404
- });
405
- const modelOptions = await resolveSessionModelOptions({
406
- services,
407
- hasExistingSession: sessionManager.buildSessionContext().messages.length > 0,
408
- ...(initialModel === undefined ? {} : { initialModel }),
409
- ...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
410
- });
411
- services.diagnostics.push(...modelOptions.diagnostics);
412
- const customTools = createPiWebCustomToolDefinitions(cwd, delegationToolsEnabled, spawn, subsessions, askUser);
413
- const result = await createAgentSessionFromServices({
414
- services,
415
- sessionManager,
416
- customTools,
417
- ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
418
- ...(modelOptions.model === undefined ? {} : { model: modelOptions.model }),
419
- ...(modelOptions.thinkingLevel === undefined ? {} : { thinkingLevel: modelOptions.thinkingLevel }),
420
- ...(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] };
421
469
  });
422
- return { ...result, services, diagnostics: services.diagnostics };
423
470
  };
424
471
  }
425
- function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd, managementAllowPrivileged = false, appendSystemPromptSections = []) {
426
- 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 });
427
475
  const privileges = managementPrivileges(managementContext, { allowPrivileged: managementAllowPrivileged });
428
- const fullPrivileges = privileges.full === true;
429
- if (!fullPrivileges)
476
+ const resolvedDelegationToolsEnabled = delegationToolsEnabled
477
+ ?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
478
+ if (!normalPermissions)
430
479
  await ensureManagedRelaySkills(cwd);
431
480
  const auditIdentity = await resolveManagementAuditIdentity(cwd, managementContext, managementProjectIdForCwd, logger);
432
481
  const accessHandle = workbenchAccessHandle(managementContext);
433
- const state = workbench === undefined ? undefined : await workbench.accessStates.prepare(accessHandle, workbench.client);
434
- 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, {
435
484
  ...(managementAudit === undefined ? {} : { recorder: managementAudit }),
436
485
  ...auditIdentity,
437
486
  sessionId: sessionManager.getSessionId(),
@@ -457,13 +506,18 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
457
506
  const scopedSubsessions = subsessions === undefined
458
507
  ? undefined
459
508
  : {
460
- ...subsessions,
461
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),
462
513
  };
514
+ const scopedAskUser = askUser === undefined ? undefined : {
515
+ open: (input) => askUser.open({ ...input, managementContext }),
516
+ };
463
517
  const controlledManagementTools = [
464
- ...(delegationToolsEnabled && scopedSpawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn: scopedSpawn }))] : []),
465
- ...(delegationToolsEnabled && scopedSubsessions !== undefined ? createSubsessionToolDefinitions(cwd, scopedSubsessions).map((tool) => defineTool(tool)) : []),
466
- ...(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)]),
467
521
  ...workbenchTools,
468
522
  ];
469
523
  const controlledManagementToolNames = controlledManagementTools.map((tool) => tool.name);
@@ -475,27 +529,36 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
475
529
  }, "management privileged grant ignored because managementEmbed.allowPrivileged is disabled");
476
530
  }
477
531
  const extraToolNames = [...controlledManagementToolNames, ...privilegedManagementToolNames(privileges)];
478
- const policyAgentDir = fullPrivileges ? undefined : await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext, extraToolNames, privileges);
479
- 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 () => {
480
535
  const settingsManager = await createScopedSettingsManager({
481
536
  cwd,
482
537
  scopeDirectory: await resolveSettingsScope(cwd, "management"),
483
538
  globalAgentDir: agentDir,
484
539
  mode: "management",
540
+ // Signed management entry authorizes the project without changing ordinary trust decisions.
541
+ projectTrusted: true,
485
542
  });
486
- const services = await createAgentSessionServices({
543
+ const { services, projectTrustDiagnostics } = normalPermissions ? await createNormalPermissionSessionServices({
487
544
  cwd,
488
545
  agentDir,
489
546
  modelRuntime,
490
547
  settingsManager,
491
- resourceLoaderOptions: fullPrivileges ? {
492
- ...piWebResourceLoaderOptions(appendSystemPromptSections),
493
- skillsOverride: (base) => deduplicateBundledRelaySkills(cwd, base),
494
- } : {
495
- agentsFilesOverride: (base) => filterManagedGlobalContextFiles(cwd, agentDir, base),
496
- skillsOverride: (base) => receipt === undefined ? filterManagedProjectSkills(cwd, base) : filterManagedWorkbenchSkills(cwd, base, receipt),
497
- },
498
- });
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: [] };
499
562
  const modelOptions = await resolveSessionModelOptions({
500
563
  services,
501
564
  hasExistingSession: sessionManager.buildSessionContext().messages.length > 0,
@@ -503,12 +566,8 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
503
566
  ...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
504
567
  });
505
568
  services.diagnostics.push(...modelOptions.diagnostics);
506
- const managedToolOptions = createManagedAgentToolOptions(cwd);
507
- const customTools = fullPrivileges ? [
508
- ...createPiWebCustomToolDefinitions(cwd, delegationToolsEnabled, scopedSpawn, scopedSubsessions, askUser),
509
- ...workbenchTools,
510
- ] : [
511
- ...managedAgentToolDefinitions(cwd, managedToolOptions),
569
+ const customTools = normalPermissions ? createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, scopedSpawn, scopedSubsessions, scopedAskUser) : [
570
+ ...managedAgentToolDefinitions(cwd, createManagedAgentToolOptions(cwd)),
512
571
  createManagedPythonToolDefinition(cwd, managementContext, { network: privileges.network }),
513
572
  createManagedBashToolDefinition(cwd, managementContext, { network: privileges.network, hostFilesystem: privileges.bash }),
514
573
  ...controlledManagementTools,
@@ -518,15 +577,16 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
518
577
  sessionManager,
519
578
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
520
579
  customTools,
521
- ...(fullPrivileges
522
- ? { excludeTools: managementContext.tools?.deny ?? [] }
523
- : { tools: managementAgentToolNames(managementContext, extraToolNames, privileges) }),
580
+ ...(normalPermissions ? {} : { tools: managementAgentToolNames(managementContext, extraToolNames, privileges) }),
524
581
  ...(modelOptions.model === undefined ? {} : { model: modelOptions.model }),
525
582
  ...(modelOptions.thinkingLevel === undefined ? {} : { thinkingLevel: modelOptions.thinkingLevel }),
526
583
  ...(modelOptions.scopedModels.length === 0 ? {} : { scopedModels: modelOptions.scopedModels }),
527
584
  };
528
585
  const result = await createAgentSessionFromServices(options);
529
- 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] };
530
590
  });
531
591
  };
532
592
  }
@@ -582,6 +642,7 @@ export class PiSessionService {
582
642
  this.activities = new SessionActivityCoordinator();
583
643
  /** Runtime-identity gate held while Pi may await abandoned-branch summarization. */
584
644
  this.treeNavigations = new WeakSet();
645
+ this.unpersistedTreeBranchLeaves = new WeakMap();
585
646
  /** Counts async operations that may append an entry before they settle. */
586
647
  this.sessionEntryMutationCounts = new WeakMap();
587
648
  /** Serializes enabled-model read/modify/write operations. */
@@ -617,6 +678,7 @@ export class PiSessionService {
617
678
  this.scopedNotificationStores = new Map();
618
679
  this.scopedUnreadStores = new Map();
619
680
  this.sessionEventScopes = new WeakMap();
681
+ this.sessionManagementContexts = new WeakMap();
620
682
  this.notificationGenerationBySession = new WeakMap();
621
683
  this.idleSessionFileResolutions = new WeakMap();
622
684
  /** The parked extension Promise resolvers behind the store's open dialogs. */
@@ -650,6 +712,10 @@ export class PiSessionService {
650
712
  this.managementAudit = deps.managementAudit;
651
713
  this.managementProjectIdForCwd = deps.managementProjectIdForCwd;
652
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
+ });
653
719
  this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
654
720
  this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
655
721
  this.onUnreadChanged = deps.onUnreadChanged;
@@ -663,18 +729,19 @@ export class PiSessionService {
663
729
  // Subsessions are gated behind their own flag, and they
664
730
  // also require the spawn capability (they share its project-scope resolver).
665
731
  const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
666
- 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 : {
667
734
  spawn: (input) => this.spawnSubsession(input),
668
- list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
669
- check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
670
- read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
671
- }, 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);
672
739
  this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
673
740
  this.workspaceActivity = deps.workspaceActivity;
674
741
  this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
675
742
  this.commandService = new SessionCommandService((sessionId, eventScope) => this.getActiveForCommand(sessionId, eventScope), (sessionId, text, eventScope) => this.submitCommandPrompt(sessionId, text, eventScope), events, {
676
743
  onCompactionStart: (session) => {
677
- this.beginSessionEntryMutation(session, "compact the session");
744
+ this.beginSessionEntryMutation(session, "压缩会话");
678
745
  this.publishActivity(session, "正在压缩上下文", "active");
679
746
  this.publishStatus(session);
680
747
  },
@@ -693,7 +760,7 @@ export class PiSessionService {
693
760
  isTreeNavigationActive: (session) => this.treeNavigations.has(session),
694
761
  runSessionReplacement: (session, operation) => {
695
762
  const runtime = this.activeRuntimeForSession(session);
696
- 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);
697
764
  },
698
765
  }, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
699
766
  }
@@ -741,7 +808,6 @@ export class PiSessionService {
741
808
  await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" }, scope);
742
809
  readyDeleteRecords.push(record);
743
810
  }
744
- await this.ensureArchivedRecordsMoved(readyDeleteRecords);
745
811
  const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
746
812
  deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
747
813
  await this.forgetUnreadSessions(deleteRecords, scope);
@@ -768,7 +834,7 @@ export class PiSessionService {
768
834
  const activeSessions = Array.from(new Set(this.active.values()));
769
835
  for (const active of activeSessions) {
770
836
  this.forgetUnreadActivity(active.runtime.session);
771
- this.pendingAskStore.forgetSession(active.runtime.session.sessionId);
837
+ this.pendingAskStore.forgetSession(this.dialogSessionKey(active.runtime.session));
772
838
  this.endSessionExtensionDialogs(active.runtime.session);
773
839
  }
774
840
  this.active.clear();
@@ -787,10 +853,15 @@ export class PiSessionService {
787
853
  store.clearAll("service-dispose");
788
854
  await Promise.all(activeSessions.map(async (active) => {
789
855
  active.unsubscribe();
856
+ active.runtime.setRebindSession(undefined);
790
857
  this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
791
- await active.runtime.session.abort();
792
- await active.runtime.dispose();
793
- }));
858
+ try {
859
+ await this.abortSessionOperations(active.runtime.session);
860
+ }
861
+ finally {
862
+ await active.runtime.dispose();
863
+ }
864
+ })).finally(() => this.activityMarker.dispose());
794
865
  await this.publishUnreadMutations([]);
795
866
  }
796
867
  async list(cwd, managementContext) {
@@ -858,9 +929,9 @@ export class PiSessionService {
858
929
  await this.publishUnreadMutations(this.unreadForScope(eventScopeFromManagementContext(managementContext)).reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
859
930
  }
860
931
  const archivedSessions = archivedForCwd
861
- .sort(compareArchivedRecords)
862
932
  .map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
863
- .filter(isDefined);
933
+ .filter(isDefined)
934
+ .sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified));
864
935
  return {
865
936
  sessions: [...unarchivedSessions, ...archivedSessions],
866
937
  entriesById: new Map(sessions.map((session) => [session.id, session])),
@@ -872,15 +943,42 @@ export class PiSessionService {
872
943
  await this.normalModelRuntimeForCwd(cwd);
873
944
  return this.startSession(cwd, options);
874
945
  }
875
- async startSession(cwd, options) {
876
- const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
877
- ...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
878
- ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
879
- ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
880
- ...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
881
- ...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
882
- startupIntent: "create",
883
- });
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) {
884
982
  const { session } = active.runtime;
885
983
  const created = {
886
984
  id: session.sessionId,
@@ -893,13 +991,22 @@ export class PiSessionService {
893
991
  firstMessage: "",
894
992
  // Include the parent so listeners can nest the new session in the tree
895
993
  // immediately, instead of showing it flat until the next reload.
896
- ...(options.parentSession === undefined ? {} : { parentSessionPath: options.parentSession }),
994
+ ...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
897
995
  };
898
- // Broadcast so other clients (and the spawning agent's UI) can add the new
899
- // session to their list without a manual reload.
900
996
  this.events.publishGlobal({ type: "session.created", session: created }, active.eventScope);
901
997
  return created;
902
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
+ }
903
1010
  /**
904
1011
  * Start a new session on behalf of a LLM and deliver an initial prompt to it.
905
1012
  * The target cwd is constrained to a workspace of the same registered project
@@ -964,9 +1071,9 @@ export class PiSessionService {
964
1071
  ...(parentSessionFile === undefined ? {} : { parentSessionFile }),
965
1072
  cwd: decision.cwd,
966
1073
  };
967
- await this.registerVerifiedSubsession(link, eventScopeFromManagementContext(input.managementContext));
968
- this.persistSubsessionLink(link);
969
- this.persistSubsessionChildMarker(input.parentSessionId, created.id);
1074
+ const scopedLink = await this.registerVerifiedSubsession(link, input.managementContext);
1075
+ this.persistSubsessionLink(scopedLink);
1076
+ this.persistSubsessionChildMarker(scopedLink);
970
1077
  await this.prompt(created.id, input.prompt, undefined, undefined, {
971
1078
  ...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
972
1079
  });
@@ -1042,6 +1149,23 @@ export class PiSessionService {
1042
1149
  this.modelScopeMutationQueue = queued.then(() => undefined, () => undefined);
1043
1150
  return queued;
1044
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
+ }
1045
1169
  applyEnabledModelScope(session, available, enabledIds) {
1046
1170
  const context = this.modelScopeContext(session);
1047
1171
  const availableIds = available.map(modelScopeId);
@@ -1084,14 +1208,16 @@ export class PiSessionService {
1084
1208
  * agent loop reports to the model as an error tool result.
1085
1209
  */
1086
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.
1087
- async openAsk(input) {
1088
- 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 });
1089
1215
  // A supersede closes the earlier ask, so the browsers watching it must hear
1090
1216
  // that before they hear about its replacement.
1091
1217
  if (result.superseded !== undefined)
1092
- this.publishAskClosed(input.sessionId, result.superseded);
1093
- this.events.publish(input.sessionId, { type: "ask.opened", ask: result.ask });
1094
- 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);
1095
1221
  return result;
1096
1222
  }
1097
1223
  /**
@@ -1107,8 +1233,8 @@ export class PiSessionService {
1107
1233
  const session = await this.getOrOpen(ref, managementContext);
1108
1234
  // Checked before the store closes the ask so a refused delivery cannot
1109
1235
  // discard answers the user already submitted.
1110
- this.assertTreeNavigationInactive(session, "answer questions");
1111
- 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));
1112
1238
  }
1113
1239
  /**
1114
1240
  * Close the open ask without answers. The model is still told, naming every
@@ -1118,8 +1244,8 @@ export class PiSessionService {
1118
1244
  async cancelAsk(ref, askId, managementContext) {
1119
1245
  await this.assertWritable(ref);
1120
1246
  const session = await this.getOrOpen(ref, managementContext);
1121
- this.assertTreeNavigationInactive(session, "dismiss questions");
1122
- 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));
1123
1249
  }
1124
1250
  /**
1125
1251
  * Publish and deliver a closed ask. A stale close is reported rather than
@@ -1131,13 +1257,13 @@ export class PiSessionService {
1131
1257
  if (result.status === "stale")
1132
1258
  return { result: "stale", sessionStatus: this.statusFromSession(session) };
1133
1259
  const { outcome } = result;
1134
- this.publishAskClosed(session.sessionId, outcome);
1135
- 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" }));
1136
1262
  this.publishStatus(session);
1137
1263
  return { result: "closed", outcome, sessionStatus: this.statusFromSession(session) };
1138
1264
  }
1139
- publishAskClosed(sessionId, outcome) {
1140
- 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);
1141
1267
  }
1142
1268
  /**
1143
1269
  * Void the session's open ask because the user sent a chat message instead of
@@ -1146,11 +1272,11 @@ export class PiSessionService {
1146
1272
  * itself triggers rather than becoming a turn of its own.
1147
1273
  */
1148
1274
  async voidOpenAskForUserMessage(session) {
1149
- const outcome = this.pendingAskStore.cancelOpen(session.sessionId);
1275
+ const outcome = this.pendingAskStore.cancelOpen(this.dialogSessionKey(session));
1150
1276
  if (outcome === undefined)
1151
1277
  return;
1152
- this.publishAskClosed(session.sessionId, outcome);
1153
- 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" }));
1154
1280
  this.publishStatus(session);
1155
1281
  }
1156
1282
  /**
@@ -1195,8 +1321,12 @@ export class PiSessionService {
1195
1321
  * `async` so a rejected dialog becomes a rejection rather than a synchronous
1196
1322
  * throw from a promise-returning method.
1197
1323
  */
1198
- async openExtensionDialog(session, request, opts) {
1199
- 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]);
1200
1330
  // A pre-aborted signal dismisses the dialog before it ever opens.
1201
1331
  if (signal?.aborted === true)
1202
1332
  return extensionDialogCancelValue(request.kind);
@@ -1278,18 +1408,19 @@ export class PiSessionService {
1278
1408
  }, this.eventScopeForSession(session));
1279
1409
  }
1280
1410
  /** Summaries of the tracked subsessions spawned by `parentSessionId`. */
1281
- async listSubsessions(parentSessionId, parentSessionFile) {
1411
+ async listSubsessions(parentSessionId, parentSessionFile, managementContext) {
1282
1412
  const parentFile = nonEmptyString(parentSessionFile);
1283
- await this.hydrateSubsessionsForParent(parentSessionId, parentFile);
1284
- 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));
1285
1416
  if (childIds === undefined)
1286
1417
  return [];
1287
- const authorizedChildIds = [...childIds].filter((childId) => this.subsessionLinkBelongsToParent(parentSessionId, parentFile, childId));
1288
- 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)) })));
1289
1420
  }
1290
1421
  /** Status and final result of a subsession, scoped to the caller's children. */
1291
- async checkSubsession(parentSessionId, sessionId, parentSessionFile) {
1292
- 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);
1293
1424
  const messages = historyMessages(session);
1294
1425
  return {
1295
1426
  sessionId,
@@ -1300,8 +1431,8 @@ export class PiSessionService {
1300
1431
  };
1301
1432
  }
1302
1433
  /** Filtered, paginated transcript of a subsession, scoped to the caller's children. */
1303
- async readSubsession(parentSessionId, sessionId, query, parentSessionFile) {
1304
- 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);
1305
1436
  const view = buildTranscriptView(historyMessages(session), query);
1306
1437
  return {
1307
1438
  sessionId,
@@ -1311,52 +1442,61 @@ export class PiSessionService {
1311
1442
  };
1312
1443
  }
1313
1444
  /** Open a session after verifying it is one of the caller's tracked children. */
1314
- async openSubsession(parentSessionId, sessionId, parentSessionFile) {
1445
+ async openSubsession(parentSessionId, sessionId, parentSessionFile, managementContext) {
1315
1446
  const parentFile = nonEmptyString(parentSessionFile);
1316
- await this.hydrateSubsessionsForParent(parentSessionId, parentFile);
1317
- 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)) {
1318
1450
  throw new Error(`Session ${sessionId} is not one of your subsessions`);
1319
1451
  }
1320
- return this.getOrOpenTrackedSubsession(sessionId);
1452
+ return this.getOrOpenTrackedSubsession(sessionId, scope);
1321
1453
  }
1322
- subsessionLinkBelongsToParent(parentSessionId, parentSessionFile, childSessionId) {
1323
- const link = this.subsessionLinks.get(childSessionId);
1454
+ subsessionLinkBelongsToParent(parentSessionId, parentSessionFile, childSessionId, scope) {
1455
+ const link = this.subsessionLinks.get(activeSessionKey(childSessionId, scope));
1324
1456
  if (link?.parentSessionId !== parentSessionId)
1325
1457
  return false;
1326
1458
  return parentSessionFile === undefined || trackedLinkParentFileMatches(link, parentSessionFile);
1327
1459
  }
1328
1460
  activeChildForSubsessionLink(link) {
1329
- 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;
1330
1463
  }
1331
1464
  activeParentForSubsessionLink(link) {
1332
- 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;
1333
1467
  }
1334
1468
  subsessionLinkForActiveChild(session) {
1335
- const childId = session.sessionId;
1336
- const parentId = this.subsessionParents.get(childId);
1337
- 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);
1338
1472
  if (parentId === undefined || link?.parentSessionId !== parentId)
1339
1473
  return undefined;
1340
1474
  return sessionFileMatches(session, link.childSessionFile) ? link : undefined;
1341
1475
  }
1342
- async registerVerifiedSubsession(link, scope = NORMAL_SESSION_EVENT_SCOPE) {
1476
+ async registerVerifiedSubsession(link, managementContext) {
1343
1477
  const { childSessionId, parentSessionId } = link;
1344
- 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);
1345
1483
  if (previousParentId !== undefined && previousParentId !== parentSessionId) {
1346
- const previousChildren = this.subsessionChildren.get(previousParentId);
1484
+ const previousParentKey = activeSessionKey(previousParentId, scope);
1485
+ const previousChildren = this.subsessionChildren.get(previousParentKey);
1347
1486
  previousChildren?.delete(childSessionId);
1348
1487
  if (previousChildren?.size === 0)
1349
- this.subsessionChildren.delete(previousParentId);
1488
+ this.subsessionChildren.delete(previousParentKey);
1350
1489
  }
1351
- this.subsessionParents.set(childSessionId, parentSessionId);
1352
- const children = this.subsessionChildren.get(parentSessionId) ?? new Set();
1490
+ this.subsessionParents.set(childKey, parentSessionId);
1491
+ const children = this.subsessionChildren.get(parentKey) ?? new Set();
1353
1492
  children.add(childSessionId);
1354
- this.subsessionChildren.set(parentSessionId, children);
1355
- this.subsessionLinks.set(childSessionId, link);
1356
- if (!this.subsessionNotifyArmed.has(childSessionId))
1357
- this.subsessionNotifyArmed.set(childSessionId, false);
1358
- 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);
1359
1498
  await this.publishUnreadMutations(this.unreadForScope(scope).excludeSession(childSessionId, cwd));
1499
+ return scopedLink;
1360
1500
  }
1361
1501
  cwdForVerifiedSubsession(link) {
1362
1502
  const activeCwd = this.activeChildForSubsessionLink(link)?.runtime.session.sessionManager.getCwd();
@@ -1374,17 +1514,19 @@ export class PiSessionService {
1374
1514
  }
1375
1515
  throw new Error("Could not resolve cwd for verified tracked sub-session");
1376
1516
  }
1377
- unregisterSubsession(childSessionId) {
1378
- const parentSessionId = this.subsessionParents.get(childSessionId);
1379
- this.subsessionParents.delete(childSessionId);
1380
- this.subsessionLinks.delete(childSessionId);
1381
- 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);
1382
1523
  if (parentSessionId === undefined)
1383
1524
  return;
1384
- const children = this.subsessionChildren.get(parentSessionId);
1525
+ const parentKey = activeSessionKey(parentSessionId, scope);
1526
+ const children = this.subsessionChildren.get(parentKey);
1385
1527
  children?.delete(childSessionId);
1386
1528
  if (children?.size === 0)
1387
- this.subsessionChildren.delete(parentSessionId);
1529
+ this.subsessionChildren.delete(parentKey);
1388
1530
  }
1389
1531
  persistSubsessionLink(link) {
1390
1532
  const parent = this.activeParentForSubsessionLink(link)?.runtime.session;
@@ -1399,8 +1541,9 @@ export class PiSessionService {
1399
1541
  this.logger.info({ parentSessionId: link.parentSessionId, sessionId: link.childSessionId, error: error instanceof Error ? error.message : String(error) }, "failed to persist subsession link");
1400
1542
  }
1401
1543
  }
1402
- persistSubsessionChildMarker(parentSessionId, childSessionId) {
1403
- const child = this.activeForSessionId(childSessionId)?.runtime.session;
1544
+ persistSubsessionChildMarker(link) {
1545
+ const { parentSessionId, childSessionId } = link;
1546
+ const child = this.activeChildForSubsessionLink(link)?.runtime.session;
1404
1547
  if (child === undefined)
1405
1548
  return;
1406
1549
  if (child.sessionManager.appendCustomEntry === undefined)
@@ -1412,14 +1555,15 @@ export class PiSessionService {
1412
1555
  this.logger.info({ parentSessionId, sessionId: childSessionId, error: error instanceof Error ? error.message : String(error) }, "failed to persist subsession child marker");
1413
1556
  }
1414
1557
  }
1415
- async hydrateSubsessionsForParent(parentSessionId, parentSessionFile) {
1416
- const hydrationKey = subsessionHydratedParentKey(parentSessionId, parentSessionFile);
1558
+ async hydrateSubsessionsForParent(parentSessionId, parentSessionFile, managementContext) {
1559
+ const scope = eventScopeFromManagementContext(managementContext);
1560
+ const hydrationKey = subsessionHydratedParentKey(activeSessionKey(parentSessionId, scope), parentSessionFile);
1417
1561
  if (this.subsessionHydratedParents.has(hydrationKey))
1418
1562
  return;
1419
- const activeParent = this.activeForSessionId(parentSessionId);
1563
+ const activeParent = this.activeForSessionIdAndScope(parentSessionId, scope);
1420
1564
  if (activeParent !== undefined && (parentSessionFile === undefined || activeSessionFileMatches(activeParent, parentSessionFile))) {
1421
1565
  const activeParentFile = nonEmptyString(activeParent.runtime.session.sessionFile);
1422
- 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);
1423
1567
  if (complete)
1424
1568
  this.subsessionHydratedParents.add(hydrationKey);
1425
1569
  return;
@@ -1435,11 +1579,11 @@ export class PiSessionService {
1435
1579
  catch {
1436
1580
  return;
1437
1581
  }
1438
- const complete = await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile);
1582
+ const complete = await this.registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile, managementContext);
1439
1583
  if (complete)
1440
1584
  this.subsessionHydratedParents.add(hydrationKey);
1441
1585
  }
1442
- async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile, scope = NORMAL_SESSION_EVENT_SCOPE) {
1586
+ async registerPersistedSubsessionLinks(parentSessionId, parentManager, parentSessionFile, managementContext) {
1443
1587
  // Parent custom links are the authoritative recovery record: verify the
1444
1588
  // exact live child file/header before tracking. Do not negatively cache a
1445
1589
  // scan while a candidate child is temporarily unavailable.
@@ -1454,7 +1598,7 @@ export class PiSessionService {
1454
1598
  complete = false;
1455
1599
  continue;
1456
1600
  }
1457
- await this.registerVerifiedSubsession(verified, scope);
1601
+ await this.registerVerifiedSubsession(verified, managementContext);
1458
1602
  }
1459
1603
  return complete;
1460
1604
  }
@@ -1475,7 +1619,7 @@ export class PiSessionService {
1475
1619
  const link = await this.verifiedSubsessionLinkFromOpenedChild(session);
1476
1620
  if (link === undefined)
1477
1621
  return;
1478
- await this.registerVerifiedSubsession(link, this.eventScopeForSession(session));
1622
+ await this.registerVerifiedSubsession(link, this.sessionManagementContexts.get(session));
1479
1623
  }
1480
1624
  verifiedSubsessionLinkFromOpenedChild(session) {
1481
1625
  return verifiedTrackedSubsessionLink(this.sessionManager, {
@@ -1485,8 +1629,8 @@ export class PiSessionService {
1485
1629
  cwd: session.sessionManager.getCwd(),
1486
1630
  });
1487
1631
  }
1488
- async getOrOpenTrackedSubsession(sessionId) {
1489
- const link = this.subsessionLinks.get(sessionId);
1632
+ async getOrOpenTrackedSubsession(sessionId, scope) {
1633
+ const link = this.subsessionLinks.get(activeSessionKey(sessionId, scope));
1490
1634
  if (link === undefined)
1491
1635
  throw new Error("Session not found");
1492
1636
  const active = this.activeChildForSubsessionLink(link);
@@ -1496,12 +1640,14 @@ export class PiSessionService {
1496
1640
  if (!(await sessionFileHeaderMatches(link.childSessionFile, { sessionId, parentSessionFile: link.parentSessionFile })))
1497
1641
  throw new Error("Session not found");
1498
1642
  const sessionManager = this.sessionManager.open(link.childSessionFile);
1499
- 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;
1500
1646
  }
1501
1647
  throw new Error("Session not found");
1502
1648
  }
1503
- async subsessionSummaryFields(childSessionId) {
1504
- const link = this.subsessionLinks.get(childSessionId);
1649
+ async subsessionSummaryFields(childSessionId, scope) {
1650
+ const link = this.subsessionLinks.get(activeSessionKey(childSessionId, scope));
1505
1651
  const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
1506
1652
  if (active !== undefined) {
1507
1653
  return { cwd: active.runtime.cwd, status: this.subsessionStatus(active.runtime.session) };
@@ -1520,12 +1666,12 @@ export class PiSessionService {
1520
1666
  return "error";
1521
1667
  return "idle";
1522
1668
  }
1523
- workingSubsessionIds(parentSessionId) {
1524
- const childIds = this.subsessionChildren.get(parentSessionId);
1669
+ workingSubsessionIds(parentSessionId, scope) {
1670
+ const childIds = this.subsessionChildren.get(activeSessionKey(parentSessionId, scope));
1525
1671
  if (childIds === undefined)
1526
1672
  return [];
1527
1673
  return [...childIds].filter((childId) => {
1528
- const link = this.subsessionLinks.get(childId);
1674
+ const link = this.subsessionLinks.get(activeSessionKey(childId, scope));
1529
1675
  const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link);
1530
1676
  return active !== undefined && this.hasActiveWork(active.runtime.session);
1531
1677
  });
@@ -1541,27 +1687,26 @@ export class PiSessionService {
1541
1687
  if (link === undefined)
1542
1688
  return;
1543
1689
  const childId = link.childSessionId;
1690
+ const childKey = activeSessionKey(childId, link.eventScope);
1544
1691
  if (this.hasActiveWork(session)) {
1545
- this.subsessionNotifyArmed.set(childId, true);
1692
+ this.subsessionNotifyArmed.set(childKey, true);
1546
1693
  return;
1547
1694
  }
1548
- if (this.subsessionNotifyArmed.get(childId) !== true)
1695
+ if (this.subsessionNotifyArmed.get(childKey) !== true)
1549
1696
  return;
1550
- this.subsessionNotifyArmed.set(childId, false);
1697
+ this.subsessionNotifyArmed.set(childKey, false);
1551
1698
  const status = this.activityForSession(session)?.phase === "error" ? "error" : "idle";
1552
1699
  const finalText = finalAssistantText(historyMessages(session));
1553
1700
  const outputSection = formatSubsessionNotificationOutput(childId, finalText);
1554
- const workingIds = this.workingSubsessionIds(link.parentSessionId);
1701
+ const workingIds = this.workingSubsessionIds(link.parentSessionId, link.eventScope);
1555
1702
  const next = workingIds.length === 0
1556
1703
  ? "No other tracked subsessions are working."
1557
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.`;
1558
1705
  const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n${outputSection}`;
1559
- void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
1706
+ void this.notifyParentOfSubsession(link, text);
1560
1707
  }
1561
- async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
1562
- const link = this.subsessionLinks.get(childSessionId);
1563
- if (link?.parentSessionId !== parentSessionId)
1564
- throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
1708
+ async getOrOpenParentForSubsession(link) {
1709
+ const { parentSessionId } = link;
1565
1710
  const active = this.activeParentForSubsessionLink(link);
1566
1711
  if (active !== undefined)
1567
1712
  return active.runtime.session;
@@ -1572,7 +1717,9 @@ export class PiSessionService {
1572
1717
  throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
1573
1718
  }
1574
1719
  const sessionManager = this.sessionManager.open(parentSessionFile);
1575
- 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;
1576
1723
  }
1577
1724
  /**
1578
1725
  * Deliver a subsession-completion notice to the parent as a system-authored
@@ -1581,9 +1728,10 @@ export class PiSessionService {
1581
1728
  * queues behind in-flight work (`deliverAs: "followUp"`), preserving the
1582
1729
  * established "queue if busy, send and act if idle" behavior.
1583
1730
  */
1584
- async notifyParentOfSubsession(parentId, childId, text) {
1731
+ async notifyParentOfSubsession(link, text) {
1732
+ const { parentSessionId: parentId, childSessionId: childId } = link;
1585
1733
  try {
1586
- const session = await this.getOrOpenParentForSubsession(parentId, childId);
1734
+ const session = await this.getOrOpenParentForSubsession(link);
1587
1735
  if (this.treeNavigations.has(session)) {
1588
1736
  const pending = this.deferredSubsessionNotifications.get(session) ?? [];
1589
1737
  pending.push({ parentId, childId, text });
@@ -1602,6 +1750,9 @@ export class PiSessionService {
1602
1750
  }
1603
1751
  async status(ref, managementContext) {
1604
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
+ }
1605
1756
  if (this.hasActiveWork(session))
1606
1757
  return this.statusFromSession(session);
1607
1758
  const branch = await this.readableSessionBranch(session);
@@ -1636,6 +1787,60 @@ export class PiSessionService {
1636
1787
  : annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
1637
1788
  return { seq, partial };
1638
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
+ }
1639
1844
  async availableModels(ref, managementContext) {
1640
1845
  const session = await this.getOrOpen(ref, managementContext);
1641
1846
  const models = await this.sessionModelCandidates(session);
@@ -1655,7 +1860,7 @@ export class PiSessionService {
1655
1860
  const scope = this.modelScopeContext(session);
1656
1861
  if (!scope.editable)
1657
1862
  throw new Error("模型可用范围由此工作区的 .pi/settings.json 控制");
1658
- this.assertTreeNavigationInactive(session, "change enabled models");
1863
+ this.assertTreeNavigationInactive(session, "修改已启用模型");
1659
1864
  await session.modelRuntime.refresh({ allowNetwork: false });
1660
1865
  const currentIds = (await this.modelScopeSnapshot(session, scope)).enabledIds;
1661
1866
  const available = session.modelRuntime.getAvailableSnapshot();
@@ -1666,7 +1871,7 @@ export class PiSessionService {
1666
1871
  if (!enabled && session.model !== undefined && modelScopeId(session.model) === targetId)
1667
1872
  throw new Error("不能停用当前模型");
1668
1873
  const nextIds = applyEnabledModelToggle(currentIds, availableIds, targetId, enabled);
1669
- this.assertTreeNavigationInactive(session, "change enabled models");
1874
+ this.assertTreeNavigationInactive(session, "修改已启用模型");
1670
1875
  if (nextIds !== currentIds)
1671
1876
  this.applyEnabledModelScope(session, available, nextIds);
1672
1877
  return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry));
@@ -1681,7 +1886,7 @@ export class PiSessionService {
1681
1886
  const scope = this.modelScopeContext(session);
1682
1887
  if (!scope.editable)
1683
1888
  throw new Error("模型可用范围由此工作区的 .pi/settings.json 控制");
1684
- this.assertTreeNavigationInactive(session, "change enabled models");
1889
+ this.assertTreeNavigationInactive(session, "修改已启用模型");
1685
1890
  await session.modelRuntime.refresh({ allowNetwork: false });
1686
1891
  const available = session.modelRuntime.getAvailableSnapshot();
1687
1892
  const availableIds = available.map(modelScopeId);
@@ -1692,7 +1897,7 @@ export class PiSessionService {
1692
1897
  throw new Error("当前模型不可用");
1693
1898
  nextIds = [modelScopeId(current)];
1694
1899
  }
1695
- this.assertTreeNavigationInactive(session, "change enabled models");
1900
+ this.assertTreeNavigationInactive(session, "修改已启用模型");
1696
1901
  this.applyEnabledModelScope(session, available, nextIds);
1697
1902
  return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry));
1698
1903
  });
@@ -1700,14 +1905,14 @@ export class PiSessionService {
1700
1905
  async setModel(ref, provider, modelId, managementContext) {
1701
1906
  await this.assertWritable(ref);
1702
1907
  const session = await this.getOrOpen(ref, managementContext);
1703
- this.assertTreeNavigationInactive(session, "change models");
1908
+ this.assertTreeNavigationInactive(session, "更改模型");
1704
1909
  const candidates = await this.sessionModelCandidates(session);
1705
- this.assertTreeNavigationInactive(session, "change models");
1910
+ this.assertTreeNavigationInactive(session, "更改模型");
1706
1911
  const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
1707
1912
  ?? session.modelRuntime.getModel(provider, modelId);
1708
1913
  if (model === undefined)
1709
1914
  throw new Error(`未找到模型:${provider}/${modelId}`);
1710
- await this.runSessionEntryMutation(session, "change models", () => session.setModel(model));
1915
+ await this.runSessionEntryMutation(session, "更改模型", () => session.setModel(model));
1711
1916
  this.publishActivity(session, `模型:${model.id}`, "idle", model.provider);
1712
1917
  this.publishStatus(session);
1713
1918
  return this.statusFromSession(session);
@@ -1718,7 +1923,7 @@ export class PiSessionService {
1718
1923
  const result = await this.runModelScopeMutation(async () => {
1719
1924
  await session.modelRuntime.refresh({ allowNetwork: false });
1720
1925
  await this.synchronizeSessionModelScope(session, session.modelRuntime.getAvailableSnapshot());
1721
- return this.runSessionEntryMutation(session, "change models", () => session.cycleModel(direction));
1926
+ return this.runSessionEntryMutation(session, "更改模型", () => session.cycleModel(direction));
1722
1927
  });
1723
1928
  if (result === undefined)
1724
1929
  throw new Error(session.scopedModels.length > 0 ? "作用域内只有一个模型" : "只有一个可用模型");
@@ -1733,14 +1938,14 @@ export class PiSessionService {
1733
1938
  async setThinkingLevel(ref, level, managementContext) {
1734
1939
  await this.assertWritable(ref);
1735
1940
  const session = await this.getOrOpen(ref, managementContext);
1736
- this.assertTreeNavigationInactive(session, "change the thinking level");
1941
+ this.assertTreeNavigationInactive(session, "更改思考级别");
1737
1942
  // pi owns the valid set; validate against the session's live levels rather
1738
1943
  // than a hardcoded union so this stays correct if pi changes the set.
1739
1944
  const available = session.getAvailableThinkingLevels();
1740
1945
  const match = available.find((candidate) => candidate === level);
1741
1946
  if (match === undefined)
1742
1947
  throw new Error(`无效的思考级别:${level}`);
1743
- await this.runSessionEntryMutation(session, "change the thinking level", () => {
1948
+ await this.runSessionEntryMutation(session, "更改思考级别", () => {
1744
1949
  session.setThinkingLevel(match);
1745
1950
  return Promise.resolve();
1746
1951
  });
@@ -1751,7 +1956,7 @@ export class PiSessionService {
1751
1956
  async cycleThinkingLevel(ref, managementContext) {
1752
1957
  await this.assertWritable(ref);
1753
1958
  const session = await this.getOrOpen(ref, managementContext);
1754
- 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()));
1755
1960
  if (level === undefined)
1756
1961
  throw new Error("当前模型不支持思考");
1757
1962
  this.publishActivity(session, `思考级别:${level}`, "idle");
@@ -1765,7 +1970,12 @@ export class PiSessionService {
1765
1970
  commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
1766
1971
  }
1767
1972
  for (const template of session.promptTemplates) {
1768
- 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
+ });
1769
1979
  }
1770
1980
  for (const skill of session.resourceLoader.getSkills().skills) {
1771
1981
  commands.push({ name: `skill:${skill.name}`, ...(skill.description === undefined ? {} : { description: skill.description }), source: "skill" });
@@ -1784,7 +1994,7 @@ export class PiSessionService {
1784
1994
  const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
1785
1995
  await this.assertWritable(ref);
1786
1996
  const session = await this.getOrOpen(ref, options?.managementContext);
1787
- this.assertTreeNavigationInactive(session, "send a prompt");
1997
+ this.assertTreeNavigationInactive(session, "发送消息");
1788
1998
  this.maybeGenerateSessionName(session, promptText);
1789
1999
  const isQueued = session.isStreaming || session.isCompacting;
1790
2000
  const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
@@ -1805,24 +2015,78 @@ export class PiSessionService {
1805
2015
  void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
1806
2016
  }
1807
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) {
1808
2023
  this.auditUserPrompt(session, text);
1809
2024
  this.publishActivity(session, behavior === "steer" ? "插队消息已排队" : behavior === "followUp" ? "消息已排队" : "消息已接收", "active");
1810
2025
  const eventScope = this.eventScopeForSession(session);
1811
2026
  if (behavior === undefined && echoUserMessage)
1812
2027
  this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) }, eventScope);
1813
2028
  const promptOptions = buildPromptOptions(behavior, images);
1814
- 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) => {
1815
2031
  const message = error instanceof Error ? error.message : String(error);
1816
2032
  this.publishActivity(session, "发生错误", "error", message);
1817
2033
  this.events.publish(session.sessionId, { type: "session.error", message }, eventScope);
1818
2034
  });
1819
- void promptPromise;
1820
2035
  return promptPromise;
1821
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
+ }
1822
2085
  enqueuePromptDuringCompaction(session, text, kind, images = [], echoUserMessage = true) {
1823
- const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
2086
+ const sessionKey = activeSessionKey(session.sessionId, this.eventScopeForSession(session));
2087
+ const queue = this.compactionPromptQueues.get(sessionKey) ?? [];
1824
2088
  queue.push({ kind, text, ...(images.length > 0 ? { images } : {}), ...(echoUserMessage ? {} : { echoUserMessage: false }) });
1825
- this.compactionPromptQueues.set(session.sessionId, queue);
2089
+ this.compactionPromptQueues.set(sessionKey, queue);
1826
2090
  this.publishActivity(session, "压缩期间消息已排队", "active");
1827
2091
  this.publishStatus(session);
1828
2092
  }
@@ -1844,7 +2108,7 @@ export class PiSessionService {
1844
2108
  await this.assertWritable(ref);
1845
2109
  const active = await this.getActive(ref, managementContext);
1846
2110
  const { session } = active.runtime;
1847
- this.assertTreeNavigationInactive(session, "run a shell command");
2111
+ this.assertTreeNavigationInactive(session, "运行 Shell 命令");
1848
2112
  const isExcluded = text.startsWith("!!");
1849
2113
  const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
1850
2114
  if (!command)
@@ -1854,7 +2118,7 @@ export class PiSessionService {
1854
2118
  const eventScope = active.eventScope;
1855
2119
  this.publishActivity(session, "正在运行命令", "active", command);
1856
2120
  this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded }, eventScope);
1857
- void this.runSessionEntryMutation(session, "run a shell command", () => session.executeBash(command, (chunk) => {
2121
+ void this.runSessionEntryMutation(session, "运行 Shell 命令", () => session.executeBash(command, (chunk) => {
1858
2122
  this.events.publish(session.sessionId, { type: "shell.chunk", chunk }, eventScope);
1859
2123
  this.publishActivity(session, "正在运行命令", "active", command);
1860
2124
  this.publishStatus(session);
@@ -1889,42 +2153,74 @@ export class PiSessionService {
1889
2153
  }
1890
2154
  async navigateTree(ref, request, managementContext) {
1891
2155
  if (request.targetId.trim() === "")
1892
- throw new Error("Session tree target is required");
2156
+ throw new Error("会话树目标条目不能为空");
1893
2157
  if (this.isTreeExclusiveSessionIdentityActive(sessionIdFromLookup(ref))) {
1894
- throw new Error("Stop current session activity before navigating the session tree");
2158
+ throw new Error("切换会话树前请先停止当前会话活动");
1895
2159
  }
1896
2160
  await this.assertWritable(ref);
1897
2161
  const options = sessionTreeNavigationOptions(request);
1898
2162
  const session = await this.getOrOpen(ref, managementContext);
1899
2163
  if (typeof session.navigateTree !== "function")
1900
- throw new Error("Session tree navigation is not supported by this Pi runtime");
2164
+ throw new Error("当前 Pi 运行时不支持会话树导航");
1901
2165
  if (this.hasActiveWork(session))
1902
- throw new Error("Stop current session activity before navigating the session tree");
2166
+ throw new Error("切换会话树前请先停止当前会话活动");
1903
2167
  // Acquire synchronously after the active-work check. No leaf-producing work
1904
2168
  // may enter this runtime until Pi's potentially asynchronous summary settles.
1905
2169
  this.treeNavigations.add(session);
1906
2170
  try {
1907
- if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
1908
- 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 后重试。");
1909
2174
  }
2175
+ const activeEditableTargetParentId = activeEditableTreeTargetParentId(session.sessionManager, request.targetId, oldLeafId);
1910
2176
  this.publishActivity(session, options.summarize ? "正在汇总分支" : "正在切换会话树", "active");
1911
2177
  this.publishStatus(session);
1912
- 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
+ }
1913
2193
  if (result.cancelled) {
1914
2194
  if (this.isCurrentActiveSession(session)) {
1915
2195
  this.publishActivity(session, result.aborted === true ? "分支汇总已终止" : "会话树切换已取消", "idle");
1916
2196
  }
1917
2197
  return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
1918
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
+ }
1919
2214
  if (this.isCurrentActiveSession(session))
1920
2215
  this.publishActivity(session, "已切换会话树", "idle");
1921
2216
  return { cancelled: false, ...(result.editorText === undefined ? {} : { editorText: result.editorText }) };
1922
2217
  }
1923
2218
  catch (error) {
1924
2219
  const message = error instanceof Error ? error.message : String(error);
1925
- this.publishActivity(session, "重新加载失败", "error", message);
1926
- this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
1927
- 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
+ }
1928
2224
  throw error;
1929
2225
  }
1930
2226
  finally {
@@ -1941,19 +2237,22 @@ export class PiSessionService {
1941
2237
  }
1942
2238
  async forkFromTree(ref, request, managementContext) {
1943
2239
  if (request.entryId.trim() === "")
1944
- throw new Error("Session tree entry is required");
2240
+ throw new Error("会话树条目不能为空");
1945
2241
  const requestedSessionId = sessionIdFromLookup(ref);
1946
2242
  if (this.isTreeExclusiveSessionIdentityActive(requestedSessionId)) {
1947
- throw new Error("Stop current session activity before forking the session tree");
2243
+ throw new Error("分叉会话树前请先停止当前会话活动");
1948
2244
  }
1949
2245
  await this.assertWritable(ref);
1950
2246
  const session = await this.getOrOpen(ref, managementContext);
1951
2247
  if (this.hasActiveWork(session))
1952
- 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
+ }
1953
2252
  const sourceSessionId = session.sessionId;
1954
2253
  const sourceEventScope = this.eventScopeForSession(session);
1955
2254
  const sourceCwd = session.sessionManager.getCwd();
1956
- this.publishActivity(session, "正在从会话树条目分叉", "active");
2255
+ this.publishActivity(session, "正在从条目分叉会话", "active");
1957
2256
  this.publishStatus(session);
1958
2257
  try {
1959
2258
  const result = await this.commandService.forkEntry(session.sessionId, request.entryId, sourceEventScope, {
@@ -1962,10 +2261,10 @@ export class PiSessionService {
1962
2261
  if (result.type === "unsupported")
1963
2262
  throw new Error(result.message);
1964
2263
  if (result.type !== "done")
1965
- throw new Error("Session fork is unavailable");
2264
+ throw new Error("会话分叉不可用");
1966
2265
  if (result.session === undefined) {
1967
2266
  if (this.isCurrentActiveSession(session)) {
1968
- this.publishActivity(session, "会话分叉已取消", "idle");
2267
+ this.publishActivity(session, "分叉已取消", "idle");
1969
2268
  this.publishStatus(session);
1970
2269
  }
1971
2270
  return { cancelled: true };
@@ -1986,7 +2285,7 @@ export class PiSessionService {
1986
2285
  catch (error) {
1987
2286
  const message = error instanceof Error ? error.message : String(error);
1988
2287
  if (this.isCurrentActiveSession(session)) {
1989
- this.publishActivity(session, "会话分叉失败", "error", message);
2288
+ this.publishActivity(session, "分叉失败", "error", message);
1990
2289
  this.events.publish(session.sessionId, { type: "session.error", message }, sourceEventScope);
1991
2290
  this.publishStatus(session);
1992
2291
  }
@@ -2145,8 +2444,6 @@ export class PiSessionService {
2145
2444
  if (this.archiveStore.deleteArchived === undefined)
2146
2445
  throw new Error("归档存储不支持删除");
2147
2446
  await this.closeActiveInScope(record.sessionId, eventScopeFromManagementContext(managementContext));
2148
- if (record.archivePath === undefined)
2149
- await this.ensureArchivedRecordMoved(record);
2150
2447
  await this.archiveStore.deleteArchived(record.sessionId);
2151
2448
  await this.forgetUnreadSessions([record], eventScopeFromManagementContext(managementContext));
2152
2449
  }
@@ -2180,12 +2477,7 @@ export class PiSessionService {
2180
2477
  failures.push({ sessionId: item.record.sessionId, error: errorMessage(error) });
2181
2478
  }
2182
2479
  }
2183
- const moveFailures = await this.moveLegacyArchivedRecordsForDelete(readyRecords);
2184
- failures.push(...moveFailures);
2185
- const moveFailureIds = new Set(moveFailures.map((failure) => failure.sessionId));
2186
- const deleteIds = readyRecords
2187
- .map((record) => record.sessionId)
2188
- .filter((sessionId) => !moveFailureIds.has(sessionId));
2480
+ const deleteIds = readyRecords.map((record) => record.sessionId);
2189
2481
  let deletedSessionIds = [];
2190
2482
  try {
2191
2483
  deletedSessionIds = await this.archiveStoreDeleteArchivedMany(deleteIds);
@@ -2225,6 +2517,7 @@ export class PiSessionService {
2225
2517
  ...(managementContext === undefined ? {} : { managementContext }),
2226
2518
  ...(candidateGeneration === undefined ? {} : { notificationGeneration: candidateGeneration }),
2227
2519
  });
2520
+ await this.reloadModelScope(reopened.runtime.session);
2228
2521
  if (candidateGeneration !== undefined) {
2229
2522
  this.publishNotificationMutations(this.notificationsForSession(session).commitReplacement(candidateGeneration));
2230
2523
  }
@@ -2240,22 +2533,22 @@ export class PiSessionService {
2240
2533
  this.publishStatus(reopenedSession);
2241
2534
  }
2242
2535
  async detachParent(ref, managementContext) {
2536
+ await this.assertWritable(ref);
2243
2537
  const session = await this.getOrOpen(ref, managementContext);
2244
2538
  const sessionFile = session.sessionFile;
2245
2539
  if (sessionFile === undefined || sessionFile === "")
2246
2540
  throw new Error("会话尚未持久化");
2247
2541
  clearSessionFileParent(sessionFile);
2248
- this.sessionManager.invalidateSessionFile?.(sessionFile);
2542
+ this.sessionManager.invalidateSessionFile(sessionFile);
2249
2543
  clearParentSessionHeader(session.sessionManager);
2250
- this.unregisterSubsession(session.sessionId);
2544
+ this.unregisterSubsession(session.sessionId, this.eventScopeForSession(session));
2251
2545
  await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }], this.eventScopeForSession(session));
2252
2546
  }
2253
2547
  async abort(ref, managementContext) {
2254
2548
  const active = this.activeForLookup(ref, managementContext);
2255
2549
  if (active === undefined)
2256
2550
  return;
2257
- const sessionId = active.runtime.session.sessionId;
2258
- this.clearCompactionPromptQueue(sessionId);
2551
+ this.clearCompactionPromptQueue(active.runtime.session);
2259
2552
  clearSessionQueue(active.runtime.session);
2260
2553
  this.abortRunScopedExtensionDialogs(active.runtime.session);
2261
2554
  try {
@@ -2274,25 +2567,15 @@ export class PiSessionService {
2274
2567
  async clearQueue(ref, managementContext) {
2275
2568
  await this.assertWritable(ref);
2276
2569
  const session = await this.getOrOpen(ref, managementContext);
2277
- this.clearCompactionPromptQueue(session.sessionId);
2570
+ this.clearCompactionPromptQueue(session);
2278
2571
  clearSessionQueue(session);
2279
2572
  this.publishStatus(session);
2280
2573
  return this.statusFromSession(session);
2281
2574
  }
2282
2575
  async stop(ref, managementContext) {
2283
- let active = this.activeForLookup(ref, managementContext);
2284
- if (active === undefined) {
2285
- const startup = this.startupSessionForLookup(ref, managementContext);
2286
- if (startup !== undefined) {
2287
- this.endSessionExtensionDialogs(startup);
2288
- const pendingOpens = this.pendingSessionOpenPromises(startup.sessionId);
2289
- if (pendingOpens.length > 0)
2290
- await Promise.allSettled(pendingOpens);
2291
- active = this.activeForLookup(ref, managementContext);
2292
- }
2293
- }
2294
- if (active !== undefined) {
2295
- 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));
2296
2579
  return;
2297
2580
  }
2298
2581
  if (isPiSessionRef(ref)) {
@@ -2310,10 +2593,11 @@ export class PiSessionService {
2310
2593
  async archiveStoreArchiveMany(inputs) {
2311
2594
  if (inputs.length === 0)
2312
2595
  return [];
2596
+ const oldestFirst = [...inputs].sort((a, b) => Date.parse(a.modified) - Date.parse(b.modified));
2313
2597
  if (this.archiveStore.archiveMany !== undefined)
2314
- return this.archiveStore.archiveMany(inputs);
2598
+ return this.archiveStore.archiveMany(oldestFirst);
2315
2599
  const records = [];
2316
- for (const input of inputs)
2600
+ for (const input of oldestFirst)
2317
2601
  records.push(await this.archiveStore.archive(input));
2318
2602
  return records;
2319
2603
  }
@@ -2328,38 +2612,16 @@ export class PiSessionService {
2328
2612
  await this.archiveStore.deleteArchived(sessionId);
2329
2613
  return [...sessionIds];
2330
2614
  }
2331
- async moveLegacyArchivedRecordsForDelete(records) {
2332
- const legacyRecords = records.filter((record) => record.archivePath === undefined);
2333
- if (legacyRecords.length === 0)
2334
- return [];
2335
- let sessionsByCwd;
2336
- try {
2337
- sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
2338
- }
2339
- catch (error) {
2340
- return legacyRecords.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
2341
- }
2342
- const moveInputs = legacyRecords
2343
- .map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
2344
- .filter(isDefined)
2345
- .map(archiveInputFromListEntry);
2346
- if (moveInputs.length === 0)
2347
- return [];
2348
- try {
2349
- await this.archiveStoreArchiveMany(moveInputs);
2350
- return [];
2351
- }
2352
- catch (error) {
2353
- const failedIds = new Set(moveInputs.map((input) => input.sessionId));
2354
- return legacyRecords
2355
- .filter((record) => failedIds.has(record.sessionId))
2356
- .map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
2357
- }
2358
- }
2359
2615
  async cleanupPlan(request) {
2360
- 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
+ ]);
2361
2623
  return planSessionCleanup({
2362
- sessions,
2624
+ sessions: [...sessions, ...[...scopedSessions.values()].flat()],
2363
2625
  archivedRecords,
2364
2626
  activeSessions: this.cleanupActiveSessionStatuses(),
2365
2627
  thresholds: request.thresholds,
@@ -2395,24 +2657,6 @@ export class PiSessionService {
2395
2657
  return record;
2396
2658
  }
2397
2659
  }
2398
- async ensureArchivedRecordMoved(record) {
2399
- const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
2400
- if (session === undefined)
2401
- return record;
2402
- const [moved] = await this.archiveStoreArchiveMany([archiveInputFromListEntry(session)]);
2403
- return moved ?? record;
2404
- }
2405
- async ensureArchivedRecordsMoved(records) {
2406
- const legacyRecords = records.filter((record) => record.archivePath === undefined);
2407
- if (legacyRecords.length === 0)
2408
- return;
2409
- const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
2410
- const moveInputs = legacyRecords
2411
- .map((record) => sessionsByCwd.get(record.cwd)?.find((candidate) => candidate.id === record.sessionId))
2412
- .filter(isDefined)
2413
- .map(archiveInputFromListEntry);
2414
- await this.archiveStoreArchiveMany(moveInputs);
2415
- }
2416
2660
  async archiveInputForSession(session) {
2417
2661
  const cwd = session.sessionManager.getCwd();
2418
2662
  const sessionFile = session.sessionFile;
@@ -2478,7 +2722,7 @@ export class PiSessionService {
2478
2722
  if (startup.session.sessionId === sessionId && startup.eventScope === eventScope)
2479
2723
  this.endSessionExtensionDialogs(startup.session);
2480
2724
  }
2481
- const pendingOpens = this.pendingSessionOpenPromises(sessionId);
2725
+ const pendingOpens = this.pendingSessionOpenPromises(sessionId, eventScope);
2482
2726
  if (pendingOpens.length > 0)
2483
2727
  await Promise.allSettled(pendingOpens);
2484
2728
  const activeSessions = this.activeSessionsForId(sessionId).filter((active) => active.eventScope === eventScope);
@@ -2489,13 +2733,7 @@ export class PiSessionService {
2489
2733
  await Promise.all(activeSessions.map((active) => this.closeActiveSession(active, notificationPolicy)));
2490
2734
  }
2491
2735
  async closeActiveInScope(sessionId, eventScope, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
2492
- const active = this.activeForSessionIdAndScope(sessionId, eventScope);
2493
- if (active !== undefined) {
2494
- await this.closeActiveSession(active, notificationPolicy);
2495
- }
2496
- else if (notificationPolicy.kind === "clear") {
2497
- this.publishNotificationMutations(this.notificationsForScope(eventScope).clearSession(sessionId, notificationPolicy.reason));
2498
- }
2736
+ await this.closeActive(sessionId, notificationPolicy, eventScope);
2499
2737
  }
2500
2738
  async closeActiveSession(active, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
2501
2739
  const sessionId = active.runtime.session.sessionId;
@@ -2508,18 +2746,18 @@ export class PiSessionService {
2508
2746
  this.publishNotificationMutations(mutations);
2509
2747
  }
2510
2748
  this.forgetUnreadActivity(active.runtime.session);
2511
- this.pendingAskStore.forgetSession(sessionId);
2749
+ this.pendingAskStore.forgetSession(this.dialogSessionKey(active.runtime.session));
2512
2750
  this.endSessionExtensionDialogs(active.runtime.session);
2513
2751
  this.active.delete(activeSessionKey(sessionId, active.eventScope));
2514
2752
  this.activities.delete(activeSessionKey(sessionId, active.eventScope));
2515
2753
  this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
2516
2754
  this.clearAuthLossWarningsForSession(sessionId);
2517
- this.clearCompactionPromptQueue(sessionId);
2755
+ this.clearCompactionPromptQueue(active.runtime.session);
2518
2756
  // Disarm subsession notification before teardown so the abort below cannot
2519
2757
  // emit a "stopped working" event that notifies the parent (e.g. on archive).
2520
2758
  // The parent/children link is kept so the parent can still see the child.
2521
2759
  if (this.subsessionLinkForActiveChild(active.runtime.session) !== undefined)
2522
- this.subsessionNotifyArmed.delete(sessionId);
2760
+ this.subsessionNotifyArmed.delete(activeSessionKey(sessionId, active.eventScope));
2523
2761
  clearSessionQueue(active.runtime.session);
2524
2762
  active.unsubscribe();
2525
2763
  active.runtime.setRebindSession(undefined);
@@ -2527,6 +2765,8 @@ export class PiSessionService {
2527
2765
  await this.abortSessionOperations(active.runtime.session);
2528
2766
  }
2529
2767
  finally {
2768
+ if (active.eventScope === NORMAL_SESSION_EVENT_SCOPE)
2769
+ await this.activityMarker.release(active.runtime.session.sessionFile);
2530
2770
  await active.runtime.dispose();
2531
2771
  }
2532
2772
  }
@@ -2585,7 +2825,9 @@ export class PiSessionService {
2585
2825
  const leafId = session.sessionManager.getLeafId();
2586
2826
  const latestEntry = session.sessionManager.getEntries?.().at(-1);
2587
2827
  // Unsummarized tree navigation changes only the SDK's in-memory leaf.
2588
- 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)
2589
2831
  return session.sessionManager.getBranch();
2590
2832
  const sessionFile = session.sessionFile ?? session.sessionManager.getSessionFile();
2591
2833
  const resolvedPath = sessionFile === undefined ? await this.idleSessionFilePath(session) : undefined;
@@ -2597,19 +2839,37 @@ export class PiSessionService {
2597
2839
  const snapshot = await this.sessionManager.readBranch(path);
2598
2840
  if (snapshot === undefined)
2599
2841
  return session.sessionManager.getBranch();
2600
- 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;
2601
2854
  }
2602
2855
  async idleSessionFilePath(session) {
2603
2856
  const cached = this.idleSessionFileResolutions.get(session);
2604
2857
  const at = this.now().getTime();
2605
2858
  if (cached !== undefined && at - cached.at < IDLE_SESSION_FILE_RESOLUTION_THROTTLE_MS)
2606
2859
  return cached.path;
2607
- const match = await this.sessionManager.resolveSessionFile?.(session.sessionManager.getCwd(), session.sessionId);
2860
+ const match = await this.sessionManager.resolveSessionFile(session.sessionManager.getCwd(), session.sessionId);
2608
2861
  this.idleSessionFileResolutions.set(session, { at, path: match?.path });
2609
2862
  return match?.path;
2610
2863
  }
2611
2864
  async getActive(ref, managementContext) {
2612
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;
2613
2873
  if (active !== undefined)
2614
2874
  return active;
2615
2875
  const archived = await this.getArchived(ref);
@@ -2618,10 +2878,8 @@ export class PiSessionService {
2618
2878
  return this.openPending(archived.sessionId, archived.cwd, managementContext, () => this.sessionManager.open(archivePath), { notifications: "disabled" });
2619
2879
  }
2620
2880
  const match = isPiSessionRef(ref)
2621
- ? this.sessionManager.resolveSessionFile === undefined
2622
- ? findSessionByIdOrPrefix(await this.sessionManager.list(ref.cwd), ref.id)
2623
- : await this.sessionManager.resolveSessionFile(ref.cwd, ref.id)
2624
- : findSessionByIdOrPrefix(await this.sessionManager.listAll?.() ?? [], ref);
2881
+ ? await this.sessionManager.resolveSessionFile(ref.cwd, ref.id)
2882
+ : findSessionByIdOrPrefix(await this.sessionManager.listAll(), ref);
2625
2883
  if (!match)
2626
2884
  throw new Error("未找到会话");
2627
2885
  return this.openPending(match.id, match.cwd, managementContext, () => this.sessionManager.open(match.path));
@@ -2631,8 +2889,12 @@ export class PiSessionService {
2631
2889
  const existing = this.pendingSessionOpens.get(key);
2632
2890
  if (existing !== undefined)
2633
2891
  return existing.promise;
2892
+ const active = this.activeForLookup({ id: sessionId, cwd }, managementContext);
2893
+ if (active !== undefined)
2894
+ return Promise.resolve(active);
2634
2895
  const pending = {
2635
2896
  sessionId,
2897
+ eventScope: eventScopeFromManagementContext(managementContext),
2636
2898
  promise: this.create(openSessionManager(), cwd, {
2637
2899
  ...options,
2638
2900
  ...(managementContext === undefined ? {} : { managementContext }),
@@ -2645,9 +2907,9 @@ export class PiSessionService {
2645
2907
  this.pendingSessionOpens.set(key, pending);
2646
2908
  return pending.promise;
2647
2909
  }
2648
- pendingSessionOpenPromises(sessionId) {
2910
+ pendingSessionOpenPromises(sessionId, eventScope) {
2649
2911
  return [...this.pendingSessionOpens.values()]
2650
- .filter((pending) => sessionId === undefined || pending.sessionId === sessionId)
2912
+ .filter((pending) => (sessionId === undefined || pending.sessionId === sessionId) && (eventScope === undefined || pending.eventScope === eventScope))
2651
2913
  .map((pending) => pending.promise);
2652
2914
  }
2653
2915
  async getArchived(ref) {
@@ -2696,6 +2958,21 @@ export class PiSessionService {
2696
2958
  throw new Error(`Session not active: ${sessionId}`);
2697
2959
  return this.submitPrompt(active.runtime.session, text, undefined, [], false);
2698
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
+ }
2699
2976
  activeForSessionId(sessionId) {
2700
2977
  return this.activeSessionsForId(sessionId)[0];
2701
2978
  }
@@ -2707,17 +2984,25 @@ export class PiSessionService {
2707
2984
  activeSessionsForId(sessionId) {
2708
2985
  return [...new Set(this.active.values())].filter((active) => active.runtime.session.sessionId === sessionId);
2709
2986
  }
2710
- async create(sessionManager, cwd, options = {}) {
2987
+ create(sessionManager, cwd, options = {}) {
2711
2988
  const startup = this.startupProgress(sessionManager, options.startupIntent ?? "open", options.startupToken, eventScopeFromManagementContext(options.managementContext));
2712
- try {
2713
- return await this.createSessionRuntime(sessionManager, cwd, options, startup);
2714
- }
2715
- 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(() => {
2716
2996
  startup.end();
2717
- }
2997
+ if (this.pendingSessionOpens.get(key) === pending)
2998
+ this.pendingSessionOpens.delete(key);
2999
+ });
3000
+ this.pendingSessionOpens.set(key, pending);
3001
+ return pending.promise;
2718
3002
  }
2719
- async bindSessionExtensions(session, generation, owner) {
2720
- 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);
2721
3006
  const key = activeSessionKey(session.sessionId, owner.eventScope);
2722
3007
  this.startupSessions.set(key, { session, ...owner });
2723
3008
  try {
@@ -2732,6 +3017,9 @@ export class PiSessionService {
2732
3017
  });
2733
3018
  }
2734
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;
2735
3023
  if (this.startupSessions.get(key)?.session === session)
2736
3024
  this.startupSessions.delete(key);
2737
3025
  }
@@ -2740,50 +3028,66 @@ export class PiSessionService {
2740
3028
  this.sessionEventScopes.set(session, active.eventScope);
2741
3029
  this.runtimeBySession.set(session, active.runtime);
2742
3030
  active.unsubscribe();
2743
- for (const [sessionId, candidate] of this.active.entries()) {
3031
+ for (const [sessionKey, candidate] of this.active.entries()) {
2744
3032
  if (candidate === active) {
2745
- this.active.delete(sessionId);
2746
- if (!sessionId.endsWith(`\0${session.sessionId}`))
2747
- this.clearCompactionPromptQueue(candidate.runtime.session.sessionId);
3033
+ this.active.delete(sessionKey);
3034
+ if (sessionKey !== activeSessionKey(session.sessionId, active.eventScope))
3035
+ this.clearCompactionPromptQueueKey(sessionKey);
2748
3036
  }
2749
3037
  }
2750
- 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) => {
2751
3042
  this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel), active.eventScope);
2752
3043
  this.auditToolExecutionEvent(active, event);
2753
3044
  this.auditAssistantResponseEvent(active, event);
2754
3045
  this.publishActivityForEvent(session, event);
2755
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
+ }
2756
3054
  if (eventType === "agent_end")
2757
3055
  this.abortRunScopedExtensionDialogs(session);
2758
3056
  if (eventType === "compaction_end")
2759
- this.scheduleCompactionQueueDrain(session.sessionId);
3057
+ this.scheduleCompactionQueueDrain(session);
2760
3058
  if (eventType === "agent_start" || eventType === "agent_end")
2761
- this.scheduleCompactionQueueDrain(session.sessionId);
3059
+ this.scheduleCompactionQueueDrain(session);
2762
3060
  this.publishStatus(session);
2763
3061
  this.updateSubsessionTracking(session);
2764
3062
  });
3063
+ active.unsubscribe = () => {
3064
+ if (active.eventScope === NORMAL_SESSION_EVENT_SCOPE)
3065
+ this.sessionEvents.close(session);
3066
+ unsubscribe();
3067
+ };
2765
3068
  this.active.set(activeSessionKey(session.sessionId, active.eventScope), active);
2766
3069
  }
2767
- scheduleCompactionQueueDrain(sessionId, delayMs = 0) {
2768
- 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))
2769
3074
  return;
2770
3075
  const timer = setTimeout(() => {
2771
- this.compactionDrainTimers.delete(sessionId);
2772
- this.drainCompactionPromptQueue(sessionId);
3076
+ this.compactionDrainTimers.delete(sessionKey);
3077
+ this.drainCompactionPromptQueue(session);
2773
3078
  }, delayMs);
2774
- this.compactionDrainTimers.set(sessionId, timer);
3079
+ this.compactionDrainTimers.set(sessionKey, timer);
2775
3080
  }
2776
- drainCompactionPromptQueue(sessionId) {
2777
- const active = this.activeForSessionId(sessionId);
2778
- if (active === undefined)
3081
+ drainCompactionPromptQueue(session) {
3082
+ if (!this.isCurrentActiveSession(session))
2779
3083
  return;
2780
- const { session } = active.runtime;
3084
+ const sessionKey = activeSessionKey(session.sessionId, this.eventScopeForSession(session));
2781
3085
  if (session.isCompacting) {
2782
- this.scheduleCompactionQueueDrain(sessionId, 100);
3086
+ this.scheduleCompactionQueueDrain(session, 100);
2783
3087
  return;
2784
3088
  }
2785
3089
  if (session.isStreaming) {
2786
- const queued = this.takeCompactionPromptQueue(sessionId);
3090
+ const queued = this.takeCompactionPromptQueue(sessionKey);
2787
3091
  if (queued.length === 0)
2788
3092
  return;
2789
3093
  this.publishStatus(session);
@@ -2791,12 +3095,12 @@ export class PiSessionService {
2791
3095
  void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images, prompt.echoUserMessage ?? true);
2792
3096
  return;
2793
3097
  }
2794
- const prompt = this.shiftCompactionPrompt(sessionId);
3098
+ const prompt = this.shiftCompactionPrompt(sessionKey);
2795
3099
  if (prompt === undefined)
2796
3100
  return;
2797
3101
  this.publishStatus(session);
2798
3102
  const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images, prompt.echoUserMessage ?? true);
2799
- void submitted.finally(() => { this.scheduleCompactionQueueDrain(sessionId); });
3103
+ void submitted.finally(() => { this.scheduleCompactionQueueDrain(session); });
2800
3104
  }
2801
3105
  takeCompactionPromptQueue(sessionId) {
2802
3106
  const queued = this.compactionPromptQueues.get(sessionId) ?? [];
@@ -2810,12 +3114,15 @@ export class PiSessionService {
2810
3114
  this.compactionPromptQueues.delete(sessionId);
2811
3115
  return prompt;
2812
3116
  }
2813
- clearCompactionPromptQueue(sessionId) {
2814
- this.compactionPromptQueues.delete(sessionId);
2815
- 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);
2816
3123
  if (timer !== undefined) {
2817
3124
  clearTimeout(timer);
2818
- this.compactionDrainTimers.delete(sessionId);
3125
+ this.compactionDrainTimers.delete(sessionKey);
2819
3126
  }
2820
3127
  }
2821
3128
  clearCompactionDrainTimers() {
@@ -2904,6 +3211,7 @@ export class PiSessionService {
2904
3211
  // the session still reports active work transiently, so the event-driven
2905
3212
  // latch may not fire. The heartbeat re-checks once the session settles.
2906
3213
  this.updateSubsessionTracking(session);
3214
+ void this.refreshActivityMarker(session);
2907
3215
  const activity = this.activities.get(activeSessionKey(session.sessionId, active.eventScope));
2908
3216
  if (!this.hasActiveWork(session)) {
2909
3217
  if (activity?.phase === "active")
@@ -2936,7 +3244,7 @@ export class PiSessionService {
2936
3244
  return this.treeNavigations.has(session)
2937
3245
  || this.isSessionEntryMutationActive(session)
2938
3246
  || this.isTreeExclusiveOperationActive(session)
2939
- || sessionHasActiveWork(session, this.compactionQueuedMessages(session.sessionId).length);
3247
+ || sessionHasActiveWork(session, this.compactionQueuedMessages(session).length);
2940
3248
  }
2941
3249
  async runTreeExclusiveOperation(targets, activeError, operation) {
2942
3250
  const sessionIds = new Set();
@@ -2996,7 +3304,7 @@ export class PiSessionService {
2996
3304
  }
2997
3305
  assertTreeNavigationInactive(session, action) {
2998
3306
  if (this.treeNavigations.has(session))
2999
- throw new Error(`Cannot ${action} while session tree navigation is active`);
3307
+ throw new Error(`会话树导航期间无法${action}`);
3000
3308
  }
3001
3309
  async runSessionEntryMutation(session, action, operation) {
3002
3310
  this.beginSessionEntryMutation(session, action);
@@ -3130,16 +3438,16 @@ export class PiSessionService {
3130
3438
  const message = getProperty(event, "message");
3131
3439
  if (getString(message, "role") !== "assistant")
3132
3440
  return;
3133
- this.recordManagementContentAudit(active, "assistant_response", auditVisibleAssistantMessage(message));
3441
+ this.recordManagementMessageAudit(active, "assistant_response", auditVisibleAssistantMessage(message));
3134
3442
  }
3135
3443
  auditUserPrompt(session, text) {
3136
3444
  const active = [...new Set(this.active.values())]
3137
3445
  .find(candidate => candidate.runtime.session === session);
3138
3446
  if (active === undefined)
3139
3447
  return;
3140
- this.recordManagementContentAudit(active, "user_prompt", text);
3448
+ this.recordManagementMessageAudit(active, "user_prompt", text);
3141
3449
  }
3142
- recordManagementContentAudit(active, action, content) {
3450
+ recordManagementMessageAudit(active, action, content) {
3143
3451
  const identity = active.managementAuditIdentity;
3144
3452
  if (identity === undefined || this.managementAudit === undefined)
3145
3453
  return;
@@ -3158,7 +3466,7 @@ export class PiSessionService {
3158
3466
  sessionId: active.runtime.session.sessionId,
3159
3467
  action,
3160
3468
  error: error instanceof Error ? error.message : String(error),
3161
- }, "failed to enqueue management-mode content audit");
3469
+ }, "failed to enqueue management-mode message audit");
3162
3470
  }
3163
3471
  }
3164
3472
  /**
@@ -3220,7 +3528,20 @@ export class PiSessionService {
3220
3528
  this.events.publishGlobal({ type: "activity.update", activity }, eventScope);
3221
3529
  this.observeUnreadActivityState(session);
3222
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
+ }
3223
3543
  publishStatus(session) {
3544
+ void this.refreshActivityMarker(session);
3224
3545
  const status = this.statusFromSession(session);
3225
3546
  this.clearStaleActiveActivity(session);
3226
3547
  const eventScope = this.eventScopeForSession(session);
@@ -3262,7 +3583,7 @@ export class PiSessionService {
3262
3583
  const eventScope = this.eventScopeForSession(session);
3263
3584
  return eventScope === NORMAL_SESSION_EVENT_SCOPE ? session.sessionId : activeSessionKey(session.sessionId, eventScope);
3264
3585
  }
3265
- publishStatusForSessionId(sessionId) {
3586
+ publishStatusForSessionId(sessionId, scope) {
3266
3587
  const sessions = new Set();
3267
3588
  for (const active of this.activeSessionsForId(sessionId))
3268
3589
  sessions.add(active.runtime.session);
@@ -3270,15 +3591,17 @@ export class PiSessionService {
3270
3591
  if (startup.session.sessionId === sessionId)
3271
3592
  sessions.add(startup.session);
3272
3593
  }
3273
- for (const session of sessions)
3274
- this.publishStatus(session);
3594
+ for (const session of sessions) {
3595
+ if (scope === undefined || this.eventScopeForSession(session) === scope)
3596
+ this.publishStatus(session);
3597
+ }
3275
3598
  }
3276
3599
  statusFromSession(session, messageCount = session.messages.length) {
3277
3600
  const stats = session.getSessionStats();
3278
3601
  const model = session.model === undefined ? undefined : modelToClientModel(session.model);
3279
3602
  const contextUsage = session.getContextUsage();
3280
3603
  const warnings = this.warningsForSession(session);
3281
- const pendingAsk = this.pendingAskStore.pendingAsk(session.sessionId);
3604
+ const pendingAsk = this.pendingAskStore.pendingAsk(this.dialogSessionKey(session));
3282
3605
  const pendingDialogs = this.pendingExtensionDialogStore.pendingDialogs(this.dialogSessionKey(session));
3283
3606
  return {
3284
3607
  sessionId: session.sessionId,
@@ -3289,7 +3612,7 @@ export class PiSessionService {
3289
3612
  isCompacting: session.isCompacting,
3290
3613
  isBashRunning: session.isBashRunning,
3291
3614
  pendingMessageCount: this.pendingMessageCount(session),
3292
- queuedMessages: queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)),
3615
+ queuedMessages: queuedMessagesFromSession(session, this.compactionQueuedMessages(session)),
3293
3616
  messageCount,
3294
3617
  tokens: stats.tokens,
3295
3618
  cost: stats.cost,
@@ -3308,19 +3631,28 @@ export class PiSessionService {
3308
3631
  warningsForSession(session) {
3309
3632
  const runtime = [...this.active.values()].find((active) => active.runtime.session === session)?.runtime;
3310
3633
  const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
3311
- 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);
3312
3644
  if (anthropic !== undefined)
3313
3645
  warnings.push(anthropic);
3314
3646
  return warnings;
3315
3647
  }
3316
3648
  pendingMessageCount(session) {
3317
- return session.pendingMessageCount + this.compactionQueuedMessages(session.sessionId).length;
3649
+ return session.pendingMessageCount + this.compactionQueuedMessages(session).length;
3318
3650
  }
3319
- compactionQueuedMessages(sessionId) {
3320
- return this.compactionPromptQueues.get(sessionId) ?? [];
3651
+ compactionQueuedMessages(session) {
3652
+ return this.compactionPromptQueues.get(activeSessionKey(session.sessionId, this.eventScopeForSession(session))) ?? [];
3321
3653
  }
3322
3654
  hasQueuedMessageText(session, text) {
3323
- return queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)).some((message) => message.text === text);
3655
+ return queuedMessagesFromSession(session, this.compactionQueuedMessages(session)).some((message) => message.text === text);
3324
3656
  }
3325
3657
  notificationsForScope(scope) {
3326
3658
  if (scope === NORMAL_SESSION_EVENT_SCOPE)
@@ -3357,6 +3689,9 @@ export class PiSessionService {
3357
3689
  await this.publishUnreadMutations([]);
3358
3690
  return this.unreadForScope(eventScope).durableCatalogSnapshot();
3359
3691
  }
3692
+ async reconcileUnreadWorkspaces(cwds) {
3693
+ await this.publishUnreadMutations(this.unreadStore.reconcileWorkspaces(cwds));
3694
+ }
3360
3695
  async acknowledgeUnread(sessionId, request, context) {
3361
3696
  const store = this.unreadForScope(eventScopeFromManagementContext(context));
3362
3697
  const result = store.acknowledge(sessionId, {
@@ -3380,7 +3715,7 @@ export class PiSessionService {
3380
3715
  return result.snapshot;
3381
3716
  }
3382
3717
  async deliverSubsessionNotification(session, notification) {
3383
- 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" }));
3384
3719
  this.publishStatus(session);
3385
3720
  }
3386
3721
  logSubsessionNotificationFailure(parentId, childId, error) {
@@ -3401,6 +3736,7 @@ export class PiSessionService {
3401
3736
  this.replaceSessionNotificationContext(session, candidateGeneration);
3402
3737
  },
3403
3738
  });
3739
+ await this.reloadModelScope(session);
3404
3740
  if (candidateGeneration !== undefined) {
3405
3741
  this.publishNotificationMutations(this.notificationsForSession(session).commitReplacement(candidateGeneration));
3406
3742
  }
@@ -3414,44 +3750,34 @@ export class PiSessionService {
3414
3750
  }
3415
3751
  const message = error instanceof Error ? error.message : String(error);
3416
3752
  this.publishActivity(session, "重新加载失败", "error", message);
3417
- this.events.publish(session.sessionId, { type: "session.error", message });
3753
+ this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
3418
3754
  this.publishStatus(session);
3419
3755
  throw error;
3420
3756
  }
3421
3757
  });
3422
3758
  }
3423
- async dismissWarning(ref, dismissId) {
3424
- const session = await this.getOrOpen(ref);
3759
+ async dismissWarning(ref, dismissId, managementContext) {
3760
+ const session = await this.getOrOpen(ref, managementContext);
3425
3761
  dismissSessionWarning(session, dismissId);
3426
3762
  this.publishStatus(session);
3427
3763
  return this.statusFromSession(session);
3428
3764
  }
3429
3765
  openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
3430
- const active = this.activeForLookup({ id: sessionId, cwd }, options.managementContext);
3431
- if (active !== undefined)
3432
- return Promise.resolve(active);
3433
- const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
3434
- const existing = this.pendingSessionOpens.get(key);
3435
- if (existing !== undefined)
3436
- return existing.promise;
3437
- const pending = {
3438
- sessionId,
3439
- promise: this.create(openSessionManager(), cwd, options),
3440
- };
3441
- pending.promise = pending.promise.finally(() => {
3442
- if (this.pendingSessionOpens.get(key) === pending)
3443
- this.pendingSessionOpens.delete(key);
3444
- });
3445
- this.pendingSessionOpens.set(key, pending);
3446
- return pending.promise;
3766
+ return this.openPending(sessionId, cwd, options.managementContext, openSessionManager, options);
3447
3767
  }
3448
3768
  isCurrentActiveSession(session) {
3449
3769
  return [...this.active.values()].some((active) => active.runtime.session === session);
3450
3770
  }
3451
3771
  async createSessionRuntime(sessionManager, cwd, options, startup) {
3452
3772
  startup.report(STARTUP_PHASE_RUNTIME);
3453
- const delegationToolsEnabled = options.creationProvenance !== "tracked-subsession"
3454
- && 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);
3455
3781
  const runtime = await this.createAgentRuntime(this.createRuntime, {
3456
3782
  cwd,
3457
3783
  agentDir: this.agentDir,
@@ -3472,6 +3798,13 @@ export class PiSessionService {
3472
3798
  };
3473
3799
  let boundSession = runtime.session;
3474
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
+ }
3475
3808
  const notificationStore = this.notificationsForScope(active.eventScope);
3476
3809
  let notificationGeneration = options.notificationGeneration;
3477
3810
  let notificationOwnership = options.notifications === "disabled"
@@ -3495,17 +3828,23 @@ export class PiSessionService {
3495
3828
  if (notificationGeneration !== undefined)
3496
3829
  this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
3497
3830
  try {
3831
+ startupSignal?.throwIfAborted();
3498
3832
  if (options.creationProvenance === "tracked-subsession") {
3499
3833
  await this.publishUnreadMutations(this.unreadForScope(active.eventScope).excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
3500
3834
  }
3501
3835
  else {
3502
3836
  await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
3503
3837
  }
3838
+ startupSignal?.throwIfAborted();
3504
3839
  startup.report(STARTUP_PHASE_EXTENSIONS);
3505
- await this.bindSessionExtensions(runtime.session, notificationGeneration, active);
3840
+ // Startup hooks emit through the same scoped stream as hosted turns.
3506
3841
  this.bindRuntime(active);
3842
+ await this.bindSessionExtensions(runtime.session, notificationGeneration, active, startupSignal);
3843
+ startupSignal?.throwIfAborted();
3507
3844
  runtime.setRebindSession(async (session) => {
3508
3845
  this.sessionEventScopes.set(session, active.eventScope);
3846
+ if (options.managementContext !== undefined)
3847
+ this.sessionManagementContexts.set(session, options.managementContext);
3509
3848
  const priorGeneration = notificationGeneration;
3510
3849
  let candidateGeneration;
3511
3850
  try {
@@ -3536,6 +3875,7 @@ export class PiSessionService {
3536
3875
  throw error;
3537
3876
  }
3538
3877
  });
3878
+ startupSignal?.throwIfAborted();
3539
3879
  this.active.set(activeSessionKey(runtime.session.sessionId, active.eventScope), active);
3540
3880
  if (notificationOwnership === "replacement" && notificationGeneration !== undefined) {
3541
3881
  this.publishNotificationMutations(notificationStore.commitReplacement(notificationGeneration));
@@ -3563,7 +3903,7 @@ export class PiSessionService {
3563
3903
  this.active.delete(sessionKey);
3564
3904
  this.activities.delete(sessionKey);
3565
3905
  this.clearAuthLossWarningsForSession(runtime.session.sessionId);
3566
- this.clearCompactionPromptQueue(runtime.session.sessionId);
3906
+ this.clearCompactionPromptQueue(runtime.session);
3567
3907
  removedActive = true;
3568
3908
  }
3569
3909
  if (removedActive) {
@@ -3577,11 +3917,14 @@ export class PiSessionService {
3577
3917
  }
3578
3918
  throw error;
3579
3919
  }
3920
+ finally {
3921
+ startupSignal?.removeEventListener("abort", cancelStartup);
3922
+ }
3580
3923
  }
3581
3924
  replaceSessionNotificationContext(session, generation) {
3582
3925
  session.extensionRunner.setUIContext(this.sessionUiContext(session, generation), "rpc");
3583
3926
  }
3584
- sessionUiContext(session, generation) {
3927
+ sessionUiContext(session, generation, startupDialogs = {}) {
3585
3928
  const baseUiContext = session.extensionRunner.getUIContext();
3586
3929
  const eventScope = this.eventScopeForSession(session);
3587
3930
  const notificationStore = this.notificationsForScope(eventScope);
@@ -3596,14 +3939,6 @@ export class PiSessionService {
3596
3939
  }
3597
3940
  const added = notificationStore.addNotification(generation, message, type);
3598
3941
  this.publishNotificationMutations(added.mutations);
3599
- if (added.notification === undefined)
3600
- return;
3601
- this.events.publish(session.sessionId, {
3602
- type: "command.output",
3603
- level: type === "error" ? "error" : "info",
3604
- message,
3605
- notificationId: added.notification.id,
3606
- }, eventScope);
3607
3942
  };
3608
3943
  // PI WEB owns the browser-facing dialog, notification, and text-formatting
3609
3944
  // boundaries: the three dialog primitives park daemon-held Promises that
@@ -3617,13 +3952,13 @@ export class PiSessionService {
3617
3952
  if (property === "theme")
3618
3953
  return plainTextTheme;
3619
3954
  if (property === "confirm") {
3620
- 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);
3621
3956
  }
3622
3957
  if (property === "select") {
3623
- 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);
3624
3959
  }
3625
3960
  if (property === "input") {
3626
- 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);
3627
3962
  }
3628
3963
  const value = Reflect.get(target, property, receiver);
3629
3964
  return value;
@@ -3664,11 +3999,12 @@ export class PiSessionService {
3664
3999
  }
3665
4000
  publishUnreadMutations(mutations) {
3666
4001
  if (mutations.length > 0) {
3667
- const scopes = new Set();
4002
+ const scopes = new Map();
3668
4003
  for (const mutation of mutations) {
3669
- 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);
3670
4006
  }
3671
- scopes.forEach((scope) => this.onUnreadChanged?.(scope));
4007
+ scopes.forEach((hasNewCompletion, scope) => this.onUnreadChanged?.(scope, hasNewCompletion));
3672
4008
  }
3673
4009
  this.enqueueUnreadMutations(mutations);
3674
4010
  this.unreadPublicationFlushRequested = true;
@@ -3849,7 +4185,7 @@ function clientSessionFromListEntry(session) {
3849
4185
  created: session.created.toISOString(),
3850
4186
  modified: session.modified.toISOString(),
3851
4187
  messageCount: session.messageCount,
3852
- firstMessage: session.firstMessage,
4188
+ firstMessage: clientSessionFirstMessagePreview(session.firstMessage),
3853
4189
  ...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
3854
4190
  };
3855
4191
  }
@@ -4083,37 +4419,12 @@ function buildPromptOptions(behavior, images) {
4083
4419
  options.images = images;
4084
4420
  return Object.keys(options).length > 0 ? options : undefined;
4085
4421
  }
4086
- function stringValue(value) {
4087
- return typeof value === "string" ? value : "";
4088
- }
4089
4422
  function historyMessages(session) {
4090
4423
  return historyMessagesFromManager(session.sessionManager);
4091
4424
  }
4092
4425
  function historyMessagesFromManager(sessionManager) {
4093
4426
  return historyMessagesFromEntries(sessionManager.getBranch());
4094
4427
  }
4095
- function historyMessagesFromEntries(entries) {
4096
- const messages = [];
4097
- let thinkingLevel;
4098
- for (const entry of entries) {
4099
- if (!isRecord(entry))
4100
- continue;
4101
- if (entry["type"] === "message")
4102
- messages.push(annotateAssistantThinkingLevel(entry["message"], thinkingLevel));
4103
- else if (entry["type"] === "thinking_level_change") {
4104
- const level = getString(entry, "thinkingLevel");
4105
- if (level !== undefined)
4106
- thinkingLevel = level;
4107
- }
4108
- else if (entry["type"] === "custom_message" && entry["display"] === true)
4109
- messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
4110
- else if (entry["type"] === "compaction")
4111
- messages.push({ role: "system", source: "compaction", content: `Compacted history:\n\n${stringValue(entry["summary"])}` });
4112
- else if (entry["type"] === "branch_summary")
4113
- messages.push({ role: "system", source: "branch_summary", content: `Branch summary:\n\n${stringValue(entry["summary"])}` });
4114
- }
4115
- return messages;
4116
- }
4117
4428
  function transcriptMessageCount(entries) {
4118
4429
  let count = 0;
4119
4430
  for (const entry of entries) {
@@ -4122,6 +4433,30 @@ function transcriptMessageCount(entries) {
4122
4433
  }
4123
4434
  return count;
4124
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
+ }
4125
4460
  /** custom entry type used to persist parent -> child subsession links outside LLM context. */
4126
4461
  const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
4127
4462
  /** custom entry type used to mark a child as created by spawn_subsession. */