@jmfederico/pi-web 1.202607.3 → 1.202608.1

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 (236) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +69 -7
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/{CodeViewer-CDVbMiN9.js → CodeViewer-mEiBZFA5.js} +2 -2
  5. package/dist/client/assets/{TerminalPanel-CTS1CgqF.js → TerminalPanel-O9381Eme.js} +6 -6
  6. package/dist/client/assets/index-uI2AGFUy.js +4091 -0
  7. package/dist/client/index.html +1 -1
  8. package/dist/config.js +109 -73
  9. package/dist/config.js.map +1 -1
  10. package/dist/docker/piWebDockerCommandPlan.js +4 -3
  11. package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
  12. package/dist/nativeServices/servicePlan.js +1 -1
  13. package/dist/nativeServices/servicePlan.js.map +1 -1
  14. package/dist/pi-web-plugins/git/browser/git-contract.js +108 -0
  15. package/dist/pi-web-plugins/git/browser/git-panel.js +609 -0
  16. package/dist/pi-web-plugins/git/browser/gitFileList.js +43 -0
  17. package/dist/pi-web-plugins/git/browser/gitFileShared.js +16 -0
  18. package/dist/pi-web-plugins/git/browser/gitFileTree.js +81 -0
  19. package/dist/pi-web-plugins/git/browser/gitFileViewPreference.js +35 -0
  20. package/dist/pi-web-plugins/git/browser/gitRoute.js +42 -0
  21. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +10 -0
  22. package/dist/pi-web-plugins/git/browser/unifiedDiff.js +301 -0
  23. package/dist/{server/git/gitService.js → pi-web-plugins/git/git-backend.js} +209 -60
  24. package/dist/pi-web-plugins/git/package.json +16 -0
  25. package/dist/pi-web-plugins/git/server-plugin.js +176 -0
  26. package/dist/pi-web-plugins/info/infoInternals.js +1 -2
  27. package/dist/pi-web-plugins/info/package.json +1 -1
  28. package/dist/pi-web-plugins/info/pi-web-plugin.js +2 -2
  29. package/dist/pi-web-plugins/relays/package.json +1 -1
  30. package/dist/pi-web-plugins/relays/pi-web-plugin.js +3 -3
  31. package/dist/pi-web-plugins/relays/relayDiscovery.js +159 -27
  32. package/dist/pi-web-plugins/relays/relaysPanelElement.js +173 -16
  33. package/dist/pi-web-plugins/updates/package.json +1 -1
  34. package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
  35. package/dist/pi-web-plugins/workspace-tasks/package.json +1 -1
  36. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +3 -3
  37. package/dist/plugin-api.d.ts +33 -16
  38. package/dist/pluginRecoveryCli.js +172 -0
  39. package/dist/pluginRecoveryCli.js.map +1 -0
  40. package/dist/server/activity/workspaceActivityService.js +27 -19
  41. package/dist/server/activity/workspaceActivityService.js.map +1 -1
  42. package/dist/server/app.js +35 -37
  43. package/dist/server/app.js.map +1 -1
  44. package/dist/server/browserMessageProjection.js +0 -2
  45. package/dist/server/browserMessageProjection.js.map +1 -1
  46. package/dist/server/configRoutes.js +4 -31
  47. package/dist/server/configRoutes.js.map +1 -1
  48. package/dist/server/machines/machineClient.js +23 -4
  49. package/dist/server/machines/machineClient.js.map +1 -1
  50. package/dist/server/machines/machinePluginProxyRoutes.js +74 -17
  51. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  52. package/dist/server/machines/machineProxyRoutes.js +167 -21
  53. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  54. package/dist/server/machines/machineService.js +22 -0
  55. package/dist/server/machines/machineService.js.map +1 -1
  56. package/dist/server/piWebPluginCatalog.js +584 -0
  57. package/dist/server/piWebPluginCatalog.js.map +1 -0
  58. package/dist/server/piWebPluginLifecycle.js +162 -0
  59. package/dist/server/piWebPluginLifecycle.js.map +1 -0
  60. package/dist/server/piWebPluginService.js +159 -261
  61. package/dist/server/piWebPluginService.js.map +1 -1
  62. package/dist/server/piWebStatus.js +40 -45
  63. package/dist/server/piWebStatus.js.map +1 -1
  64. package/dist/server/plugins/pluginBackendProxyRoutes.js +69 -0
  65. package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -0
  66. package/dist/server/plugins/serverPluginExec.js +184 -0
  67. package/dist/server/plugins/serverPluginExec.js.map +1 -0
  68. package/dist/server/plugins/serverPluginRuntime.js +480 -0
  69. package/dist/server/plugins/serverPluginRuntime.js.map +1 -0
  70. package/dist/server/realtime/sessionEventHub.js +26 -11
  71. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  72. package/dist/server/requestCancellation.js +32 -0
  73. package/dist/server/requestCancellation.js.map +1 -0
  74. package/dist/server/sessiond/agentHttpDispatcher.js +108 -0
  75. package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -0
  76. package/dist/server/sessiond/agentProcessEnvironment.js +61 -0
  77. package/dist/server/sessiond/agentProcessEnvironment.js.map +1 -0
  78. package/dist/server/sessiond/pluginBackendRoutes.js +64 -0
  79. package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -0
  80. package/dist/server/sessiond/sessionDaemonShutdown.js +24 -0
  81. package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -0
  82. package/dist/server/sessiond/sessionProxyRoutes.js +2 -2
  83. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  84. package/dist/server/sessiond/sessionServiceDependencies.js +3 -1
  85. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  86. package/dist/server/sessiond/sessiondStateOwnership.js +235 -0
  87. package/dist/server/sessiond/sessiondStateOwnership.js.map +1 -0
  88. package/dist/server/sessiond/workspaceCatalogRoutes.js +31 -0
  89. package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -0
  90. package/dist/server/sessiond/workspaceRemovalRoutes.js +41 -0
  91. package/dist/server/sessiond/workspaceRemovalRoutes.js.map +1 -0
  92. package/dist/server/sessiond.js +283 -96
  93. package/dist/server/sessiond.js.map +1 -1
  94. package/dist/server/sessions/askUserTool.js +0 -3
  95. package/dist/server/sessions/askUserTool.js.map +1 -1
  96. package/dist/server/sessions/authRoutes.js +0 -10
  97. package/dist/server/sessions/authRoutes.js.map +1 -1
  98. package/dist/server/sessions/authService.js +15 -78
  99. package/dist/server/sessions/authService.js.map +1 -1
  100. package/dist/server/sessions/dockerEnvironmentFacts.js +254 -0
  101. package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -0
  102. package/dist/server/sessions/messagePaging.js +2 -2
  103. package/dist/server/sessions/messagePaging.js.map +1 -1
  104. package/dist/server/sessions/modelCatalogRefresher.js +4 -3
  105. package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
  106. package/dist/server/sessions/oauthLoginFlowService.js +2 -5
  107. package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
  108. package/dist/server/sessions/pendingAskStore.js +0 -3
  109. package/dist/server/sessions/pendingAskStore.js.map +1 -1
  110. package/dist/server/sessions/piSessionManagerGateway.js +134 -3
  111. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
  112. package/dist/server/sessions/piSessionService.js +318 -253
  113. package/dist/server/sessions/piSessionService.js.map +1 -1
  114. package/dist/server/sessions/sessionCommandService.js +24 -1
  115. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  116. package/dist/server/sessions/sessionEnvironmentFacts.js +48 -0
  117. package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -0
  118. package/dist/server/sessions/sessionFileFormat.js +27 -0
  119. package/dist/server/sessions/sessionFileFormat.js.map +1 -0
  120. package/dist/server/sessions/sessionFileHeader.js +82 -23
  121. package/dist/server/sessions/sessionFileHeader.js.map +1 -1
  122. package/dist/server/sessions/sessionRoutes.js +88 -56
  123. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  124. package/dist/server/sessions/sessionSummaryScanner.js +491 -0
  125. package/dist/server/sessions/sessionSummaryScanner.js.map +1 -0
  126. package/dist/server/sessions/spawnSessionTool.js +8 -1
  127. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  128. package/dist/server/sessions/spawnSubsessionTool.js +11 -9
  129. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  130. package/dist/server/sessions/spawnTargetResolver.js +1 -1
  131. package/dist/server/status/machineStatusRoutes.js +8 -0
  132. package/dist/server/status/machineStatusRoutes.js.map +1 -0
  133. package/dist/server/status/machineStatusService.js +181 -0
  134. package/dist/server/status/machineStatusService.js.map +1 -0
  135. package/dist/server/status/workspaceAttribution.js +87 -0
  136. package/dist/server/status/workspaceAttribution.js.map +1 -0
  137. package/dist/server/terminalProxyRoutes.js +2 -1
  138. package/dist/server/terminalProxyRoutes.js.map +1 -1
  139. package/dist/server/workspaceExplorerRoutes.js +20 -13
  140. package/dist/server/workspaceExplorerRoutes.js.map +1 -1
  141. package/dist/server/workspaces/effectivePathAccess.js +0 -19
  142. package/dist/server/workspaces/effectivePathAccess.js.map +1 -1
  143. package/dist/server/workspaces/fileContentService.js +18 -9
  144. package/dist/server/workspaces/fileContentService.js.map +1 -1
  145. package/dist/server/workspaces/filePreviewResponseHeaders.js +18 -0
  146. package/dist/server/workspaces/filePreviewResponseHeaders.js.map +1 -0
  147. package/dist/server/workspaces/filePreviewResponsePolicy.js +55 -0
  148. package/dist/server/workspaces/filePreviewResponsePolicy.js.map +1 -0
  149. package/dist/server/workspaces/filePreviewService.js +81 -0
  150. package/dist/server/workspaces/filePreviewService.js.map +1 -0
  151. package/dist/server/workspaces/projectWorkspaceCwds.js +0 -5
  152. package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -1
  153. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +402 -0
  154. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -0
  155. package/dist/server/workspaces/workspaceCatalog.js +47 -0
  156. package/dist/server/workspaces/workspaceCatalog.js.map +1 -0
  157. package/dist/server/workspaces/workspaceContext.js +1 -3
  158. package/dist/server/workspaces/workspaceContext.js.map +1 -1
  159. package/dist/server/workspaces/workspaceDeletionRoutes.js +39 -103
  160. package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
  161. package/dist/server/workspaces/workspaceProviderRegistry.js +651 -0
  162. package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -0
  163. package/dist/server/workspaces/workspaceRemovalService.js +256 -0
  164. package/dist/server/workspaces/workspaceRemovalService.js.map +1 -0
  165. package/dist/server/workspaces/workspaceRouteErrors.js +7 -0
  166. package/dist/server/workspaces/workspaceRouteErrors.js.map +1 -0
  167. package/dist/server/workspaces/worktreePreRemoveHook.js +39 -0
  168. package/dist/server/workspaces/worktreePreRemoveHook.js.map +1 -0
  169. package/dist/server-plugin-api.d.ts +140 -0
  170. package/dist/server-plugin-api.js +2 -0
  171. package/dist/server-plugin-api.js.map +1 -0
  172. package/dist/serverPluginRecovery.js +138 -0
  173. package/dist/serverPluginRecovery.js.map +1 -0
  174. package/dist/sessiond/activeAgentProfile.js +3 -22
  175. package/dist/sessiond/activeAgentProfile.js.map +1 -1
  176. package/dist/sessiond/config.js +13 -2
  177. package/dist/sessiond/config.js.map +1 -1
  178. package/dist/sessiond/sessionDaemonClient.js +11 -9
  179. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  180. package/dist/shared/activeAgentProfile.js +2 -41
  181. package/dist/shared/activeAgentProfile.js.map +1 -1
  182. package/dist/shared/activity.js +0 -3
  183. package/dist/shared/activity.js.map +1 -1
  184. package/dist/shared/apiTypes.js +7 -14
  185. package/dist/shared/apiTypes.js.map +1 -1
  186. package/dist/shared/capabilities.js +8 -40
  187. package/dist/shared/capabilities.js.map +1 -1
  188. package/dist/shared/federatedRoutes.js +28 -8
  189. package/dist/shared/federatedRoutes.js.map +1 -1
  190. package/dist/shared/machinePluginIds.js +4 -2
  191. package/dist/shared/machinePluginIds.js.map +1 -1
  192. package/dist/shared/machineStatus.js +94 -0
  193. package/dist/shared/machineStatus.js.map +1 -0
  194. package/dist/shared/piWebStatusParsing.js +29 -0
  195. package/dist/shared/piWebStatusParsing.js.map +1 -1
  196. package/dist/shared/pluginApiTypes.d.ts +148 -0
  197. package/dist/shared/pluginApiTypes.js +3 -0
  198. package/dist/shared/pluginApiTypes.js.map +1 -0
  199. package/dist/shared/pluginBackendProtocol.js +110 -0
  200. package/dist/shared/pluginBackendProtocol.js.map +1 -0
  201. package/dist/shared/pluginIds.js +5 -0
  202. package/dist/shared/pluginIds.js.map +1 -1
  203. package/dist/shared/pluginRecoveryCommands.js +14 -0
  204. package/dist/shared/pluginRecoveryCommands.js.map +1 -0
  205. package/dist/shared/workspaceFiles.js +41 -2
  206. package/dist/shared/workspaceFiles.js.map +1 -1
  207. package/dist/shared/workspaceRemovalProtocol.js +24 -0
  208. package/dist/shared/workspaceRemovalProtocol.js.map +1 -0
  209. package/docs/config.md +140 -47
  210. package/docs/plugins.md +386 -137
  211. package/examples/workspace-provider-plugin/README.md +52 -0
  212. package/examples/workspace-provider-plugin/package.json +25 -0
  213. package/examples/workspace-provider-plugin/src/browser/index.ts +69 -0
  214. package/examples/workspace-provider-plugin/src/server.ts +64 -0
  215. package/examples/workspace-provider-plugin/tsconfig.json +21 -0
  216. package/package.json +32 -12
  217. package/server-plugin-api.d.ts +1 -0
  218. package/dist/client/assets/UnifiedDiffViewer-Cqke12ZX.js +0 -39
  219. package/dist/client/assets/index-LME0LfPb.js +0 -4068
  220. package/dist/plugin-api/unstable.d.ts +0 -22
  221. package/dist/server/activity/workspaceActivityRoutes.js +0 -4
  222. package/dist/server/activity/workspaceActivityRoutes.js.map +0 -1
  223. package/dist/server/git/gitService.js.map +0 -1
  224. package/dist/server/gitRoutes.js +0 -23
  225. package/dist/server/gitRoutes.js.map +0 -1
  226. package/dist/server/sessions/parentSessionLocator.js +0 -75
  227. package/dist/server/sessions/parentSessionLocator.js.map +0 -1
  228. package/dist/server/workspaces/gitWorktreeDiscovery.js +0 -43
  229. package/dist/server/workspaces/gitWorktreeDiscovery.js.map +0 -1
  230. package/dist/server/workspaces/imagePreviewService.js +0 -40
  231. package/dist/server/workspaces/imagePreviewService.js.map +0 -1
  232. package/dist/server/workspaces/workspaceService.js +0 -52
  233. package/dist/server/workspaces/workspaceService.js.map +0 -1
  234. package/dist/shared/apiTypes.d.ts +0 -1234
  235. package/dist/shared/thinkingLevels.d.ts +0 -27
  236. package/plugin-api/unstable.d.ts +0 -1
