cli-jaw 2.2.16 → 2.2.17

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 (411) hide show
  1. package/README.md +10 -1
  2. package/dist/bin/commands/_shared/dashboard-client.js +42 -0
  3. package/dist/bin/commands/_shared/dashboard-client.js.map +1 -0
  4. package/dist/bin/commands/_shared/search-format.js +40 -0
  5. package/dist/bin/commands/_shared/search-format.js.map +1 -0
  6. package/dist/bin/commands/chat-search.js +5 -3
  7. package/dist/bin/commands/chat-search.js.map +1 -1
  8. package/dist/bin/commands/dashboard-chat.js +14 -31
  9. package/dist/bin/commands/dashboard-chat.js.map +1 -1
  10. package/dist/bin/commands/dashboard-memory.js +12 -49
  11. package/dist/bin/commands/dashboard-memory.js.map +1 -1
  12. package/dist/bin/commands/doctor.js +33 -10
  13. package/dist/bin/commands/doctor.js.map +1 -1
  14. package/dist/bin/commands/init.js +13 -1
  15. package/dist/bin/commands/init.js.map +1 -1
  16. package/dist/bin/commands/memory.js +5 -1
  17. package/dist/bin/commands/memory.js.map +1 -1
  18. package/dist/bin/commands/slack.js +57 -3
  19. package/dist/bin/commands/slack.js.map +1 -1
  20. package/dist/bin/commands/tui/input-handler.js +1 -1
  21. package/dist/bin/commands/tui/input-handler.js.map +1 -1
  22. package/dist/bin/commands/tui/slash-command-runner.js +4 -5
  23. package/dist/bin/commands/tui/slash-command-runner.js.map +1 -1
  24. package/dist/bin/postinstall.js +16 -18
  25. package/dist/bin/postinstall.js.map +1 -1
  26. package/dist/lib/media-kind.js +28 -0
  27. package/dist/lib/media-kind.js.map +1 -0
  28. package/dist/lib/upload.js +27 -10
  29. package/dist/lib/upload.js.map +1 -1
  30. package/dist/server.js +35 -51
  31. package/dist/server.js.map +1 -1
  32. package/dist/src/agent/args.js +34 -0
  33. package/dist/src/agent/args.js.map +1 -1
  34. package/dist/src/agent/codex-app-client.js +1016 -135
  35. package/dist/src/agent/codex-app-client.js.map +1 -1
  36. package/dist/src/agent/codex-app-events.js +72 -1
  37. package/dist/src/agent/codex-app-events.js.map +1 -1
  38. package/dist/src/agent/codex-host-pool.js +617 -0
  39. package/dist/src/agent/codex-host-pool.js.map +1 -0
  40. package/dist/src/agent/jwc-runtime.js +12 -4
  41. package/dist/src/agent/jwc-runtime.js.map +1 -1
  42. package/dist/src/agent/kiro-auth.js +10 -4
  43. package/dist/src/agent/kiro-auth.js.map +1 -1
  44. package/dist/src/agent/kiro-runtime.js +45 -8
  45. package/dist/src/agent/kiro-runtime.js.map +1 -1
  46. package/dist/src/agent/lifecycle-handler.js +75 -52
  47. package/dist/src/agent/lifecycle-handler.js.map +1 -1
  48. package/dist/src/agent/memory-flush-controller.js +241 -33
  49. package/dist/src/agent/memory-flush-controller.js.map +1 -1
  50. package/dist/src/agent/prompt-context.js +4 -0
  51. package/dist/src/agent/prompt-context.js.map +1 -1
  52. package/dist/src/agent/runtime-pool.js +64 -34
  53. package/dist/src/agent/runtime-pool.js.map +1 -1
  54. package/dist/src/agent/session-persistence.js +71 -15
  55. package/dist/src/agent/session-persistence.js.map +1 -1
  56. package/dist/src/agent/spawn/queue.js +319 -162
  57. package/dist/src/agent/spawn/queue.js.map +1 -1
  58. package/dist/src/agent/spawn.js +581 -268
  59. package/dist/src/agent/spawn.js.map +1 -1
  60. package/dist/src/bgtask/types.js +1 -1
  61. package/dist/src/bgtask/types.js.map +1 -1
  62. package/dist/src/browser/adaptive-fetch/safety.js +25 -0
  63. package/dist/src/browser/adaptive-fetch/safety.js.map +1 -1
  64. package/dist/src/browser/connection.js +2 -8
  65. package/dist/src/browser/connection.js.map +1 -1
  66. package/dist/src/cli/capability-probe-worker.js +172 -0
  67. package/dist/src/cli/capability-probe-worker.js.map +1 -0
  68. package/dist/src/cli/capability-probe.js +83 -0
  69. package/dist/src/cli/capability-probe.js.map +1 -0
  70. package/dist/src/cli/claude-models.js +2 -2
  71. package/dist/src/cli/claude-models.js.map +1 -1
  72. package/dist/src/cli/cli-status-worker.js +296 -0
  73. package/dist/src/cli/cli-status-worker.js.map +1 -0
  74. package/dist/src/cli/cli-status.js +81 -0
  75. package/dist/src/cli/cli-status.js.map +1 -0
  76. package/dist/src/cli/command-context.js +17 -5
  77. package/dist/src/cli/command-context.js.map +1 -1
  78. package/dist/src/cli/compact.js +56 -19
  79. package/dist/src/cli/compact.js.map +1 -1
  80. package/dist/src/cli/handlers-runtime.js +6 -4
  81. package/dist/src/cli/handlers-runtime.js.map +1 -1
  82. package/dist/src/cli/handlers.js +7 -2
  83. package/dist/src/cli/handlers.js.map +1 -1
  84. package/dist/src/cli/opencodex-models.js +5 -32
  85. package/dist/src/cli/opencodex-models.js.map +1 -1
  86. package/dist/src/cli/opencodex-runtime.js +82 -0
  87. package/dist/src/cli/opencodex-runtime.js.map +1 -0
  88. package/dist/src/cli/readiness.js +35 -18
  89. package/dist/src/cli/readiness.js.map +1 -1
  90. package/dist/src/cli/registry-live.js +8 -2
  91. package/dist/src/cli/registry-live.js.map +1 -1
  92. package/dist/src/cli/registry.js +1 -1
  93. package/dist/src/cli/registry.js.map +1 -1
  94. package/dist/src/core/browser-open-default.js +6 -4
  95. package/dist/src/core/browser-open-default.js.map +1 -1
  96. package/dist/src/core/browser-open.js +11 -15
  97. package/dist/src/core/browser-open.js.map +1 -1
  98. package/dist/src/core/bus.js +7 -1
  99. package/dist/src/core/bus.js.map +1 -1
  100. package/dist/src/core/chat-sessions.js +121 -12
  101. package/dist/src/core/chat-sessions.js.map +1 -1
  102. package/dist/src/core/cli-detect.js +148 -4
  103. package/dist/src/core/cli-detect.js.map +1 -1
  104. package/dist/src/core/codex-config.js +21 -0
  105. package/dist/src/core/codex-config.js.map +1 -1
  106. package/dist/src/core/compact.js +73 -21
  107. package/dist/src/core/compact.js.map +1 -1
  108. package/dist/src/core/config.js +449 -20
  109. package/dist/src/core/config.js.map +1 -1
  110. package/dist/src/core/db.js +225 -0
  111. package/dist/src/core/db.js.map +1 -1
  112. package/dist/src/core/event-bus.js +49 -4
  113. package/dist/src/core/event-bus.js.map +1 -1
  114. package/dist/src/core/event-scope.js +100 -0
  115. package/dist/src/core/event-scope.js.map +1 -0
  116. package/dist/src/core/main-session.js +64 -21
  117. package/dist/src/core/main-session.js.map +1 -1
  118. package/dist/src/core/message-summary.js +12 -2
  119. package/dist/src/core/message-summary.js.map +1 -1
  120. package/dist/src/core/platform-kind.js +139 -0
  121. package/dist/src/core/platform-kind.js.map +1 -0
  122. package/dist/src/core/rate-limit.js +156 -0
  123. package/dist/src/core/rate-limit.js.map +1 -0
  124. package/dist/src/core/runtime-settings.js +165 -17
  125. package/dist/src/core/runtime-settings.js.map +1 -1
  126. package/dist/src/core/session-context.js +7 -0
  127. package/dist/src/core/session-context.js.map +1 -0
  128. package/dist/src/core/session-ops.js +53 -9
  129. package/dist/src/core/session-ops.js.map +1 -1
  130. package/dist/src/core/settings-merge.js +105 -3
  131. package/dist/src/core/settings-merge.js.map +1 -1
  132. package/dist/src/core/settings-watch.js +29 -5
  133. package/dist/src/core/settings-watch.js.map +1 -1
  134. package/dist/src/core/widget-watcher.js +15 -2
  135. package/dist/src/core/widget-watcher.js.map +1 -1
  136. package/dist/src/discord/bot.js +5 -2
  137. package/dist/src/discord/bot.js.map +1 -1
  138. package/dist/src/discord/commands.js +4 -4
  139. package/dist/src/discord/commands.js.map +1 -1
  140. package/dist/src/goal/heartbeat.js +1 -1
  141. package/dist/src/goal/heartbeat.js.map +1 -1
  142. package/dist/src/goal/runtime.js +1 -1
  143. package/dist/src/goal/runtime.js.map +1 -1
  144. package/dist/src/goal-run/controller.js +2 -2
  145. package/dist/src/goal-run/controller.js.map +1 -1
  146. package/dist/src/manager/internal-fetch.js +15 -0
  147. package/dist/src/manager/internal-fetch.js.map +1 -0
  148. package/dist/src/manager/logs.js +2 -1
  149. package/dist/src/manager/logs.js.map +1 -1
  150. package/dist/src/manager/memory/chat-federation.js +104 -11
  151. package/dist/src/manager/memory/chat-federation.js.map +1 -1
  152. package/dist/src/manager/memory/federation.js +100 -0
  153. package/dist/src/manager/memory/federation.js.map +1 -1
  154. package/dist/src/manager/metadata.js +4 -1
  155. package/dist/src/manager/metadata.js.map +1 -1
  156. package/dist/src/manager/notes/assets.js +1 -1
  157. package/dist/src/manager/notes/assets.js.map +1 -1
  158. package/dist/src/manager/notes/frontmatter.js +5 -49
  159. package/dist/src/manager/notes/frontmatter.js.map +1 -1
  160. package/dist/src/manager/notes/link-resolver.js +2 -2
  161. package/dist/src/manager/notes/link-resolver.js.map +1 -1
  162. package/dist/src/manager/notes/remote-assets.js +1 -1
  163. package/dist/src/manager/notes/remote-assets.js.map +1 -1
  164. package/dist/src/manager/notes/routes.js +2 -2
  165. package/dist/src/manager/notes/routes.js.map +1 -1
  166. package/dist/src/manager/notes/store.js +2 -2
  167. package/dist/src/manager/notes/store.js.map +1 -1
  168. package/dist/src/manager/notes/trash.js +1 -1
  169. package/dist/src/manager/notes/trash.js.map +1 -1
  170. package/dist/src/manager/notes/vault-index.js +2 -2
  171. package/dist/src/manager/notes/vault-index.js.map +1 -1
  172. package/dist/src/manager/notes/wiki-links.js +1 -1
  173. package/dist/src/manager/notes/wiki-links.js.map +1 -1
  174. package/dist/src/manager/notes/wiki-routes.js +84 -0
  175. package/dist/src/manager/notes/wiki-routes.js.map +1 -0
  176. package/dist/src/manager/proxy.js +1 -0
  177. package/dist/src/manager/proxy.js.map +1 -1
  178. package/dist/src/manager/routes/dashboard-memory.js +24 -1
  179. package/dist/src/manager/routes/dashboard-memory.js.map +1 -1
  180. package/dist/src/manager/routes/runtime-monitor.js +1 -1
  181. package/dist/src/manager/routes/runtime-monitor.js.map +1 -1
  182. package/dist/src/manager/scan.js +5 -3
  183. package/dist/src/manager/scan.js.map +1 -1
  184. package/dist/src/manager/server.js +20 -6
  185. package/dist/src/manager/server.js.map +1 -1
  186. package/dist/src/manager/telegram-hub/hub-bot.js +4 -3
  187. package/dist/src/manager/telegram-hub/hub-bot.js.map +1 -1
  188. package/dist/src/manager/worker-events.js +2 -1
  189. package/dist/src/manager/worker-events.js.map +1 -1
  190. package/dist/src/memory/heartbeat.js +6 -5
  191. package/dist/src/memory/heartbeat.js.map +1 -1
  192. package/dist/src/memory/indexing.js +162 -45
  193. package/dist/src/memory/indexing.js.map +1 -1
  194. package/dist/src/messaging/channel-validate.js +14 -1
  195. package/dist/src/messaging/channel-validate.js.map +1 -1
  196. package/dist/src/messaging/redact.js +2 -1
  197. package/dist/src/messaging/redact.js.map +1 -1
  198. package/dist/src/messaging/session-key.js +21 -2
  199. package/dist/src/messaging/session-key.js.map +1 -1
  200. package/dist/src/notes/constants.js.map +1 -0
  201. package/dist/src/notes/frontmatter.js +59 -0
  202. package/dist/src/notes/frontmatter.js.map +1 -0
  203. package/dist/src/notes/path-guards.js.map +1 -0
  204. package/dist/src/notes/search.js.map +1 -0
  205. package/dist/src/orchestrator/attestation.js +1 -1
  206. package/dist/src/orchestrator/attestation.js.map +1 -1
  207. package/dist/src/orchestrator/gateway.js +135 -31
  208. package/dist/src/orchestrator/gateway.js.map +1 -1
  209. package/dist/src/orchestrator/pipeline.js +53 -21
  210. package/dist/src/orchestrator/pipeline.js.map +1 -1
  211. package/dist/src/orchestrator/scope.js +36 -2
  212. package/dist/src/orchestrator/scope.js.map +1 -1
  213. package/dist/src/orchestrator/session-lanes.js +87 -0
  214. package/dist/src/orchestrator/session-lanes.js.map +1 -0
  215. package/dist/src/orchestrator/session-work.js +25 -0
  216. package/dist/src/orchestrator/session-work.js.map +1 -0
  217. package/dist/src/orchestrator/state-machine.js +1 -5
  218. package/dist/src/orchestrator/state-machine.js.map +1 -1
  219. package/dist/src/orchestrator/worker-registry.js +20 -10
  220. package/dist/src/orchestrator/worker-registry.js.map +1 -1
  221. package/dist/src/prompt/builder.js +40 -2
  222. package/dist/src/prompt/builder.js.map +1 -1
  223. package/dist/src/prompt/templates/a1-system.md +8 -1
  224. package/dist/src/routes/agent-control.js +44 -8
  225. package/dist/src/routes/agent-control.js.map +1 -1
  226. package/dist/src/routes/chat-sessions.js +34 -6
  227. package/dist/src/routes/chat-sessions.js.map +1 -1
  228. package/dist/src/routes/command.js +63 -8
  229. package/dist/src/routes/command.js.map +1 -1
  230. package/dist/src/routes/events.js +11 -2
  231. package/dist/src/routes/events.js.map +1 -1
  232. package/dist/src/routes/link-preview.js +1 -27
  233. package/dist/src/routes/link-preview.js.map +1 -1
  234. package/dist/src/routes/messages.js +6 -5
  235. package/dist/src/routes/messages.js.map +1 -1
  236. package/dist/src/routes/messaging.js +45 -1
  237. package/dist/src/routes/messaging.js.map +1 -1
  238. package/dist/src/routes/orchestrate.js +53 -33
  239. package/dist/src/routes/orchestrate.js.map +1 -1
  240. package/dist/src/routes/search.js +84 -0
  241. package/dist/src/routes/search.js.map +1 -0
  242. package/dist/src/routes/session-request.js +29 -0
  243. package/dist/src/routes/session-request.js.map +1 -0
  244. package/dist/src/routes/settings.js +110 -3
  245. package/dist/src/routes/settings.js.map +1 -1
  246. package/dist/src/routes/static.js +30 -8
  247. package/dist/src/routes/static.js.map +1 -1
  248. package/dist/src/routes/system.js +1 -1
  249. package/dist/src/routes/system.js.map +1 -1
  250. package/dist/src/routes/wiki.js +122 -0
  251. package/dist/src/routes/wiki.js.map +1 -0
  252. package/dist/src/search/contract.js +2 -0
  253. package/dist/src/search/contract.js.map +1 -0
  254. package/dist/src/search/coordinator.js +153 -0
  255. package/dist/src/search/coordinator.js.map +1 -0
  256. package/dist/src/search/provider.js +40 -0
  257. package/dist/src/search/provider.js.map +1 -0
  258. package/dist/src/search/providers/chat.js +115 -0
  259. package/dist/src/search/providers/chat.js.map +1 -0
  260. package/dist/src/search/providers/memory.js +53 -0
  261. package/dist/src/search/providers/memory.js.map +1 -0
  262. package/dist/src/search/providers/wiki.js +80 -0
  263. package/dist/src/search/providers/wiki.js.map +1 -0
  264. package/dist/src/slack/api.js +10 -1
  265. package/dist/src/slack/api.js.map +1 -1
  266. package/dist/src/slack/attachment-recovery.js +42 -0
  267. package/dist/src/slack/attachment-recovery.js.map +1 -0
  268. package/dist/src/slack/bot.js +120 -41
  269. package/dist/src/slack/bot.js.map +1 -1
  270. package/dist/src/slack/commands.js +24 -8
  271. package/dist/src/slack/commands.js.map +1 -1
  272. package/dist/src/slack/events.js +12 -1
  273. package/dist/src/slack/events.js.map +1 -1
  274. package/dist/src/slack/history.js +108 -0
  275. package/dist/src/slack/history.js.map +1 -0
  276. package/dist/src/slack/inbound-file.js +242 -0
  277. package/dist/src/slack/inbound-file.js.map +1 -0
  278. package/dist/src/slack/inbound-url.js +39 -0
  279. package/dist/src/slack/inbound-url.js.map +1 -0
  280. package/dist/src/slack/ingress.js +140 -0
  281. package/dist/src/slack/ingress.js.map +1 -0
  282. package/dist/src/slack/manifest.js +3 -0
  283. package/dist/src/slack/manifest.js.map +1 -1
  284. package/dist/src/slack/socket.js +1 -1
  285. package/dist/src/slack/socket.js.map +1 -1
  286. package/dist/src/slack/thread-tracker.js +86 -0
  287. package/dist/src/slack/thread-tracker.js.map +1 -0
  288. package/dist/src/telegram/bot.js +21 -11
  289. package/dist/src/telegram/bot.js.map +1 -1
  290. package/dist/src/wiki/config.js +193 -0
  291. package/dist/src/wiki/config.js.map +1 -0
  292. package/dist/src/wiki/entities.js +61 -0
  293. package/dist/src/wiki/entities.js.map +1 -0
  294. package/dist/src/wiki/ontology.js +72 -0
  295. package/dist/src/wiki/ontology.js.map +1 -0
  296. package/dist/src/wiki/prompt.js +172 -0
  297. package/dist/src/wiki/prompt.js.map +1 -0
  298. package/dist/src/wiki/safe-traversal.js +198 -0
  299. package/dist/src/wiki/safe-traversal.js.map +1 -0
  300. package/dist/src/wiki/scaffold.js +77 -0
  301. package/dist/src/wiki/scaffold.js.map +1 -0
  302. package/package.json +1 -1
  303. package/public/css/session-hub.css +266 -0
  304. package/public/dist/assets/AdvancedExport-DYGcCAUq.js +1 -0
  305. package/public/dist/assets/Agent-CNNQ7KkJ.js +1 -0
  306. package/public/dist/assets/{DesignPanel-BGMPhZSx.js → DesignPanel-DcL369nB.js} +1 -1
  307. package/public/dist/assets/{DocPanel-Dz-J_l5M.js → DocPanel-K8A2pm_1.js} +1 -1
  308. package/public/dist/assets/{FolderPanel-DbPhzzvi.js → FolderPanel-CV52EzwG.js} +1 -1
  309. package/public/dist/assets/{MilkdownWysiwygEditor-Em7iC6y9.js → MilkdownWysiwygEditor-BKOCDI4Z.js} +1 -1
  310. package/public/dist/assets/api-B25zNNFJ.js +1 -0
  311. package/public/dist/assets/{app-DX4e6isX.css → app-DjgGeXhJ.css} +1 -1
  312. package/public/dist/assets/app-Dsgp4xpU.js +55 -0
  313. package/public/dist/assets/bgtask-badge-C6h5C-o4.js +1 -0
  314. package/public/dist/assets/{bgtask-badge-qMkKOZx0.js → bgtask-badge-CpavV0Jc.js} +4 -4
  315. package/public/dist/assets/{employees-BnobWM_2.js → employees-BJyEgim7.js} +4 -4
  316. package/public/dist/assets/idb-cache-B5PX63p7.js +1 -0
  317. package/public/dist/assets/{idb-cache-BzzftUET.js → idb-cache-lOntwU3S.js} +1 -1
  318. package/public/dist/assets/{iframe-renderer-C5vRbbl9.js → iframe-renderer-B8k3AvTv.js} +12 -12
  319. package/public/dist/assets/iframe-renderer-BO_GgCTR.js +1 -0
  320. package/public/dist/assets/manager-LkGs6qEJ.css +1 -0
  321. package/public/dist/assets/manager-kXYQyMhR.js +13 -0
  322. package/public/dist/assets/{memory-DF4kPwwC.js → memory-D2RbBVO-.js} +11 -11
  323. package/public/dist/assets/memory-amsvg4Ey.js +1 -0
  324. package/public/dist/assets/message-history-BGHZxvgV.js +1 -0
  325. package/public/dist/assets/message-history-Bw2E_b8L.js +1 -0
  326. package/public/dist/assets/pi-settings-5HVg1sXJ.js +1 -0
  327. package/public/dist/assets/render-BkGcpH7z.js +157 -0
  328. package/public/dist/assets/settings-B1FPORO2.js +56 -0
  329. package/public/dist/assets/settings-CMN-zN4Q.js +1 -0
  330. package/public/dist/assets/settings-core-B_OjGYVN.js +1 -0
  331. package/public/dist/assets/settings-core-CxRwqx86.js +144 -0
  332. package/public/dist/assets/sidebar-DW78DANx.js +49 -0
  333. package/public/dist/assets/skills-Dz-9Yrxr.js +1 -0
  334. package/public/dist/assets/{skills-IGquen_j.js → skills-lWi0ZFiV.js} +6 -6
  335. package/public/dist/assets/{slash-commands-ZRDOL99E.js → slash-commands-DYOI6nKj.js} +1 -1
  336. package/public/dist/assets/slash-commands-xJFCdTTM.js +1 -0
  337. package/public/dist/assets/{trace-drawer-D8To8fYt.js → trace-drawer-DBX9xzmt.js} +1 -1
  338. package/public/dist/assets/{ui-Hx_mNoDf.js → ui-Bx7eqnXt.js} +1 -1
  339. package/public/dist/assets/ui-JOcrYeG-.js +1 -0
  340. package/public/dist/assets/{wiki-link-suggestions-DZQc78gj.js → wiki-link-suggestions-Dp6kBQVu.js} +1 -1
  341. package/public/dist/index.html +4 -4
  342. package/public/dist/manager/index.html +2 -2
  343. package/public/index.html +3 -2
  344. package/public/js/event-channel.ts +25 -0
  345. package/public/js/features/channel-onboarding-flow.ts +16 -1
  346. package/public/js/features/channel-onboarding.ts +20 -2
  347. package/public/js/features/chat-messages.ts +3 -1
  348. package/public/js/features/chat-search.ts +2 -1
  349. package/public/js/features/chat.ts +56 -18
  350. package/public/js/features/elicitation.ts +15 -0
  351. package/public/js/features/idb-cache.ts +34 -0
  352. package/public/js/features/message-history.ts +2 -1
  353. package/public/js/features/session-hub.ts +334 -0
  354. package/public/js/features/settings-cli-status-render.ts +1 -1
  355. package/public/js/features/settings-cli-status.ts +25 -11
  356. package/public/js/features/settings-core.ts +72 -3
  357. package/public/js/features/settings-types.ts +32 -0
  358. package/public/js/features/ui-status.ts +2 -1
  359. package/public/js/main.ts +20 -0
  360. package/public/js/render/compose-block.ts +7 -0
  361. package/public/js/ws.ts +22 -2
  362. package/public/locales/en.json +46 -4
  363. package/public/locales/ja.json +46 -4
  364. package/public/locales/ko.json +46 -4
  365. package/public/locales/zh.json +46 -4
  366. package/public/manager/src/App.tsx +1 -1
  367. package/public/manager/src/InstancePreview.tsx +5 -1
  368. package/public/manager/src/api.ts +2 -1
  369. package/public/manager/src/components/InstanceGroups.tsx +10 -0
  370. package/public/manager/src/components/InstanceListContent.tsx +24 -1
  371. package/public/manager/src/components/InstanceRow.tsx +33 -3
  372. package/public/manager/src/components/InstanceSessionList.tsx +132 -0
  373. package/public/manager/src/hooks/useActiveSessionDisclosure.ts +45 -0
  374. package/public/manager/src/lib/resilient-fetch.ts +93 -0
  375. package/public/manager/src/lib/session-store.ts +216 -0
  376. package/public/manager/src/manager-components.css +97 -0
  377. package/public/manager/src/settings/pages/AdvancedExport.tsx +12 -2
  378. package/public/manager/src/settings/pages/Agent.tsx +113 -1
  379. package/public/manager/src/settings/pages/components/agent/agent-meta.ts +1 -1
  380. package/scripts/burst-scenario.mjs +72 -0
  381. package/scripts/check-redaction-sinks.mjs +1 -0
  382. package/scripts/check-strict-baseline.mjs +1 -1
  383. package/dist/src/manager/notes/constants.js.map +0 -1
  384. package/dist/src/manager/notes/path-guards.js.map +0 -1
  385. package/dist/src/manager/notes/search.js.map +0 -1
  386. package/public/dist/assets/AdvancedExport-DsDpqXoH.js +0 -1
  387. package/public/dist/assets/Agent-BjbZVZ9p.js +0 -1
  388. package/public/dist/assets/api-C9dOxby7.js +0 -1
  389. package/public/dist/assets/app-BzD_ZLvt.js +0 -55
  390. package/public/dist/assets/bgtask-badge-Dqbp-Y4e.js +0 -1
  391. package/public/dist/assets/idb-cache-ZH6lOebk.js +0 -1
  392. package/public/dist/assets/iframe-renderer-Df4KIVyx.js +0 -1
  393. package/public/dist/assets/manager-C4eNDNFt.css +0 -1
  394. package/public/dist/assets/manager-DaN201mN.js +0 -13
  395. package/public/dist/assets/memory-D-bULTHl.js +0 -1
  396. package/public/dist/assets/message-history-C4WOisGx.js +0 -1
  397. package/public/dist/assets/message-history-ZBoza6ew.js +0 -1
  398. package/public/dist/assets/pi-settings-CBUGDAuT.js +0 -1
  399. package/public/dist/assets/render-DYX6GRQ0.js +0 -122
  400. package/public/dist/assets/settings-DbJpGoaX.js +0 -55
  401. package/public/dist/assets/settings-SreihUje.js +0 -1
  402. package/public/dist/assets/settings-core-B1Y5Nb4Z.js +0 -1
  403. package/public/dist/assets/settings-core-DobZWIZ7.js +0 -140
  404. package/public/dist/assets/sidebar-Cp22g0Gv.js +0 -49
  405. package/public/dist/assets/skills-DwPIP08e.js +0 -1
  406. package/public/dist/assets/slash-commands-BBVfwcnW.js +0 -1
  407. package/public/dist/assets/ui-CAicpUZc.js +0 -1
  408. /package/dist/src/{manager/notes → notes}/constants.js +0 -0
  409. /package/dist/src/{manager/notes → notes}/path-guards.js +0 -0
  410. /package/dist/src/{manager/notes → notes}/search.js +0 -0
  411. /package/public/dist/assets/{pi-settings-Dh5lgb87.js → pi-settings-DEuKgvIj.js} +0 -0