@@ -16,8 +16,6 @@ import { parsePromptAttachments } from "../../shared/promptAttachments.js";
16
16
  import { ASK_USER_ANSWERS_CUSTOM_TYPE, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
17
17
  import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
18
18
  import { readSessionHeaderSummary } from "./sessionFileHeader.js";
19
- import { countOutOfListingChildren, locateOutOfListingParents } from "./parentSessionLocator.js";
20
- import { siblingWorkspaceCwds } from "../workspaces/projectWorkspaceCwds.js";
21
19
  import { createAskUserToolDefinition } from "./askUserTool.js";
22
20
  import { PendingAskStore, renderAskUserAnswersText } from "./pendingAskStore.js";
23
21
  import { PendingExtensionDialogStore } from "./pendingExtensionDialogStore.js";
@@ -56,20 +54,44 @@ function spawnTargetError(decision) {
56
54
  return new Error("Spawning session is not in a registered project");
57
55
  return new Error(`cwd must be a workspace of this project. Allowed: ${decision.allowedCwds.join(", ")}`);
58
56
  }
59
- function authLossWarningKey(sessionId, provider, modelId) {
60
- return `${sessionId}:${provider}/${modelId}`;
57
+ /**
58
+ * Tracked subsessions are worktree-scoped, so a requested target other than the
59
+ * parent's own cwd fails closed instead of being silently retargeted. The
60
+ * message names the rule and both supported ways to get work done elsewhere.
61
+ */
62
+ function subsessionCwdError(spawningCwd, requestedCwd) {
63
+ return new Error(`A tracked subsession runs in this session's working directory (${spawningCwd}); ${requestedCwd} was requested. Instruct the child to work elsewhere from this workspace, or use spawn_session for an independent session in another workspace.`);
61
64
  }
62
- function sessionIdFromLookup(ref) {
63
- return typeof ref === "string" ? ref : ref.id;
65
+ function modelSpecOf(model) {
66
+ return `${model.provider}/${model.id}`;
64
67
  }
65
- function isPiSessionRef(ref) {
66
- return typeof ref !== "string";
68
+ /**
69
+ * Parse a strict `provider/model-id` spec: split on the first `/` (model ids
70
+ * may themselves contain `/`) and require both parts to be non-empty.
71
+ */
72
+ function parseModelSpec(spec) {
73
+ const slash = spec.indexOf("/");
74
+ if (slash <= 0 || slash === spec.length - 1)
75
+ return undefined;
76
+ return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
67
77
  }
68
- function lookupMatchesActiveSession(ref, active) {
69
- return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
78
+ /**
79
+ * Error for a spawn-tool model spec that matched nothing. States the facts —
80
+ * the bad spec and the required format — with deliberately no model list
81
+ * (a list would invite guesses). The agent loop turns the throw into an
82
+ * error tool result; how to recover is the agent's call.
83
+ */
84
+ function unknownSpawnModelError(modelSpec) {
85
+ return new Error(`Unknown model "${modelSpec}". Pass an exact "provider/model-id".`);
70
86
  }
71
- function lookupMatchesStartupSession(ref, session) {
72
- return !isPiSessionRef(ref) || cwdPathsEqual(session.sessionManager.getCwd(), ref.cwd);
87
+ function authLossWarningKey(sessionId, provider, modelId) {
88
+ return `${sessionId}:${provider}/${modelId}`;
89
+ }
90
+ function refMatchesActiveSession(ref, active) {
91
+ return cwdPathsEqual(active.runtime.cwd, ref.cwd);
92
+ }
93
+ function refMatchesStartupSession(ref, session) {
94
+ return cwdPathsEqual(session.sessionManager.getCwd(), ref.cwd);
73
95
  }
74
96
  function sessionTreeNavigationOptions(request) {
75
97
  switch (request.summary.mode) {
@@ -220,26 +242,31 @@ export function dismissSessionWarning(session, dismissId) {
220
242
  function defaultCreateAgentRuntime(createRuntime, options) {
221
243
  if (!(options.sessionManager instanceof SessionManager))
222
244
  throw new Error("Default runtime creation requires an SDK SessionManager");
223
- const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.delegationToolsEnabled);
245
+ const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.initialThinkingLevel, options.delegationToolsEnabled);
224
246
  return createAgentSessionRuntime(runtimeFactory, {
225
247
  cwd: options.cwd,
226
248
  agentDir: options.agentDir,
227
249
  sessionManager: options.sessionManager,
228
250
  });
229
251
  }
230
- function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, delegationToolsEnabled) {
252
+ function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, initialThinkingLevel, delegationToolsEnabled) {
231
253
  // These inputs belong only to the session being opened. A later runtime
232
- // replacement resolves its own model and delegation capability.
254
+ // replacement resolves its own model and delegation capability, and restores
255
+ // the thinking level from the existing session file.
233
256
  let pendingInitialModel = initialModel;
257
+ let pendingInitialThinkingLevel = initialThinkingLevel;
234
258
  let pendingDelegationToolsEnabled = delegationToolsEnabled;
235
259
  return async (options) => {
236
260
  const model = pendingInitialModel;
261
+ const thinkingLevel = pendingInitialThinkingLevel;
237
262
  const toolsEnabled = pendingDelegationToolsEnabled;
238
263
  pendingInitialModel = undefined;
264
+ pendingInitialThinkingLevel = undefined;
239
265
  pendingDelegationToolsEnabled = undefined;
240
266
  return createRuntime({
241
267
  ...options,
242
268
  ...(model === undefined ? {} : { initialModel: model }),
269
+ ...(thinkingLevel === undefined ? {} : { initialThinkingLevel: thinkingLevel }),
243
270
  ...(toolsEnabled === undefined ? {} : { delegationToolsEnabled: toolsEnabled }),
244
271
  });
245
272
  };
@@ -254,9 +281,28 @@ export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn,
254
281
  ...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
255
282
  ];
256
283
  }
257
- function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, askUser) {
258
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => {
259
- const services = await createAgentSessionServices({ cwd, agentDir, modelRuntime });
284
+ /**
285
+ * Resource-loader options that append PI WEB's own system-prompt sections.
286
+ *
287
+ * `appendSystemPromptOverride` composes with what the loader already resolved,
288
+ * so the operator's `SYSTEM.md` / `APPEND_SYSTEM.md` files keep their content
289
+ * and PI WEB's sections land after them. Returns `undefined` when there is
290
+ * nothing to add, leaving the loader exactly as pi configures it.
291
+ */
292
+ export function piWebResourceLoaderOptions(appendSystemPromptSections) {
293
+ if (appendSystemPromptSections.length === 0)
294
+ return undefined;
295
+ return { appendSystemPromptOverride: (base) => [...base, ...appendSystemPromptSections] };
296
+ }
297
+ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, askUser, appendSystemPromptSections = []) {
298
+ const resourceLoaderOptions = piWebResourceLoaderOptions(appendSystemPromptSections);
299
+ return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
300
+ const services = await createAgentSessionServices({
301
+ cwd,
302
+ agentDir,
303
+ modelRuntime,
304
+ ...(resourceLoaderOptions === undefined ? {} : { resourceLoaderOptions }),
305
+ });
260
306
  const resolvedDelegationToolsEnabled = delegationToolsEnabled
261
307
  ?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
262
308
  const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions, askUser);
@@ -266,6 +312,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
266
312
  customTools,
267
313
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
268
314
  ...(initialModel === undefined ? {} : { model: initialModel }),
315
+ ...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
269
316
  });
270
317
  return { ...result, services, diagnostics: services.diagnostics };
271
318
  };
@@ -325,8 +372,6 @@ export class PiSessionService {
325
372
  this.subsessionLinks = new Map();
326
373
  /** Parent id/file identities whose persisted links have already been loaded. */
327
374
  this.subsessionHydratedParents = new Set();
328
- /** Session file path -> its parsed header. Headers are written once, so successful reads are cached. */
329
- this.sessionHeaderCache = new Map();
330
375
  /**
331
376
  * Tracked subsession id -> whether a completion notification is armed.
332
377
  * Armed when the child starts working; firing on completion disarms it so a
@@ -339,37 +384,23 @@ export class PiSessionService {
339
384
  this.pendingUnreadMutations = [];
340
385
  this.unreadPublicationFlushRequested = false;
341
386
  this.unreadPublicationStopped = false;
342
- /**
343
- * Read a session file header, memoized per path. Pi writes the header once at
344
- * session creation, so a successful read stays valid for the process lifetime;
345
- * failures are not cached so a session file that appears later is picked up.
346
- */
347
- this.readCachedSessionHeader = async (sessionFile) => {
348
- const cached = this.sessionHeaderCache.get(sessionFile);
349
- if (cached !== undefined)
350
- return cached;
351
- const header = await readSessionHeaderSummary(sessionFile);
352
- if (header !== undefined)
353
- this.sessionHeaderCache.set(sessionFile, header);
354
- return header;
355
- };
356
387
  this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
357
388
  this.agentDir = deps.agentDir;
358
389
  this.sessionManager = deps.sessionManager;
359
390
  this.modelRuntime = deps.modelRuntime;
360
391
  this.spawnTargets = deps.spawnTargets;
361
- this.projectWorkspaces = deps.projectWorkspaces;
362
392
  this.logger = deps.logger ?? noopLogger;
363
393
  this.now = deps.now ?? (() => new Date());
364
394
  this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
365
395
  this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
396
+ this.onUnreadChanged = deps.onUnreadChanged;
366
397
  this.pendingAskStore = deps.pendingAskStore ?? new PendingAskStore();
367
398
  this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
368
399
  this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
369
400
  this.catalogRefreshStatus = deps.catalogRefreshStatus;
370
401
  this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
371
402
  this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
372
- // Subsessions are a beta capability gated behind their own flag, and they
403
+ // Subsessions are gated behind their own flag, and they
373
404
  // also require the spawn capability (they share its project-scope resolver).
374
405
  const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
375
406
  this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
@@ -377,11 +408,11 @@ export class PiSessionService {
377
408
  list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
378
409
  check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
379
410
  read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
380
- }, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined);
411
+ }, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined, deps.appendSystemPromptSections ?? []);
381
412
  this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