@@ -1,5 +1,8 @@
1
+ import type { ReactNode } from 'react';
1
2
  import { EmptyNavigator } from './EmptyNavigator';
2
3
  import { InstanceGroups } from './InstanceGroups';
4
+ import { InstanceSessionList } from './InstanceSessionList';
5
+ import { useActiveSessionDisclosure } from '../hooks/useActiveSessionDisclosure';
3
6
  import type {
4
7
  DashboardInstance,
5
8
  DashboardLifecycleAction,
@@ -24,6 +27,9 @@ type InstanceListContentProps = {
24
27
  showInlineLabelEditor: boolean;
25
28
  showSidebarRuntimeLine: boolean;
26
29
  showSelectedRowActions: boolean;
30
+ /** Re-clicking the already selected (Active) row routes here instead of
31
+ * onSelect — palette/keyboard selection keeps its meaning (260806 D3). */
32
+ onRouteToSettings?: (port: number) => void;
27
33
  profiles: DashboardProfile[];
28
34
  getLabel: (instance: DashboardInstance) => string;
29
35
  formatUptime: (seconds: number | null) => string;
@@ -35,11 +41,24 @@ type InstanceListContentProps = {
35
41
  };
36
42
 
37
43
  export function InstanceListContent(props: InstanceListContentProps) {
44
+ // Session disclosure state lives with the list it decorates (260806 D2);
45
+ // App.tsx stays inside its 500-line dashboard budget.
46
+ const { activeSessionCount, sessionsOpen, setSessionsOpen } =
47
+ useActiveSessionDisclosure(props.selectedInstance?.ok ? props.selectedInstance.port : null);
38
48
  const showEmpty = !props.error && !props.loading && props.instances.length === 0
39
49
  && !props.instances.some(instance => instance.hidden);
40
50
  const visibleInstances = props.selectedInstance && !props.filtered.some(instance => instance.port === props.selectedInstance?.port)
41
51
  ? [props.selectedInstance, ...props.filtered]
42
52
  : props.filtered;
53
+ const handleSelect = (instance: DashboardInstance): void => {
54
+ if (props.onRouteToSettings && props.selectedInstance?.port === instance.port) {
55
+ props.onRouteToSettings(instance.port);
56
+ return;
57
+ }
58
+ props.onSelect(instance);
59
+ };
60
+ const renderActiveSessionList = (port: number): ReactNode =>
61
+ <InstanceSessionList port={port} open={sessionsOpen} />;
43
62
 
44
63
  return (
45
64
  <>
@@ -65,10 +84,14 @@ export function InstanceListContent(props: InstanceListContentProps) {
65
84
  showInlineLabelEditor={props.showInlineLabelEditor}
66
85
  showSidebarRuntimeLine={props.showSidebarRuntimeLine}
67
86
  showSelectedRowActions={props.showSelectedRowActions}
87
+ activeSessionCount={activeSessionCount}
88
+ activeSessionsOpen={sessionsOpen}
89
+ onToggleActiveSessions={() => setSessionsOpen(open => !open)}
90
+ renderActiveSessionList={renderActiveSessionList}
68
91
  profiles={props.profiles}
69
92
  getLabel={props.getLabel}
70
93
  formatUptime={props.formatUptime}
71
- onSelect={props.onSelect}
94
+ onSelect={handleSelect}
72
95
  onPreview={props.onPreview}
73
96
  onMarkActivitySeen={props.onMarkActivitySeen}
74
97
  onInstanceLabelSave={props.onInstanceLabelSave}
@@ -19,6 +19,10 @@ type InstanceRowProps = {
19
19
  showInlineLabelEditor?: boolean;
20
20
  showRuntimeLine?: boolean;
21
21
  showSelectedActions?: boolean;
22
+ /** Session disclosure (Active row only; devlog 260806 D2). */
23
+ sessionCount?: number;
24
+ sessionsOpen?: boolean;
25
+ onToggleSessions?: (port: number) => void;
22
26
  onSelect: (instance: DashboardInstance) => void;
23
27
  onPreview: (instance: DashboardInstance) => void;
24
28
  onMarkActivitySeen: (port: number) => void;
@@ -52,6 +56,13 @@ const OpenIcon = () => (
52
56
  </svg>
53
57
  );
54
58
 
59
+ const ChevronIcon = ({ open }: { open: boolean }) => (
60
+ <svg viewBox="0 0 16 16" width="12" height="12" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"
61
+ style={{ transform: open ? 'rotate(180deg)' : undefined, transition: 'transform 0.12s' }}>
62
+ <path d="M3.5 6l4.5 4.5L12.5 6" />
63
+ </svg>
64
+ );
65
+
55
66
  export function InstanceRow(props: InstanceRowProps) {
56
67
  const lifecycle = props.instance.lifecycle;
57
68
  const reason = lifecycle?.reason || props.instance.healthReason || 'ok';
@@ -106,6 +117,25 @@ export function InstanceRow(props: InstanceRowProps) {
106
117
  {transitionLabel && <span><em className="instance-row-transition">{transitionLabel}</em></span>}
107
118
  </div>
108
119
  <div className="instance-row-quick" onClick={stopAction}>
120
+ {props.priority === 'active' && (props.sessionCount ?? 0) >= 2 && props.onToggleSessions ? (
121
+ // Same nested-interactive debt as the stop/open
122
+ // controls beside it (audit Med#2): aria-expanded +
123
+ // label only, no aria-controls, restructure tracked
124
+ // as a follow-up unit.
125
+ <button
126
+ type="button"
127
+ className="quick-btn action-sessions"
128
+ aria-expanded={props.sessionsOpen === true}
129
+ aria-label={`Sessions (${props.sessionCount})`}
130
+ title={`Sessions (${props.sessionCount})`}
131
+ onClick={(event) => {
132
+ stopAction(event);
133
+ props.onToggleSessions?.(props.instance.port);
134
+ }}
135
+ >
136
+ <ChevronIcon open={props.sessionsOpen === true} />
137
+ </button>
138
+ ) : null}
109
139
  <button
110
140
  type="button"
111
141
  className="quick-btn action-stop"
@@ -140,10 +170,10 @@ export function InstanceRow(props: InstanceRowProps) {
140
170
  </div>
141
171
  </div>
142
172
  <div className="instance-row-meta">
143
- {props.showLatestActivityTitle !== false && props.latestActivityTitle && <span className="instance-row-activity-title">{props.latestActivityTitle}</span>}
173
+ {props.priority !== 'active' && props.showLatestActivityTitle !== false && props.latestActivityTitle && <span className="instance-row-activity-title">{props.latestActivityTitle}</span>}
144
174
  {props.showRuntimeLine !== false && <span className="instance-row-runtime">{props.instance.currentCli || 'cli n/a'} / {props.instance.currentModel || 'model n/a'}</span>}
145
- <span className="instance-row-version">v{props.instance.version || 'n/a'} · {props.uptime}</span>
146
- <span className="instance-row-reason">{new Date(props.instance.lastCheckedAt).toLocaleTimeString()} · {reason}</span>
175
+ {props.priority !== 'active' && <span className="instance-row-version">v{props.instance.version || 'n/a'} · {props.uptime}</span>}
176
+ {props.priority !== 'active' && <span className="instance-row-reason">{new Date(props.instance.lastCheckedAt).toLocaleTimeString()} · {reason}</span>}
147
177
  </div>
148
178
  </button>
149
179
  {props.showInlineLabelEditor !== false && editing ? (
@@ -0,0 +1,132 @@
1
+ import { useCallback, useEffect, useSyncExternalStore } from 'react';
2
+ import type { MouseEvent } from 'react';
3
+ import {
4
+ getSessionSnapshot,
5
+ loadSessions,
6
+ retrySessions,
7
+ subscribeSessions,
8
+ switchSession,
9
+ } from '../lib/session-store';
10
+ import type { ChatSessionSummary } from '../lib/session-store';
11
+
12
+ export type { ChatSessionSummary } from '../lib/session-store';
13
+
14
+ // Inline session disclosure under the Active instance row (devlog
15
+ // 260806_manager_active_session_disclosure). Lazy: fetches only while open.
16
+ // Fields other than id/seq/label/message_count are absent when the instance
17
+ // runs with multiSession off, so everything renders with fallbacks.
18
+ /** "jaw:slack:channel:C0BM..:thread:1785.." → "slack C0BM.. · thread" */
19
+ function summarizeRemoteKey(remoteKey: string): string {
20
+ const parts = remoteKey.split(':');
21
+ if (parts.length >= 4 && parts[0] === 'jaw') {
22
+ const channel = parts[3] || '';
23
+ const threaded = parts.includes('thread');
24
+ return `${parts[1]} ${channel.slice(0, 11)}${threaded ? ' · thread' : ''}`;
25
+ }
26
+ return remoteKey.slice(0, 32);
27
+ }
28
+
29
+ function sessionTitle(session: ChatSessionSummary): string {
30
+ if (session.label && !session.label.startsWith('jaw:')) return session.label;
31
+ if (session.remoteKey) return summarizeRemoteKey(session.remoteKey);
32
+ if (session.label) return summarizeRemoteKey(session.label);
33
+ return session.seq === 0 ? 'Default session' : `Session #${session.seq}`;
34
+ }
35
+
36
+ function relativeTime(iso: string | null | undefined): string | null {
37
+ if (!iso) return null;
38
+ const then = new Date(iso.replace(' ', 'T')).getTime();
39
+ if (Number.isNaN(then)) return null;
40
+ const minutes = Math.floor((Date.now() - then) / 60000);
41
+ if (minutes < 1) return 'now';
42
+ if (minutes < 60) return `${minutes}m ago`;
43
+ const hours = Math.floor(minutes / 60);
44
+ if (hours < 24) return `${hours}h ago`;
45
+ return `${Math.floor(hours / 24)}d ago`;
46
+ }
47
+
48
+ type InstanceSessionListProps = {
49
+ port: number;
50
+ open: boolean;
51
+ onSessionSwitched?: () => void;
52
+ };
53
+
54
+ export function InstanceSessionList(props: InstanceSessionListProps) {
55
+ const subscribe = useCallback(
56
+ (callback: () => void) => subscribeSessions(props.port, callback),
57
+ [props.port],
58
+ );
59
+ const snapshot = useSyncExternalStore(subscribe, () => getSessionSnapshot(props.port));
60
+
61
+ useEffect(() => {
62
+ if (!props.open) return;
63
+ void loadSessions(props.port).catch(() => { /* snapshot exposes the error */ });
64
+ }, [props.open, props.port]);
65
+
66
+ if (!props.open) return null;
67
+
68
+ async function handleSwitch(event: MouseEvent, session: ChatSessionSummary): Promise<void> {
69
+ event.stopPropagation();
70
+ if (session.id === snapshot.data?.activeId || snapshot.switching) return;
71
+ try {
72
+ await switchSession(props.port, session.seq);
73
+ props.onSessionSwitched?.();
74
+ } catch { /* snapshot exposes the error */ }
75
+ }
76
+
77
+ const retry = (event: MouseEvent): void => {
78
+ event.stopPropagation();
79
+ void retrySessions(props.port).catch(() => { /* snapshot exposes the error */ });
80
+ };
81
+
82
+ if (snapshot.error?.kind === 'load') {
83
+ return (
84
+ <div className="instance-session-list" role="list" aria-label="Chat sessions">
85
+ <div className="instance-session-error">
86
+ <span>{snapshot.error.message}</span>
87
+ <button type="button" className="instance-session-retry" onClick={retry}>Retry</button>
88
+ </div>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ const sessions = snapshot.data?.sessions ?? [];
94
+ const activeId = snapshot.data?.activeId ?? null;
95
+
96
+ return (
97
+ <div className="instance-session-list" role="list" aria-label="Chat sessions">
98
+ {snapshot.error?.kind === 'switch' && (
99
+ <div className="instance-session-error">
100
+ <span>{snapshot.error.message}</span>
101
+ <button type="button" className="instance-session-retry" onClick={retry}>Retry</button>
102
+ </div>
103
+ )}
104
+ {sessions.map(session => {
105
+ const isActive = session.id === activeId;
106
+ const when = relativeTime(session.lastActivityAt);
107
+ return (
108
+ <button
109
+ key={session.id}
110
+ type="button"
111
+ role="listitem"
112
+ className={`instance-session-row${isActive ? ' is-active' : ''}`}
113
+ disabled={snapshot.switching}
114
+ aria-current={isActive || undefined}
115
+ onClick={(event) => void handleSwitch(event, session)}
116
+ >
117
+ <span className="instance-session-title">
118
+ {sessionTitle(session)}
119
+ {session.source && <span className="session-source-badge">{session.source}</span>}
120
+ </span>
121
+ <span className="instance-session-meta">
122
+ {snapshot.switching
123
+ ? 'switching…'
124
+ : `${session.message_count} msg${when ? ` · ${when}` : ''}`}
125
+ </span>
126
+ </button>
127
+ );
128
+ })}
129
+ {sessions.length === 0 && <div className="instance-session-empty">Loading sessions…</div>}
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,45 @@
1
+ import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
2
+ import {
3
+ getSessionSnapshot,
4
+ loadSessions,
5
+ subscribeSessions,
6
+ type SessionsSnapshot,
7
+ } from '../lib/session-store';
8
+
9
+ const EMPTY_SESSIONS_SNAPSHOT: SessionsSnapshot = {
10
+ data: null,
11
+ switching: false,
12
+ error: null,
13
+ count: 0,
14
+ };
15
+
16
+ // Session disclosure state for the Active navigator row (devlog 260806 D2):
17
+ // count fetched once per selected instance to decide chevron visibility; the
18
+ // list itself is fetched lazily by InstanceSessionList only while open.
19
+ // Extracted from App.tsx to honor the 500-line dashboard budget; the WP4
20
+ // hardening cycle replaces the fetch here with the shared session store.
21
+ export function useActiveSessionDisclosure(activeSessionPort: number | null) {
22
+ const [sessionsOpen, setSessionsOpen] = useState(false);
23
+ const subscribe = useCallback(
24
+ (callback: () => void) => activeSessionPort == null
25
+ ? () => {}
26
+ : subscribeSessions(activeSessionPort, callback),
27
+ [activeSessionPort],
28
+ );
29
+ const getSnapshot = useCallback(
30
+ () => activeSessionPort == null
31
+ ? EMPTY_SESSIONS_SNAPSHOT
32
+ : getSessionSnapshot(activeSessionPort),
33
+ [activeSessionPort],
34
+ );
35
+ const snapshot = useSyncExternalStore(subscribe, getSnapshot);
36
+
37
+ useEffect(() => {
38
+ setSessionsOpen(false);
39
+ if (activeSessionPort == null) return;
40
+ void loadSessions(activeSessionPort)
41
+ .catch(() => { /* offline or pre-session build: chevron simply stays hidden */ });
42
+ }, [activeSessionPort]);
43
+
44
+ return { activeSessionCount: snapshot.count, sessionsOpen, setSessionsOpen };
45
+ }
@@ -0,0 +1,93 @@
1
+ const DEFAULT_RETRIES = 2;
2
+ const DEFAULT_BASE_DELAY_MS = 500;
3
+ const MAX_TOTAL_WAIT_MS = 10_000;
4
+
5
+ type Delay = (ms: number, signal?: AbortSignal) => Promise<void>;
6
+
7
+ export type ResilientFetchOptions = {
8
+ retries?: number;
9
+ baseDelayMs?: number;
10
+ signal?: AbortSignal;
11
+ /** Dependency injection for deterministic unit tests. */
12
+ fetchImpl?: typeof fetch;
13
+ delay?: Delay;
14
+ random?: () => number;
15
+ };
16
+
17
+ export function parseRetryAfterMs(header: string | null): number | null {
18
+ const value = header?.trim();
19
+ if (!value) return null;
20
+
21
+ if (/^\d+$/.test(value)) {
22
+ const seconds = Number(value);
23
+ const delayMs = seconds * 1000;
24
+ return Number.isSafeInteger(delayMs) ? delayMs : null;
25
+ }
26
+
27
+ const timestamp = Date.parse(value);
28
+ if (!Number.isFinite(timestamp)) return null;
29
+ const delayMs = timestamp - Date.now();
30
+ return delayMs > 0 ? delayMs : null;
31
+ }
32
+
33
+ function abortError(): DOMException {
34
+ return new DOMException('The operation was aborted.', 'AbortError');
35
+ }
36
+
37
+ function defaultDelay(ms: number, signal?: AbortSignal): Promise<void> {
38
+ return new Promise((resolve, reject) => {
39
+ if (signal?.aborted) {
40
+ reject(abortError());
41
+ return;
42
+ }
43
+ const onAbort = () => {
44
+ clearTimeout(timer);
45
+ reject(abortError());
46
+ };
47
+ const timer = setTimeout(() => {
48
+ signal?.removeEventListener('abort', onAbort);
49
+ resolve();
50
+ }, ms);
51
+ signal?.addEventListener('abort', onAbort, { once: true });
52
+ });
53
+ }
54
+
55
+ function boundedInteger(value: number | undefined, fallback: number): number {
56
+ if (value === undefined || !Number.isFinite(value)) return fallback;
57
+ return Math.max(0, Math.floor(value));
58
+ }
59
+
60
+ export async function resilientGet(
61
+ url: string,
62
+ opts: ResilientFetchOptions = {},
63
+ ): Promise<Response> {
64
+ const retries = boundedInteger(opts.retries, DEFAULT_RETRIES);
65
+ const baseDelayMs = boundedInteger(opts.baseDelayMs, DEFAULT_BASE_DELAY_MS);
66
+ const fetchImpl = opts.fetchImpl ?? fetch;
67
+ const delay = opts.delay ?? defaultDelay;
68
+ const random = opts.random ?? Math.random;
69
+ const requestInit: RequestInit = {
70
+ method: 'GET',
71
+ ...(opts.signal ? { signal: opts.signal } : {}),
72
+ };
73
+ let totalWaitMs = 0;
74
+ let response = await fetchImpl(url, requestInit);
75
+
76
+ for (let attempt = 0; response.status === 429 && attempt < retries; attempt++) {
77
+ const remainingWaitMs = MAX_TOTAL_WAIT_MS - totalWaitMs;
78
+ if (remainingWaitMs <= 0) return response;
79
+
80
+ // RFC 6585 clients may honor Retry-After. Without it, use AWS-style
81
+ // full jitter over a capped exponential backoff to avoid retry herds
82
+ // (rate-limit hardening research 001 §B).
83
+ const retryAfterMs = parseRetryAfterMs(response.headers.get('Retry-After'));
84
+ const backoffCapMs = Math.min(baseDelayMs * (2 ** attempt), MAX_TOTAL_WAIT_MS);
85
+ const jitter = Math.min(1, Math.max(0, random())) * backoffCapMs;
86
+ const waitMs = Math.min(retryAfterMs ?? jitter, remainingWaitMs);
87
+ await delay(waitMs, opts.signal);
88
+ totalWaitMs += waitMs;
89
+ response = await fetchImpl(url, requestInit);
90
+ }
91
+
92
+ return response;
93
+ }
@@ -0,0 +1,216 @@
1
+ import { resilientGet } from './resilient-fetch';
2
+
3
+ const SESSION_TTL_MS = 2_000;
4
+
5
+ export type ChatSessionSummary = {
6
+ id: string;
7
+ seq: number;
8
+ label: string | null;
9
+ remoteKey?: string | null;
10
+ source?: string;
11
+ message_count: number;
12
+ lastActivityAt?: string | null;
13
+ };
14
+
15
+ type SessionsPayload = { sessions?: ChatSessionSummary[]; active?: string };
16
+
17
+ export type FetchResult = {
18
+ sessions: ChatSessionSummary[];
19
+ activeId: string | null;
20
+ };
21
+
22
+ type SessionStoreErrorKind = { kind: 'load' } | { kind: 'switch'; seq: number };
23
+
24
+ export type SessionsSnapshot = {
25
+ data: FetchResult | null;
26
+ switching: boolean;
27
+ error: { kind: 'load'; message: string } | { kind: 'switch'; seq: number; message: string } | null;
28
+ count: number;
29
+ };
30
+
31
+ export type SessionStoreEntry = {
32
+ generation: number;
33
+ at: number;
34
+ promise: Promise<FetchResult> | null;
35
+ data: FetchResult | null;
36
+ switching: boolean;
37
+ lastError: SessionStoreErrorKind | null;
38
+ errorMessage: string | null;
39
+ snapshot: SessionsSnapshot;
40
+ listeners: Set<() => void>;
41
+ };
42
+
43
+ type SessionStoreTestOptions = {
44
+ fetchImpl?: typeof fetch;
45
+ now?: () => number;
46
+ };
47
+
48
+ const entries = new Map<number, SessionStoreEntry>();
49
+ let fetchImpl: typeof fetch = (...args) => fetch(...args);
50
+ let now = (): number => Date.now();
51
+
52
+ function makeSnapshot(entry: Pick<SessionStoreEntry, 'data' | 'switching' | 'lastError' | 'errorMessage'>): SessionsSnapshot {
53
+ let error: SessionsSnapshot['error'] = null;
54
+ if (entry.lastError?.kind === 'load') {
55
+ error = { kind: 'load', message: entry.errorMessage ?? 'sessions load failed' };
56
+ } else if (entry.lastError?.kind === 'switch') {
57
+ error = {
58
+ kind: 'switch',
59
+ seq: entry.lastError.seq,
60
+ message: entry.errorMessage ?? 'session switch failed',
61
+ };
62
+ }
63
+ return {
64
+ data: entry.data,
65
+ switching: entry.switching,
66
+ error,
67
+ count: entry.data?.sessions.length ?? 0,
68
+ };
69
+ }
70
+
71
+ function createEntry(): SessionStoreEntry {
72
+ const entry: SessionStoreEntry = {
73
+ generation: 0,
74
+ at: 0,
75
+ promise: null,
76
+ data: null,
77
+ switching: false,
78
+ lastError: null,
79
+ errorMessage: null,
80
+ snapshot: { data: null, switching: false, error: null, count: 0 },
81
+ listeners: new Set(),
82
+ };
83
+ return entry;
84
+ }
85
+
86
+ function entryFor(port: number): SessionStoreEntry {
87
+ let entry = entries.get(port);
88
+ if (!entry) {
89
+ entry = createEntry();
90
+ entries.set(port, entry);
91
+ }
92
+ return entry;
93
+ }
94
+
95
+ function errorMessage(error: unknown): string {
96
+ return error instanceof Error ? error.message : String(error);
97
+ }
98
+
99
+ function commit(entry: SessionStoreEntry): void {
100
+ entry.snapshot = makeSnapshot(entry);
101
+ for (const listener of [...entry.listeners]) listener();
102
+ }
103
+
104
+ async function fetchChatSessions(port: number): Promise<FetchResult> {
105
+ const response = await resilientGet(`/i/${port}/api/chat-sessions`, { fetchImpl });
106
+ if (!response.ok) throw new Error(`sessions fetch failed: ${response.status}`);
107
+ const body = await response.json() as { ok?: boolean; data?: SessionsPayload };
108
+ const data = body.data ?? {};
109
+ return {
110
+ sessions: Array.isArray(data.sessions) ? data.sessions : [],
111
+ activeId: typeof data.active === 'string' ? data.active : null,
112
+ };
113
+ }
114
+
115
+ async function postSessionSwitch(port: number, seq: number): Promise<void> {
116
+ const response = await fetchImpl(`/i/${port}/api/chat-sessions/${seq}/switch`, { method: 'POST' });
117
+ if (response.ok) return;
118
+ const body = await response.json().catch(() => ({})) as { error?: string };
119
+ throw new Error(body.error || `switch failed: ${response.status}`);
120
+ }
121
+
122
+ export function subscribeSessions(port: number, callback: () => void): () => void {
123
+ const entry = entryFor(port);
124
+ entry.listeners.add(callback);
125
+ return () => { entry.listeners.delete(callback); };
126
+ }
127
+
128
+ export function getSessionSnapshot(port: number): SessionsSnapshot {
129
+ return entryFor(port).snapshot;
130
+ }
131
+
132
+ export function readSessions(port: number): Pick<SessionsSnapshot, 'data' | 'switching'> {
133
+ const snapshot = getSessionSnapshot(port);
134
+ return { data: snapshot.data, switching: snapshot.switching };
135
+ }
136
+
137
+ export async function loadSessions(port: number): Promise<void> {
138
+ const entry = entryFor(port);
139
+ if (entry.promise) {
140
+ await entry.promise;
141
+ return;
142
+ }
143
+ if (entry.data && now() - entry.at < SESSION_TTL_MS) return;
144
+
145
+ const generation = entry.generation;
146
+ const request = fetchChatSessions(port);
147
+ entry.promise = request;
148
+ try {
149
+ const data = await request;
150
+ // A switch invalidates older GETs. Only the current generation may commit.
151
+ if (entry.generation !== generation) return;
152
+ entry.data = data;
153
+ entry.at = now();
154
+ entry.lastError = null;
155
+ entry.errorMessage = null;
156
+ commit(entry);
157
+ } catch (error) {
158
+ if (entry.promise === request) entry.promise = null;
159
+ if (entry.generation === generation) {
160
+ entry.lastError = { kind: 'load' };
161
+ entry.errorMessage = errorMessage(error);
162
+ commit(entry);
163
+ }
164
+ throw error;
165
+ } finally {
166
+ if (entry.promise === request) entry.promise = null;
167
+ }
168
+ }
169
+
170
+ export async function switchSession(port: number, seq: number): Promise<void> {
171
+ const entry = entryFor(port);
172
+ // This port-level lock is shared by every mounted consumer.
173
+ if (entry.switching) return;
174
+ entry.switching = true;
175
+ entry.lastError = null;
176
+ entry.errorMessage = null;
177
+ commit(entry);
178
+
179
+ try {
180
+ try {
181
+ await postSessionSwitch(port, seq);
182
+ } catch (error) {
183
+ entry.lastError = { kind: 'switch', seq };
184
+ entry.errorMessage = errorMessage(error);
185
+ commit(entry);
186
+ throw error;
187
+ }
188
+
189
+ entry.generation++;
190
+ entry.at = Number.NEGATIVE_INFINITY;
191
+ entry.promise = null;
192
+ await loadSessions(port);
193
+ } finally {
194
+ entry.switching = false;
195
+ commit(entry);
196
+ }
197
+ }
198
+
199
+ export async function retrySessions(port: number): Promise<void> {
200
+ const lastError = entryFor(port).lastError;
201
+ if (lastError?.kind === 'switch') {
202
+ await switchSession(port, lastError.seq);
203
+ return;
204
+ }
205
+ await loadSessions(port);
206
+ }
207
+
208
+ export function resetSessionStoreForTest(options: SessionStoreTestOptions = {}): void {
209
+ entries.clear();
210
+ fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
211
+ now = options.now ?? (() => Date.now());
212
+ }
213
+
214
+ export function getSessionListenerCountForTest(port: number): number {
215
+ return entries.get(port)?.listeners.size ?? 0;
216
+ }