382
413
  this.workspaceActivity = deps.workspaceActivity;
383
414
  this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
384
- this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, { echoUserMessage: false }), events, {
415
+ this.commandService = new SessionCommandService((sessionId) => this.getActive(this.activeSessionRef(sessionId)), (sessionId, text) => this.prompt(this.activeSessionRef(sessionId), text, undefined, undefined, { echoUserMessage: false }), events, {
385
416
  onCompactionStart: (session) => {
386
417
  this.beginSessionEntryMutation(session, "compact the session");
387
418
  this.publishActivity(session, "compacting", "active");
@@ -465,7 +496,6 @@ export class PiSessionService {
465
496
  await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
466
497
  readyDeleteRecords.push(record);
467
498
  }
468
- await this.ensureArchivedRecordsMoved(readyDeleteRecords);
469
499
  const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
470
500
  deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
471
501
  await this.forgetUnreadSessions(deleteRecords);
@@ -506,7 +536,6 @@ export class PiSessionService {
506
536
  this.subsessionLinks.clear();
507
537
  this.subsessionHydratedParents.clear();
508
538
  this.subsessionNotifyArmed.clear();
509
- this.sessionHeaderCache.clear();
510
539
  this.notificationStore.clearAll("service-dispose");
511
540
  await Promise.all(activeSessions.map(async (active) => {
512
541
  active.unsubscribe();
@@ -524,9 +553,7 @@ export class PiSessionService {
524
553
  async list(cwd) {
525
554
  const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
526
555
  const sessionsById = new Map(sessions.map((session) => [session.id, session]));
527
- const archivedForCwd = await Promise.all(archivedRecords
528
- .filter((record) => record.cwd === cwd)
529
- .map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
556
+ const archivedForCwd = archivedRecords.filter((record) => record.cwd === cwd);
530
557
  const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
531
558
  for (const record of archivedForCwd) {
532
559
  this.publishNotificationMutations(this.notificationStore.clearSession(record.sessionId, "archive-reconcile"));
@@ -539,63 +566,7 @@ export class PiSessionService {
539
566
  .sort(compareArchivedRecords)
540
567
  .map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
541
568
  .filter(isDefined);
542
- return await this.withRelatedSessionsElsewhere([...unarchivedSessions, ...archivedSessions], cwd);
543
- }
544
- /**
545
- * Annotate a listing with the session relationships that cross workspace
546
- * boundaries: where an out-of-listing parent lives, and how many children a
547
- * listed session has in sibling workspaces.
548
- *
549
- * Both directions are best-effort. An unreadable parent header or a sibling
550
- * workspace that cannot be listed leaves the session unannotated rather than
551
- * failing the listing, and the browser falls back to its generic states.
552
- */
553
- async withRelatedSessionsElsewhere(sessions, cwd) {
554
- const [parentLocations, childCounts] = await Promise.all([
555
- locateOutOfListingParents(sessions, cwd, this.readCachedSessionHeader),
556
- this.countChildrenInSiblingWorkspaces(sessions, cwd),
557
- ]);
558
- return sessions.map((session) => {
559
- const parent = session.parentSessionPath === undefined ? undefined : parentLocations.get(session.parentSessionPath);
560
- const childrenElsewhere = childCounts.get(session.path);
561
- if (parent === undefined && childrenElsewhere === undefined)
562
- return session;
563
- const annotated = { ...session };
564
- if (parent !== undefined) {
565
- annotated.parentSessionId = parent.parentSessionId;
566
- annotated.parentSessionCwd = parent.parentSessionCwd;
567
- }
568
- if (childrenElsewhere !== undefined)
569
- annotated.childSessionsElsewhere = childrenElsewhere;
570
- return annotated;
571
- });
572
- }
573
- /**
574
- * Count children of the listed sessions that live in other workspaces of the
575
- * same project.
576
- *
577
- * Only sibling workspaces are scanned: agents may only spawn into workspaces
578
- * of the spawning session's own project, so that bounds where a child can be.
579
- * Listing is skipped entirely when no project-workspace locator is configured
580
- * or the cwd belongs to no registered project.
581
- */
582
- async countChildrenInSiblingWorkspaces(sessions, cwd) {
583
- if (this.projectWorkspaces === undefined || sessions.length === 0)
584
- return new Map();
585
- try {
586
- const siblingCwds = await siblingWorkspaceCwds(this.projectWorkspaces, cwd);
587
- if (siblingCwds.length === 0)
588
- return new Map();
589
- const listings = await Promise.all(siblingCwds.map(async (siblingCwd) => {
590
- const entries = await this.sessionManager.list(siblingCwd);
591
- return entries.flatMap((entry) => entry.parentSessionPath === undefined ? [] : [entry.parentSessionPath]);
592
- }));
593
- return countOutOfListingChildren(sessions, listings.flat());
594
- }
595
- catch (error) {
596
- this.logger.info({ cwd, error: error instanceof Error ? error.message : String(error) }, "failed to count child sessions in sibling workspaces");
597
- return new Map();
598
- }
569
+ return [...unarchivedSessions, ...archivedSessions];
599
570
  }
600
571
  async start(cwd, options = {}) {
601
572
  return this.startSession(cwd, options);
@@ -605,6 +576,7 @@ export class PiSessionService {
605
576
  startupIntent: "create",
606
577
  ...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
607
578
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
579
+ ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
608
580
  ...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
609
581
  });
610
582
  const { session } = active.runtime;
@@ -637,28 +609,55 @@ export class PiSessionService {
637
609
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
638
610
  if (!decision.allowed)
639
611
  throw spawnTargetError(decision);
640
- const created = await this.start(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
641
- await this.prompt(created.id, input.prompt);
612
+ // A model spec overrides the inherited model. Only a spec resolves
613
+ // against the spawning session; the default path must not depend on it.
614
+ const model = input.modelSpec === undefined
615
+ ? input.model
616
+ : await this.resolveSpawnModel({ id: input.spawningSessionId, cwd: input.spawningCwd }, input.modelSpec);
617
+ const created = await this.start(decision.cwd, {
618
+ ...(model === undefined ? {} : { initialModel: model }),
619
+ ...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
620
+ });
621
+ const modelUsed = this.active.get(created.id)?.runtime.session.model;
622
+ await this.prompt(created, input.prompt);
642
623
  this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
643
- return { sessionId: created.id, cwd: decision.cwd };
624
+ return {
625
+ sessionId: created.id,
626
+ cwd: decision.cwd,
627
+ ...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
628
+ };
644
629
  }
645
630
  /**
646
- * Start a *tracked* child session on behalf of a LLM. Identical to
647
- * {@link spawnSession} in how the target cwd is resolved, but the child
648
- * records its parent (so it shows in the session tree) and is registered so
649
- * the parent is notified when it stops working and can inspect it later.
631
+ * Start a *tracked* child session on behalf of a LLM. Unlike
632
+ * {@link spawnSession}, a tracked child always runs in the parent's own
633
+ * workspace: parent/child trees are worktree-scoped, so a child elsewhere
634
+ * would be invisible to the parent's listing. The child records its parent
635
+ * (so it shows in the session tree) and is registered so the parent is
636
+ * notified when it stops working and can inspect it later.
650
637
  */
651
638
  async spawnSubsession(input) {
652
639
  if (this.spawnTargets === undefined)
653
640
  throw new Error("Spawning sessions is disabled");
654
- const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
641
+ if (input.cwd !== undefined && input.cwd !== "" && !cwdPathsEqual(input.cwd, input.spawningCwd)) {
642
+ throw subsessionCwdError(input.spawningCwd, input.cwd);
643
+ }
644
+ // Resolved against the parent's own cwd only: this still refuses spawning
645
+ // from an unregistered directory, which keeps the child visible in the UI.
646
+ const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, undefined);
655
647
  if (!decision.allowed)
656
648
  throw spawnTargetError(decision);
649
+ // A model spec overrides the inherited model and is resolved against the
650
+ // parent's model runtime; only a spec resolves against the parent.
651
+ const model = input.modelSpec === undefined
652
+ ? input.model
653
+ : await this.resolveSpawnModel({ id: input.parentSessionId, cwd: input.spawningCwd }, input.modelSpec);
657
654
  const created = await this.startSession(decision.cwd, {
658
655
  ...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
659
- ...(input.model === undefined ? {} : { initialModel: input.model }),
656
+ ...(model === undefined ? {} : { initialModel: model }),
657
+ ...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
660
658
  creationProvenance: "tracked-subsession",
661
659
  });
660
+ const modelUsed = this.active.get(created.id)?.runtime.session.model;
662
661
  const parentSessionFile = nonEmptyString(input.parentSessionFile);
663
662
  const link = {
664
663
  parentSessionId: input.parentSessionId,
@@ -670,9 +669,45 @@ export class PiSessionService {
670
669
  await this.registerVerifiedSubsession(link);
671
670
  this.persistSubsessionLink(link);
672
671
  this.persistSubsessionChildMarker(input.parentSessionId, created.id);
673
- await this.prompt(created.id, input.prompt);
672
+ await this.prompt(created, input.prompt);
674
673
  this.logger.info({ parentSessionId: input.parentSessionId, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_subsession started a tracked child session");
675
- return { sessionId: created.id, cwd: decision.cwd };
674
+ return {
675
+ sessionId: created.id,
676
+ cwd: decision.cwd,
677
+ ...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
678
+ };
679
+ }
680
+ /**
681
+ * The models a session may pick from: its scoped set when model-scoped,
682
+ * otherwise the runtime's available snapshot. Refreshes the runtime catalog
683
+ * first so callers see newly configured providers and models. The refresh
684
+ * stays local (`allowNetwork: false`); network refreshes belong to the
685
+ * bounded background catalog refresher, not this request path.
686
+ */
687
+ async sessionModelCandidates(session) {
688
+ await session.modelRuntime.refresh({ allowNetwork: false });
689
+ return session.scopedModels.length > 0
690
+ ? session.scopedModels.map((scoped) => scoped.model)
691
+ : session.modelRuntime.getAvailableSnapshot();
692
+ }
693
+ /**
694
+ * Resolve a strict `provider/model-id` spec from a spawn tool against the
695
+ * *spawning* session's model runtime, using the same candidates
696
+ * {@link setModel} offers plus a direct runtime lookup as fallback. Unknown
697
+ * or malformed specs throw; the agent loop turns that into an error tool
698
+ * result the spawning agent can retry from.
699
+ */
700
+ async resolveSpawnModel(spawningRef, modelSpec) {
701
+ const session = await this.getOrOpen(spawningRef);
702
+ const parsed = parseModelSpec(modelSpec);
703
+ const candidates = await this.sessionModelCandidates(session);
704
+ const model = parsed === undefined
705
+ ? undefined
706
+ : candidates.find((candidate) => candidate.provider === parsed.provider && candidate.id === parsed.modelId)
707
+ ?? session.modelRuntime.getModel(parsed.provider, parsed.modelId);
708
+ if (model === undefined)
709
+ throw unknownSpawnModelError(modelSpec);
710
+ return model;
676
711
  }
677
712
  /**
678
713
  * Register the question set an agent wants the user to answer as the session's
@@ -1240,26 +1275,20 @@ export class PiSessionService {
1240
1275
  const streamingMessage = session.state.streamingMessage;
1241
1276
  const partial = streamingMessage === undefined || streamingMessage === null
1242
1277
  ? null
1243
- : projectBrowserMessage(streamingMessage);
1278
+ : annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
1244
1279
  return { seq, partial };
1245
1280
  }
1246
1281
  async availableModels(ref) {
1247
1282
  const session = await this.getOrOpen(ref);
1248
- await session.modelRuntime.refresh();
1249
- const models = session.scopedModels.length > 0
1250
- ? session.scopedModels.map((scoped) => scoped.model)
1251
- : session.modelRuntime.getAvailableSnapshot();
1283
+ const models = await this.sessionModelCandidates(session);
1252
1284
  return models.map(modelToClientModel);
1253
1285
  }
1254
1286
  async setModel(ref, provider, modelId) {
1255
1287
  await this.assertWritable(ref);
1256
1288
  const session = await this.getOrOpen(ref);
1257
1289
  this.assertTreeNavigationInactive(session, "change models");
1258
- await session.modelRuntime.refresh();
1290
+ const candidates = await this.sessionModelCandidates(session);
1259
1291
  this.assertTreeNavigationInactive(session, "change models");
1260
- const candidates = session.scopedModels.length > 0
1261
- ? session.scopedModels.map((scoped) => scoped.model)
1262
- : session.modelRuntime.getAvailableSnapshot();
1263
1292
  const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
1264
1293
  ?? session.modelRuntime.getModel(provider, modelId);
1265
1294
  if (model === undefined)
@@ -1432,7 +1461,7 @@ export class PiSessionService {
1432
1461
  async navigateTree(ref, request) {
1433
1462
  if (request.targetId.trim() === "")
1434
1463
  throw new Error("Session tree target is required");
1435
- if (this.isTreeExclusiveSessionIdentityActive(sessionIdFromLookup(ref))) {
1464
+ if (this.isTreeExclusiveSessionIdentityActive(ref.id)) {
1436
1465
  throw new Error("Stop current session activity before navigating the session tree");
1437
1466
  }
1438
1467
  await this.assertWritable(ref);
@@ -1482,6 +1511,83 @@ export class PiSessionService {
1482
1511
  }
1483
1512
  }
1484
1513
  }
1514
+ /**
1515
+ * Fork the session from one entry of its tree into a new session file,
1516
+ * leaving the original session untouched. The forked runtime replaces the
1517
+ * current one, so the outcome is reported for the session the client is
1518
+ * about to join rather than the forked-from record.
1519
+ */
1520
+ async forkFromTree(ref, request) {
1521
+ if (request.entryId.trim() === "")
1522
+ throw new Error("Session tree entry is required");
1523
+ if (this.isTreeExclusiveSessionIdentityActive(ref.id)) {
1524
+ throw new Error("Stop current session activity before forking the session tree");
1525
+ }
1526
+ await this.assertWritable(ref);
1527
+ const session = await this.getOrOpen(ref);
1528
+ if (this.hasActiveWork(session))
1529
+ throw new Error("Stop current session activity before forking the session tree");
1530
+ if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
1531
+ throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
1532
+ }
1533
+ this.publishActivity(session, "forking session from entry", "active");
1534
+ this.publishStatus(session);
1535
+ try {
1536
+ const result = await this.commandService.forkEntry(session.sessionId, request.entryId, {
1537
+ expectedLeafId: request.expectedLeafId,
1538
+ });
1539
+ if (result.type === "unsupported")
1540
+ throw new Error(result.message);
1541
+ if (result.type !== "done")
1542
+ throw new Error("Session fork is unavailable");
1543
+ if (result.session === undefined) {
1544
+ if (this.isCurrentActiveSession(session)) {
1545
+ this.publishActivity(session, "fork cancelled", "idle");
1546
+ this.publishStatus(session);
1547
+ }
1548
+ return { cancelled: true };
1549
+ }
1550
+ const forkedSession = this.active.get(result.session.id)?.runtime.session;
1551
+ if (forkedSession !== undefined && this.isCurrentActiveSession(forkedSession)) {
1552
+ this.publishActivity(forkedSession, "session forked", "idle");
1553
+ this.publishStatus(forkedSession);
1554
+ }
1555
+ if (result.session.id !== session.sessionId)
1556
+ this.clearSupersededSessionActivity(session);
1557
+ return {
1558
+ cancelled: false,
1559
+ session: result.session,
1560
+ ...(result.promptDraft === undefined ? {} : { promptDraft: result.promptDraft }),
1561
+ };
1562
+ }
1563
+ catch (error) {
1564
+ const message = error instanceof Error ? error.message : String(error);
1565
+ if (this.isCurrentActiveSession(session)) {
1566
+ this.publishActivity(session, "fork failed", "error", message);
1567
+ this.events.publish(session.sessionId, { type: "session.error", message });
1568
+ this.publishStatus(session);
1569
+ }
1570
+ throw error;
1571
+ }
1572
+ }
1573
+ /**
1574
+ * A changed-id fork can rebind its runtime after a heartbeat published the
1575
+ * prior identity as active. Clear every observable owner before forgetting
1576
+ * that identity's local activity record.
1577
+ */
1578
+ clearSupersededSessionActivity(session) {
1579
+ const sessionId = session.sessionId;
1580
+ if (this.activities.get(sessionId)?.phase === "active") {
1581
+ const at = new Date().toISOString();
1582
+ const stored = { phase: "idle", label: "idle", at };
1583
+ this.activities.set(sessionId, stored);
1584
+ const activity = { sessionId, ...stored };
1585
+ this.events.publish(sessionId, { type: "activity.update", activity });
1586
+ this.events.publishGlobal({ type: "activity.update", activity });
1587
+ }
1588
+ this.workspaceActivity?.removeSession(sessionId, session.sessionManager.getCwd());
1589
+ this.activities.delete(sessionId);
1590
+ }
1485
1591
  async reloadSessionRuntime(session) {
1486
1592
  if (this.hasActiveWork(session))
1487
1593
  throw new Error("Stop current session activity before reloading");
@@ -1531,7 +1637,7 @@ export class PiSessionService {
1531
1637
  const uniqueRefs = uniqueBulkSessionRefs(refs);
1532
1638
  const [archivedRecords, sessionContext] = await Promise.all([
1533
1639
  this.archiveStore.list(),
1534
- this.bulkSessionLookupContext(uniqueRefs),
1640
+ this.bulkSessionRefContext(uniqueRefs),
1535
1641
  ]);
1536
1642
  const failures = [];
1537
1643
  const alreadyArchivedSessionIds = [];
@@ -1545,7 +1651,7 @@ export class PiSessionService {
1545
1651
  unreadArchivedIdentities.push(archived);
1546
1652
  continue;
1547
1653
  }
1548
- const active = this.activeForLookup(bulkRefToLookup(ref));
1654
+ const active = this.activeForRef(bulkRefToSessionRef(ref));
1549
1655
  const listed = findListedSessionForBulkRef(sessionContext, ref);
1550
1656
  const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
1551
1657
  if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
@@ -1569,7 +1675,7 @@ export class PiSessionService {
1569
1675
  }
1570
1676
  const readyPlanItems = [];
1571
1677
  for (const item of planItems) {
1572
- const active = this.activeForLookup({ id: item.input.sessionId, cwd: item.input.cwd });
1678
+ const active = this.activeForRef({ id: item.input.sessionId, cwd: item.input.cwd });
1573
1679
  if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
1574
1680
  failures.push({ sessionId: item.input.sessionId, error: "Stop current session activity before archiving" });
1575
1681
  continue;
@@ -1646,18 +1752,6 @@ export class PiSessionService {
1646
1752
  await this.archiveStore.restore(archived.sessionId);
1647
1753
  await this.forgetUnreadSessions([archived]);
1648
1754
  }
1649
- async deleteArchived(ref) {
1650
- const record = await this.getArchived(ref);
1651
- if (record === undefined)
1652
- throw new Error("Archived session not found");
1653
- if (this.archiveStore.deleteArchived === undefined)
1654
- throw new Error("Archive store does not support deletion");
1655
- await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
1656
- if (record.archivePath === undefined)
1657
- await this.ensureArchivedRecordMoved(record);
1658
- await this.archiveStore.deleteArchived(record.sessionId);
1659
- await this.forgetUnreadSessions([record]);
1660
- }
1661
1755
  async deleteArchivedMany(refs) {
1662
1756
  if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
1663
1757
  throw new Error("Archive store does not support deletion");
@@ -1671,7 +1765,7 @@ export class PiSessionService {
1671
1765
  failures.push({ sessionId: ref.id, error: "Archived session not found" });
1672
1766
  continue;
1673
1767
  }
1674
- const active = this.activeForLookup({ id: record.sessionId, cwd: record.cwd });
1768
+ const active = this.activeForRef({ id: record.sessionId, cwd: record.cwd });
1675
1769
  if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
1676
1770
  failures.push({ sessionId: record.sessionId, error: "Stop current session activity before deleting archived session" });
1677
1771
  continue;
@@ -1688,12 +1782,7 @@ export class PiSessionService {
1688
1782
  failures.push({ sessionId: item.record.sessionId, error: errorMessage(error) });
1689
1783
  }
1690
1784
  }
1691
- const moveFailures = await this.moveLegacyArchivedRecordsForDelete(readyRecords);
1692
- failures.push(...moveFailures);
1693
- const moveFailureIds = new Set(moveFailures.map((failure) => failure.sessionId));
1694
- const deleteIds = readyRecords
1695
- .map((record) => record.sessionId)
1696
- .filter((sessionId) => !moveFailureIds.has(sessionId));
1785
+ const deleteIds = readyRecords.map((record) => record.sessionId);
1697
1786
  let deletedSessionIds = [];
1698
1787
  try {
1699
1788
  deletedSessionIds = await this.archiveStoreDeleteArchivedMany(deleteIds);
@@ -1746,6 +1835,11 @@ export class PiSessionService {
1746
1835
  if (sessionFile === undefined || sessionFile === "")
1747
1836
  throw new Error("Session is not persisted");
1748
1837
  await clearParentSession(sessionFile);
1838
+ // The header rewrite keeps the inode, and whenever it leaves the file's
1839
+ // size unchanged it is invisible to the gateway's summary memo, which
1840
+ // cannot detect such rewrites from identity + size alone and would keep
1841
+ // listing the old parent link until restart.
1842
+ this.sessionManager.invalidateSessionFile(sessionFile);
1749
1843
  clearParentSessionHeader(session.sessionManager);
1750
1844
  this.unregisterSubsession(session.sessionId);
1751
1845
  await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }]);
@@ -1765,7 +1859,7 @@ export class PiSessionService {
1765
1859
  return this.statusFromSession(session);
1766
1860
  }
1767
1861
  async abort(ref) {
1768
- const active = this.activeForLookup(ref);
1862
+ const active = this.activeForRef(ref);
1769
1863
  if (active === undefined)
1770
1864
  return;
1771
1865
  const sessionId = active.runtime.session.sessionId;
@@ -1791,31 +1885,26 @@ export class PiSessionService {
1791
1885
  }
1792
1886
  }
1793
1887
  async stop(ref) {
1794
- const active = this.activeForLookup(ref);
1888
+ const active = this.activeForRef(ref);
1795
1889
  if (active !== undefined) {
1796
1890
  await this.closeActive(active.runtime.session.sessionId);
1797
1891
  return;
1798
1892
  }
1799
- if (isPiSessionRef(ref)) {
1800
- this.publishNotificationMutations(this.notificationStore.clearSessionIdentity(ref.id, canonicalizeStoredCwd(ref.cwd), "runtime-close"));
1893
+ // A session whose open is parked (e.g. on a session_start dialog) is not
1894
+ // active yet; close it through the same path so stopping cannot block
1895
+ // behind the dialog timeout.
1896
+ const startup = this.startupSessionForRef(ref);
1897
+ if (startup !== undefined) {
1898
+ await this.closeActive(startup.sessionId);
1801
1899
  return;
1802
1900
  }
1803
- await this.closeActive(ref);
1901
+ this.publishNotificationMutations(this.notificationStore.clearSessionIdentity(ref.id, canonicalizeStoredCwd(ref.cwd), "runtime-close"));
1804
1902
  }
1805
- async bulkSessionLookupContext(refs) {
1903
+ async bulkSessionRefContext(refs) {
1806
1904
  const cwdSet = new Set();
1807
- let needsAllSessions = false;
1808
- for (const ref of refs) {
1809
- if (ref.cwd === undefined)
1810
- needsAllSessions = true;
1811
- else
1812
- cwdSet.add(ref.cwd);
1813
- }
1814
- const [sessionsByCwd, allSessions] = await Promise.all([
1815
- this.listSessionsByCwd([...cwdSet]),
1816
- needsAllSessions ? this.sessionManager.listAll?.() ?? Promise.resolve([]) : Promise.resolve(undefined),
1817
- ]);
1818
- return allSessions === undefined ? { sessionsByCwd } : { sessionsByCwd, allSessions };
1905
+ for (const ref of refs)
1906
+ cwdSet.add(ref.cwd);
1907
+ return { sessionsByCwd: await this.listSessionsByCwd([...cwdSet]) };
1819
1908
  }
1820
1909
  async listSessionsByCwd(cwds) {
1821
1910
  const uniqueCwds = uniqueStrings(cwds);
@@ -1843,36 +1932,8 @@ export class PiSessionService {
1843
1932
  await this.archiveStore.deleteArchived(sessionId);
1844
1933
  return [...sessionIds];
1845
1934
  }
1846
- async moveLegacyArchivedRecordsForDelete(records) {
1847
- const legacyRecords = records.filter((record) => record.archivePath === undefined);
1848
- if (legacyRecords.length === 0)
1849
- return [];
1850
- let sessionsByCwd;
1851
- try {
1852
- sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
1853
- }
1854
- catch (error) {
1855
- return legacyRecords.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
1856
- }
1857
- const moveInputs = legacyRecords
1858
- .map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
1859
- .filter(isDefined)
1860
- .map(archiveInputFromListEntry);
1861
- if (moveInputs.length === 0)
1862
- return [];
1863
- try {
1864
- await this.archiveStoreArchiveMany(moveInputs);
1865
- return [];
1866
- }
1867
- catch (error) {
1868
- const failedIds = new Set(moveInputs.map((input) => input.sessionId));
1869
- return legacyRecords
1870
- .filter((record) => failedIds.has(record.sessionId))
1871
- .map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
1872
- }
1873
- }
1874
1935
  async cleanupPlan(request) {
1875
- const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll?.() ?? [], this.archiveStore.list()]);
1936
+ const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll(), this.archiveStore.list()]);
1876
1937
  return planSessionCleanup({
1877
1938
  sessions,
1878
1939
  archivedRecords,
@@ -1901,34 +1962,6 @@ export class PiSessionService {
1901
1962
  }
1902
1963
  return [...sessionIds];
1903
1964
  }
1904
- async ensureArchivedSessionMoved(record, session) {
1905
- if (session === undefined || this.active.has(record.sessionId))
1906
- return record;
1907
- try {
1908
- return await this.archiveStore.archive(archiveInputFromListEntry(session));
1909
- }
1910
- catch {
1911
- return record;
1912
- }
1913
- }
1914
- async ensureArchivedRecordMoved(record) {
1915
- const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
1916
- if (session === undefined)
1917
- return record;
1918
- const [moved] = await this.archiveStoreArchiveMany([archiveInputFromListEntry(session)]);
1919
- return moved ?? record;
1920
- }
1921
- async ensureArchivedRecordsMoved(records) {
1922
- const legacyRecords = records.filter((record) => record.archivePath === undefined);
1923
- if (legacyRecords.length === 0)
1924
- return;
1925
- const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
1926
- const moveInputs = legacyRecords
1927
- .map((record) => sessionsByCwd.get(record.cwd)?.find((candidate) => candidate.id === record.sessionId))
1928
- .filter(isDefined)
1929
- .map(archiveInputFromListEntry);
1930
- await this.archiveStoreArchiveMany(moveInputs);
1931
- }
1932
1965
  async archiveInputForSession(session) {
1933
1966
  const cwd = session.sessionManager.getCwd();
1934
1967
  const sessionFile = session.sessionFile;
@@ -2065,7 +2098,7 @@ export class PiSessionService {
2065
2098
  return (await this.getActive(ref)).runtime.session;
2066
2099
  }
2067
2100
  async getActive(ref, options = {}) {
2068
- const active = this.activeForLookup(ref);
2101
+ const active = this.activeForRef(ref);
2069
2102
  if (active !== undefined)
2070
2103
  return active;
2071
2104
  const archived = await this.getArchived(ref);
@@ -2073,15 +2106,17 @@ export class PiSessionService {
2073
2106
  const { archivePath } = archived;
2074
2107
  return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath), { notifications: "disabled" });
2075
2108
  }
2076
- const match = isPiSessionRef(ref)
2077
- ? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
2078
- : (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
2109
+ // Resolve the session file directly by id: opening one known session must
2110
+ // not depend on — or wait behind — a full transcript listing of its whole
2111
+ // workspace. `getActive` routes prompt/shell/runCommand, so coupling it to
2112
+ // the listing would let an in-flight listing serialize unrelated sends.
2113
+ const match = await this.sessionManager.resolveSessionFile(ref.cwd, ref.id);
2079
2114
  if (!match)
2080
2115
  throw new Error("Session not found");
2081
2116
  return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
2082
2117
  }
2083
2118
  openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
2084
- const active = this.activeForLookup({ id: sessionId, cwd });
2119
+ const active = this.activeForRef({ id: sessionId, cwd });
2085
2120
  if (active !== undefined)
2086
2121
  return Promise.resolve(active);
2087
2122
  const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
@@ -2105,34 +2140,45 @@ export class PiSessionService {
2105
2140
  .map((pending) => pending.promise);
2106
2141
  }
2107
2142
  async getArchived(ref) {
2108
- const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
2143
+ const archived = await this.archiveStore.get(ref.id);
2109
2144
  if (archived === undefined)
2110
2145
  return undefined;
2111
- if (isPiSessionRef(ref) && archived.cwd !== ref.cwd)
2146
+ if (archived.cwd !== ref.cwd)
2112
2147
  return undefined;
2113
2148
  return archived;
2114
2149
  }
2115
2150
  isCurrentActiveSession(session) {
2116
2151
  return this.active.get(session.sessionId)?.runtime.session === session;
2117
2152
  }
2118
- activeForLookup(ref) {
2119
- const sessionId = sessionIdFromLookup(ref);
2153
+ /**
2154
+ * The command service tracks sessions by id alone; its callbacks only ever
2155
+ * run against a session the caller just resolved as active, so the cwd
2156
+ * needed for a full ref comes from that active runtime.
2157
+ */
2158
+ activeSessionRef(sessionId) {
2159
+ const active = this.active.get(sessionId);
2160
+ if (active === undefined)
2161
+ throw new Error("Session not found");
2162
+ return { id: sessionId, cwd: active.runtime.cwd };
2163
+ }
2164
+ activeForRef(ref) {
2165
+ const sessionId = ref.id;
2120
2166
  const exact = this.active.get(sessionId);
2121
- if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
2167
+ if (exact !== undefined && refMatchesActiveSession(ref, exact))
2122
2168
  return exact;
2123
2169
  for (const [candidateId, active] of this.active.entries()) {
2124
- if (candidateId.startsWith(sessionId) && lookupMatchesActiveSession(ref, active))
2170
+ if (candidateId.startsWith(sessionId) && refMatchesActiveSession(ref, active))
2125
2171
  return active;
2126
2172
  }
2127
2173
  return undefined;
2128
2174
  }
2129
- startupSessionForLookup(ref) {
2130
- const sessionId = sessionIdFromLookup(ref);
2175
+ startupSessionForRef(ref) {
2176
+ const sessionId = ref.id;
2131
2177
  const exact = this.startupSessions.get(sessionId);
2132
- if (exact !== undefined && lookupMatchesStartupSession(ref, exact))
2178
+ if (exact !== undefined && refMatchesStartupSession(ref, exact))
2133
2179
  return exact;
2134
2180
  for (const [candidateId, session] of this.startupSessions.entries()) {
2135
- if (candidateId.startsWith(sessionId) && lookupMatchesStartupSession(ref, session))
2181
+ if (candidateId.startsWith(sessionId) && refMatchesStartupSession(ref, session))
2136
2182
  return session;
2137
2183
  }
2138
2184
  return undefined;
@@ -2144,7 +2190,7 @@ export class PiSessionService {
2144
2190
  * its status projection).
2145
2191
  */
2146
2192
  async sessionForStatusOrDialogClose(ref) {
2147
- const reachable = this.activeForLookup(ref)?.runtime.session ?? this.startupSessionForLookup(ref);
2193
+ const reachable = this.activeForRef(ref)?.runtime.session ?? this.startupSessionForRef(ref);
2148
2194
  if (reachable !== undefined)
2149
2195
  return reachable;
2150
2196
  return this.getOrOpen(ref);
@@ -2175,6 +2221,7 @@ export class PiSessionService {
2175
2221
  sessionManager,
2176
2222
  delegationToolsEnabled,
2177
2223
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
2224
+ ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
2178
2225
  });
2179
2226
  const active = { runtime, unsubscribe: noop };
2180
2227
  let boundSession = runtime.session;
@@ -2322,14 +2369,6 @@ export class PiSessionService {
2322
2369
  }
2323
2370
  const added = this.notificationStore.addNotification(generation, message, type);
2324
2371
  this.publishNotificationMutations(added.mutations);
2325
- if (added.notification === undefined)
2326
- return;
2327
- this.events.publish(session.sessionId, {
2328
- type: "command.output",
2329
- level: type === "error" ? "error" : "info",
2330
- message,
2331
- notificationId: added.notification.id,
2332
- });
2333
2372
  };
2334
2373
  // PI WEB owns the browser-facing dialog, notification, and text-formatting
2335
2374
  // boundaries: the three dialog primitives park daemon-held Promises that
@@ -2387,6 +2426,11 @@ export class PiSessionService {
2387
2426
  void this.publishUnreadMutations(mutations).catch(() => undefined);
2388
2427
  }
2389
2428
  publishUnreadMutations(mutations) {
2429
+ // The store applied the mutations already, so the status projection is told
2430
+ // now rather than after the durable flush: it reads in-memory unread state
2431
+ // and must not lag behind the rows the browser is about to see.
2432
+ if (mutations.length > 0)
2433
+ this.onUnreadChanged?.();
2390
2434
  this.enqueueUnreadMutations(mutations);
2391
2435
  this.unreadPublicationFlushRequested = true;
2392
2436
  if (this.unreadPublication === undefined && this.unreadPublicationRetryTimer !== undefined) {
@@ -2476,7 +2520,7 @@ export class PiSessionService {
2476
2520
  }
2477
2521
  }
2478
2522
  active.unsubscribe = session.subscribe((event) => {
2479
- this.events.publish(session.sessionId, toClientEvent(event));
2523
+ this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel));
2480
2524
  this.publishActivityForEvent(session, event);
2481
2525
  const eventType = getString(event, "type");
2482
2526
  if (eventType === "agent_end")
@@ -2957,7 +3001,7 @@ function uniqueBulkSessionRefs(refs) {
2957
3001
  const seen = new Set();
2958
3002
  const unique = [];
2959
3003
  for (const ref of refs) {
2960
- const key = `${ref.cwd ?? ""}\0${ref.id}`;
3004
+ const key = `${ref.cwd}\0${ref.id}`;
2961
3005
  if (seen.has(key))
2962
3006
  continue;
2963
3007
  seen.add(key);
@@ -2965,16 +3009,14 @@ function uniqueBulkSessionRefs(refs) {
2965
3009
  }
2966
3010
  return unique;
2967
3011
  }
2968
- function bulkRefToLookup(ref) {
2969
- return ref.cwd === undefined ? ref.id : { id: ref.id, cwd: ref.cwd };
3012
+ function bulkRefToSessionRef(ref) {
3013
+ return { id: ref.id, cwd: ref.cwd };
2970
3014
  }
2971
3015
  function findArchivedRecordForBulkRef(records, ref) {
2972
- return records.find((record) => (ref.cwd === undefined || record.cwd === ref.cwd) && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
3016
+ return records.find((record) => record.cwd === ref.cwd && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
2973
3017
  }
2974
3018
  function findListedSessionForBulkRef(context, ref) {
2975
- if (ref.cwd !== undefined)
2976
- return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
2977
- return context.allSessions === undefined ? undefined : findSessionByIdOrPrefix(context.allSessions, ref.id);
3019
+ return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
2978
3020
  }
2979
3021
  function findSessionByIdOrPrefix(sessions, sessionId) {
2980
3022
  return sessions.find((session) => session.id === sessionId) ?? sessions.find((session) => session.id.startsWith(sessionId));
@@ -3357,13 +3399,34 @@ function buildPromptOptions(behavior, images) {
3357
3399
  function stringValue(value) {
3358
3400
  return typeof value === "string" ? value : "";
3359
3401
  }
3402
+ /**
3403
+ * Attach the thinking level in effect when an assistant message was generated,
3404
+ * so chat bubbles can show it next to the model. Non-assistant messages pass
3405
+ * through by reference; assistant messages are copied only when a level is set.
3406
+ * "off" is the absence of thinking, not a level worth labeling on every bubble.
3407
+ */
3408
+ function annotateAssistantThinkingLevel(message, thinkingLevel) {
3409
+ if (thinkingLevel === undefined || thinkingLevel === "" || thinkingLevel === "off")
3410
+ return message;
3411
+ if (!isRecord(message) || message["role"] !== "assistant")
3412
+ return message;
3413
+ return { ...message, thinkingLevel };
3414
+ }
3360
3415
  function historyMessages(session) {
3361
3416
  const messages = [];
3417
+ // Pi records the initial level at session creation and every later change, so
3418
+ // walking the branch yields the level in effect for each assistant message.
3419
+ let thinkingLevel;
3362
3420
  for (const entry of session.sessionManager.getBranch()) {
3363
3421
  if (!isRecord(entry))
3364
3422
  continue;
3365
3423
  if (entry["type"] === "message")
3366
- messages.push(entry["message"]);
3424
+ messages.push(annotateAssistantThinkingLevel(entry["message"], thinkingLevel));
3425
+ else if (entry["type"] === "thinking_level_change") {
3426
+ const level = getString(entry, "thinkingLevel");
3427
+ if (level !== undefined)
3428
+ thinkingLevel = level;
3429
+ }
3367
3430
  else if (entry["type"] === "custom_message" && entry["display"] === true)
3368
3431
  messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
3369
3432
  else if (entry["type"] === "compaction")
@@ -3408,7 +3471,7 @@ function finalAssistantText(messages) {
3408
3471
  }
3409
3472
  return "";
3410
3473
  }
3411
- function toClientEvent(event) {
3474
+ function toClientEvent(event, thinkingLevel) {
3412
3475
  const eventType = getString(event, "type");
3413
3476
  const assistantMessageEvent = getProperty(event, "assistantMessageEvent");
3414
3477
  if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "text_delta") {
@@ -3435,7 +3498,9 @@ function toClientEvent(event) {
3435
3498
  return { type: "agent.end" };
3436
3499
  if (eventType === "message_end") {
3437
3500
  const message = getProperty(event, "message");
3438
- return message === undefined ? { type: "message.end" } : { type: "message.end", message };
3501
+ if (message === undefined)
3502
+ return { type: "message.end" };
3503
+ return { type: "message.end", message: annotateAssistantThinkingLevel(message, thinkingLevel) };
3439
3504
  }
3440
3505
  return { type: "pi.event", eventType: eventType ?? "unknown" };
3441
3506
  }