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
@@ -3,10 +3,12 @@ import os from 'os';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { join } from 'path';
6
- import { DEFAULT_CLI, buildDefaultPerCli } from '../cli/registry.js';
6
+ import { DEFAULT_CLI, CLI_KEYS, buildDefaultPerCli } from '../cli/registry.js';
7
7
  import { pickFirstReadyCli } from '../cli/readiness.js';
8
8
  import { migrateLegacyClaudeValue } from '../cli/claude-models.js';
9
9
  import { resolveHomePath } from './path-expand.js';
10
+ import { SETUP_STATE_FILE } from './install-integrity.js';
11
+ import { sanitizeSettingsInput, } from './settings-merge.js';
10
12
  export { detectAllCli, detectCli, getClaudeExecHelperCandidates, getClaudeIHelperCandidates } from './cli-detection.js';
11
13
  // ─── Version (single source of truth: package.json) ──
12
14
  import { dirname } from 'path';
@@ -164,8 +166,23 @@ export function runMigration(projectDir) {
164
166
  fs.writeFileSync(MIGRATION_MARKER, JSON.stringify({ migratedAt: new Date().toISOString() }));
165
167
  }
166
168
  // ─── Settings ────────────────────────────────────────
169
+ export const SETTINGS_SCHEMA_VERSION = 3;
170
+ export const RUNTIME_DEFAULT_MIGRATION_ID = 'codex-app-default-v2';
171
+ export const MULTI_SESSION_DEFAULT_MIGRATION_ID = 'multi-session-default-v3';
172
+ // What multiSession meant before this flip. A document written by a schema that predates
173
+ // the new defaults must resolve absent keys to these, not to the new ones — otherwise the
174
+ // upgrade turns sessions on for someone who never asked (110 §4b-1).
175
+ export const LEGACY_MULTI_SESSION_BASELINE = {
176
+ enabled: false,
177
+ maxConcurrent: 1,
178
+ midRunPolicy: 'steer',
179
+ channels: { telegram: false, discord: false, slack: true },
180
+ };
167
181
  function createDefaultSettings() {
168
182
  return {
183
+ settingsSchemaVersion: SETTINGS_SCHEMA_VERSION,
184
+ runtimeDefaultMigration: null,
185
+ multiSessionDefaultMigration: null,
169
186
  port: '', // persisted by server on startup; CLI commands use as fallback
170
187
  cli: DEFAULT_CLI,
171
188
  fallbackOrder: [],
@@ -226,12 +243,35 @@ function createDefaultSettings() {
226
243
  // Slack bots typically live in shared team channels, where
227
244
  // answering every message is antisocial. DMs bypass this gate.
228
245
  mentionOnly: true,
246
+ // But a thread the bot participates in keeps flowing without
247
+ // re-mention (true = strict, mention required even in threads).
248
+ threadRequireMention: false,
229
249
  replyInThread: true,
250
+ inboundDownloadConcurrency: 6,
230
251
  },
231
252
  messaging: {
232
253
  latestSeen: { telegram: null, discord: null, slack: null },
233
254
  lastActive: { telegram: null, discord: null, slack: null },
234
255
  },
256
+ multiSession: {
257
+ // Sessions are on by default as of schema v3. An existing install does not
258
+ // get this silently: the merge substitutes the legacy baseline for its
259
+ // cohort and asks (110 §4b).
260
+ enabled: true,
261
+ // Two, not more: the observable unit is that a second tab does not wait for
262
+ // the first. Beyond that is the user's CPU and token budget to spend.
263
+ maxConcurrent: 2,
264
+ midRunPolicy: 'steer',
265
+ channels: { telegram: false, discord: false, slack: true },
266
+ },
267
+ // Opt-in Markdown vault (devlog 040). Default OFF and nothing is created on
268
+ // disk until it is explicitly enabled, so a user who never turns it on sees no
269
+ // new directory and no change in behaviour.
270
+ wiki: {
271
+ enabled: false,
272
+ root: '~/jaw-wiki',
273
+ promptDigest: false,
274
+ },
235
275
  memory: {
236
276
  enabled: true,
237
277
  flushEvery: 10,
@@ -242,10 +282,16 @@ function createDefaultSettings() {
242
282
  autoReflectAfterFlush: false,
243
283
  flushMessageWindow: 0,
244
284
  },
285
+ search: { engine: 'like' },
245
286
  trace: {
246
287
  retentionDays: 7,
247
288
  maxRows: 50000,
248
289
  },
290
+ runtime: {
291
+ codexApp: {
292
+ multiplex: false,
293
+ },
294
+ },
249
295
  code: {
250
296
  maxConcurrentSessions: 4,
251
297
  idleReapMs: 30_000,
@@ -298,6 +344,74 @@ function createDefaultSettings() {
298
344
  };
299
345
  }
300
346
  export const DEFAULT_SETTINGS = createDefaultSettings();
347
+ /**
348
+ * The keys a document must carry to legitimately claim the current schema.
349
+ *
350
+ * `cli-jaw init` writes a settings file, so a new install is not the ENOENT case the
351
+ * cohort rules were written around. Left unstamped, its document reads as v1 and the
352
+ * person who just installed is offered a migration away from a state they never had.
353
+ * Stamping the version alone is not enough either — the loader holds a document claiming
354
+ * this schema to what this schema writes — so the writer takes the whole set from here
355
+ * rather than assembling it from memory (110 §4b-3).
356
+ */
357
+ export function freshInstallSchemaFields() {
358
+ // Same question as the loader asks, answered the same way: `init` running against a
359
+ // home that already has a database is re-initialising, not installing.
360
+ const defaults = settingsForHomeWithoutSettingsFile();
361
+ return {
362
+ settingsSchemaVersion: SETTINGS_SCHEMA_VERSION,
363
+ multiSession: defaults.multiSession,
364
+ // Null only for a home with no history — there is nothing to migrate from and
365
+ // nothing to ask about. An established home carries the pending marker instead.
366
+ multiSessionDefaultMigration: defaults.multiSessionDefaultMigration,
367
+ };
368
+ }
369
+ // Things a home only has once someone has set it up or used it. A settings file can be
370
+ // deleted, lost to a failed write, or restored from a backup that omitted it — none of
371
+ // which makes the install new. These do not come back on their own.
372
+ //
373
+ // The list errs toward "established" on purpose. Calling a new home established costs a
374
+ // question the user did not need to answer; calling an established home new turns a
375
+ // feature on for someone who was never asked. Those are not the same mistake. That is
376
+ // also why the postinstall-created directories stay in the list even though they can
377
+ // precede a first boot.
378
+ const ESTABLISHED_HOME_ARTIFACTS = [
379
+ DB_PATH,
380
+ MIGRATION_MARKER,
381
+ PROMPTS_DIR,
382
+ UPLOADS_DIR,
383
+ // `jaw init` leaves all three, and an init-only home whose settings file was then
384
+ // lost is exactly the case that must not read as new.
385
+ join(JAW_HOME, SETUP_STATE_FILE),
386
+ HEARTBEAT_JOBS_PATH,
387
+ SKILLS_DIR,
388
+ WIDGETS_DIR,
389
+ ];
390
+ /**
391
+ * An absent settings file is not proof of a new install, and the difference decides
392
+ * whether sessions start on. Getting it wrong turns a feature on for someone who was
393
+ * never asked, which is the one outcome the whole migration exists to prevent — so the
394
+ * question is answered from the home directory rather than from one file in it.
395
+ */
396
+ export function isEstablishedHome() {
397
+ return ESTABLISHED_HOME_ARTIFACTS.some(path => fs.existsSync(path));
398
+ }
399
+ /**
400
+ * What a home with no readable settings should start with. A home that has been used
401
+ * before gets the previous meaning plus a pending marker, so it is asked like any other
402
+ * upgrade; only a home with no history at all gets the new defaults.
403
+ */
404
+ export function settingsForHomeWithoutSettingsFile() {
405
+ const next = createDefaultSettings();
406
+ if (!isEstablishedHome())
407
+ return next;
408
+ next.multiSession = { ...next.multiSession, ...LEGACY_MULTI_SESSION_BASELINE };
409
+ next.multiSessionDefaultMigration = {
410
+ id: MULTI_SESSION_DEFAULT_MIGRATION_ID,
411
+ state: 'pending',
412
+ };
413
+ return next;
414
+ }
301
415
  export function normalizeModelForCli(cli, model) {
302
416
  if (typeof model !== 'string')
303
417
  return model;
@@ -336,7 +450,7 @@ function normalizeActiveOverrides(activeOverrides = {}, perCli = {}) {
336
450
  return next;
337
451
  }
338
452
  /** @internal — exported for unit testing */
339
- export function migrateSettings(s) {
453
+ export function migrateSettings(s, sourceVersion = readSettingsSchemaVersion(s)) {
340
454
  if (s["planning"]) {
341
455
  if (s["planning"].cli && s["planning"].cli !== s["cli"])
342
456
  s["cli"] = s["planning"].cli;
@@ -352,6 +466,51 @@ export function migrateSettings(s) {
352
466
  }
353
467
  delete s["planning"];
354
468
  }
469
+ if (sourceVersion === 1) {
470
+ const fromCli = CLI_KEYS.includes(s["cli"]) ? s["cli"] : 'claude';
471
+ s["cli"] = fromCli;
472
+ s["settingsSchemaVersion"] = SETTINGS_SCHEMA_VERSION;
473
+ s["runtimeDefaultMigration"] = {
474
+ id: RUNTIME_DEFAULT_MIGRATION_ID,
475
+ state: fromCli === 'codex-app' ? 'already-codex-app' : 'pending',
476
+ fromCli,
477
+ toCli: 'codex-app',
478
+ };
479
+ }
480
+ else {
481
+ validateRuntimeDefaultMigration(s["runtimeDefaultMigration"]);
482
+ }
483
+ // The session-default flip is its own migration with its own marker. It is NOT
484
+ // folded into the runtime one: the two flips can be rolled back at different times
485
+ // for different reasons, and a shared marker would make that impossible to express.
486
+ // A v1 document crosses both boundaries and so gets both markers in this one pass.
487
+ if (sourceVersion < SETTINGS_SCHEMA_VERSION) {
488
+ s["settingsSchemaVersion"] = SETTINGS_SCHEMA_VERSION;
489
+ s["multiSessionDefaultMigration"] = {
490
+ id: MULTI_SESSION_DEFAULT_MIGRATION_ID,
491
+ // Someone who already turned sessions on made this decision before we asked.
492
+ // Offering it again would be noise, and accepting for them would change a
493
+ // concurrency they chose.
494
+ state: s["multiSession"]?.enabled === true ? 'already-enabled' : 'pending',
495
+ };
496
+ }
497
+ else {
498
+ validateMultiSessionDefaultMigration(s["multiSessionDefaultMigration"]);
499
+ }
500
+ // A pending marker means the user has not answered yet, so sessions being on
501
+ // contradicts it. The dedicated accept route moves both together, but it is not the
502
+ // only way in: the settings watcher merges an external file write, and a generic
503
+ // settings patch can set `multiSession.enabled` directly. Both strip the marker as
504
+ // server-owned and then leave this pair inconsistent — enabled, and still pending.
505
+ //
506
+ // Resolved by treating an enable while pending AS the answer rather than by refusing
507
+ // it. Someone who reaches in and turns it on has decided; what must not survive is a
508
+ // marker that still claims to be waiting, because the next boot would ask again and
509
+ // the record would no longer mean anything.
510
+ const sessionMigration = s["multiSessionDefaultMigration"];
511
+ if (sessionMigration?.state === 'pending' && s["multiSession"]?.enabled === true) {
512
+ s["multiSessionDefaultMigration"] = { ...sessionMigration, state: 'accepted' };
513
+ }
355
514
  // Claude model alias migration
356
515
  s["perCli"] = normalizePerCliModels(s["perCli"] || {});
357
516
  s["activeOverrides"] = normalizeActiveOverrides(s["activeOverrides"] || {}, s["perCli"] || {});
@@ -394,8 +553,14 @@ export function migrateSettings(s) {
394
553
  allowBots: false,
395
554
  mentionOnly: true,
396
555
  replyInThread: true,
556
+ inboundDownloadConcurrency: 6,
397
557
  };
398
558
  }
559
+ if (!Number.isInteger(s["slack"].inboundDownloadConcurrency)
560
+ || s["slack"].inboundDownloadConcurrency < 1
561
+ || s["slack"].inboundDownloadConcurrency > 32) {
562
+ s["slack"].inboundDownloadConcurrency = 6;
563
+ }
399
564
  if (!s["messaging"]) {
400
565
  s["messaging"] = {
401
566
  latestSeen: { telegram: null, discord: null, slack: null },
@@ -412,6 +577,44 @@ export function migrateSettings(s) {
412
577
  s["messaging"].lastActive.slack = null;
413
578
  }
414
579
  }
580
+ if (!s["multiSession"]) {
581
+ s["multiSession"] = createDefaultSettings().multiSession;
582
+ }
583
+ else {
584
+ // Deliberately the OLD defaults, not createDefaultSettings(). This function takes
585
+ // an already-merged object and cannot tell which cohort it came from, so handing
586
+ // out the new defaults here would enable sessions for a legacy document that
587
+ // reached this line by some path other than the boot merge. The boot merge fills
588
+ // both keys from the cohort baseline, which makes these two lines unreachable on
589
+ // the normal path — they are the net under it, and a net set to the new defaults
590
+ // is not a net (110 §4b-2).
591
+ if (s["multiSession"].enabled === undefined) {
592
+ s["multiSession"].enabled = LEGACY_MULTI_SESSION_BASELINE.enabled;
593
+ }
594
+ if (s["multiSession"].maxConcurrent === undefined) {
595
+ s["multiSession"].maxConcurrent = LEGACY_MULTI_SESSION_BASELINE.maxConcurrent;
596
+ }
597
+ if (s["multiSession"].midRunPolicy === undefined)
598
+ s["multiSession"].midRunPolicy = 'steer';
599
+ if (s["multiSession"].channels === undefined) {
600
+ s["multiSession"].channels = { telegram: false, discord: false, slack: true };
601
+ }
602
+ else {
603
+ if (s["multiSession"].channels.telegram === undefined)
604
+ s["multiSession"].channels.telegram = false;
605
+ if (s["multiSession"].channels.discord === undefined)
606
+ s["multiSession"].channels.discord = false;
607
+ if (s["multiSession"].channels.slack === undefined)
608
+ s["multiSession"].channels.slack = true;
609
+ }
610
+ }
611
+ const maxConcurrent = Number(s["multiSession"].maxConcurrent);
612
+ s["multiSession"].maxConcurrent = Number.isInteger(maxConcurrent) && maxConcurrent > 0
613
+ ? maxConcurrent
614
+ : 1;
615
+ if (!['steer', 'followup', 'collect', 'interrupt'].includes(s["multiSession"].midRunPolicy)) {
616
+ s["multiSession"].midRunPolicy = 'steer';
617
+ }
415
618
  if (!s["jawCeo"]) {
416
619
  s["jawCeo"] = { openaiApiKey: '' };
417
620
  }
@@ -471,10 +674,145 @@ function applyEnvOverrides(s) {
471
674
  }
472
675
  /** Mutable settings object — shared across all modules via ESM live binding */
473
676
  export let settings = createDefaultSettings();
677
+ let settingsPersistenceShape = 'absent';
678
+ function readSettingsSchemaVersion(raw) {
679
+ if (!("settingsSchemaVersion" in raw))
680
+ return 1;
681
+ const version = raw["settingsSchemaVersion"];
682
+ if (!Number.isInteger(version) || version < 1 || version > SETTINGS_SCHEMA_VERSION) {
683
+ throw new Error(`unsupported_settings_schema_version:${String(version)}`);
684
+ }
685
+ return version;
686
+ }
687
+ function validateRuntimeDefaultMigration(value) {
688
+ if (value === null || value === undefined)
689
+ return;
690
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
691
+ throw new Error('invalid_runtime_default_migration');
692
+ }
693
+ const migration = value;
694
+ const keys = Object.keys(migration).sort();
695
+ const expectedKeys = ['fromCli', 'id', 'state', 'toCli'];
696
+ const validState = ['pending', 'accepted', 'kept', 'already-codex-app'].includes(String(migration["state"]));
697
+ if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)
698
+ || migration["id"] !== RUNTIME_DEFAULT_MIGRATION_ID
699
+ || !validState
700
+ || typeof migration["fromCli"] !== 'string'
701
+ || migration["toCli"] !== 'codex-app') {
702
+ throw new Error('invalid_runtime_default_migration');
703
+ }
704
+ }
705
+ function validateMultiSessionDefaultMigration(value) {
706
+ if (value === null || value === undefined)
707
+ return;
708
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
709
+ throw new Error('invalid_multi_session_default_migration');
710
+ }
711
+ const migration = value;
712
+ const keys = Object.keys(migration).sort();
713
+ const validState = ['pending', 'accepted', 'kept', 'already-enabled'].includes(String(migration["state"]));
714
+ if (JSON.stringify(keys) !== JSON.stringify(['id', 'state'])
715
+ || migration["id"] !== MULTI_SESSION_DEFAULT_MIGRATION_ID
716
+ || !validState) {
717
+ throw new Error('invalid_multi_session_default_migration');
718
+ }
719
+ }
720
+ function isPlainRecord(value) {
721
+ return !!value && typeof value === 'object' && !Array.isArray(value);
722
+ }
723
+ // ─── Clobber-protection latch (260806) ───────────────
724
+ // When settings.json existed but could not be read at boot (corrupt JSON, an
725
+ // unsupported/future schema version, a permission error), the in-memory state is
726
+ // defaults — and any later save (messaging.latestSeen, a UI write) would flush those
727
+ // defaults over the user's real file, tokens included. That actually happened: a v2
728
+ // packaged app read a v3 file and wiped a Slack config. While latched, saves commit
729
+ // to memory only and the file on disk is left exactly as it was.
730
+ let settingsPersistenceBlockedReason = null;
731
+ let settingsPersistenceBlockedWarned = false;
732
+ export function isSettingsPersistenceBlocked() {
733
+ return settingsPersistenceBlockedReason !== null;
734
+ }
735
+ /**
736
+ * A v3 document was written by this schema, and this schema always writes both the
737
+ * multiSession block and its migration marker. If either is missing or malformed, the
738
+ * document is not what it claims to be — and the consequence of guessing is specific:
739
+ * an absent block would inherit the new defaults and start running sessions while the
740
+ * marker still said the user had not consented (110 §4b-3). Older documents are held to
741
+ * no such rule: their schema did not know these keys, so absent is normal there and the
742
+ * legacy baseline covers it.
743
+ */
744
+ function assertCurrentSchemaSessionShape(raw) {
745
+ const block = raw["multiSession"];
746
+ if (!isPlainRecord(block))
747
+ throw new Error('invalid_multi_session_block');
748
+ if (typeof block["enabled"] !== 'boolean')
749
+ throw new Error('invalid_multi_session_enabled');
750
+ const maxConcurrent = block["maxConcurrent"];
751
+ if (!Number.isInteger(maxConcurrent) || maxConcurrent < 1) {
752
+ throw new Error('invalid_multi_session_max_concurrent');
753
+ }
754
+ if ('channels' in block && !isPlainRecord(block["channels"])) {
755
+ throw new Error('invalid_multi_session_channels');
756
+ }
757
+ if (!('multiSessionDefaultMigration' in raw)) {
758
+ throw new Error('missing_multi_session_default_migration');
759
+ }
760
+ validateMultiSessionDefaultMigration(raw["multiSessionDefaultMigration"]);
761
+ }
474
762
  export function loadSettings() {
763
+ // A fresh load is a fresh verdict: a successful parse (or a genuinely absent
764
+ // file) below re-enables persistence; only the unreadable-existing-file catch
765
+ // re-arms the latch.
766
+ settingsPersistenceBlockedReason = null;
767
+ settingsPersistenceBlockedWarned = false;
475
768
  try {
476
- const raw = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
769
+ let raw = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
770
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
771
+ throw new Error('invalid_settings_document');
772
+ }
773
+ const sanitized = sanitizeSettingsInput(raw, 'boot');
774
+ raw = sanitized.value;
775
+ const nextShape = sanitized.persistenceShape;
776
+ if (sanitized.rejectedPaths.length > 0) {
777
+ console.warn(`[jaw:settings] ignored server-owned settings fields: ${sanitized.rejectedPaths.join(', ')}`);
778
+ }
779
+ if (sanitized.invalidPaths.length > 0) {
780
+ console.warn(`[jaw:settings] ignored invalid settings fields: ${sanitized.invalidPaths.join(', ')}`);
781
+ }
782
+ const sourceVersion = readSettingsSchemaVersion(raw);
783
+ // Dropping a malformed field and carrying on is right for a patch arriving over
784
+ // the API — the rest of the patch is still what the caller meant. It is wrong for
785
+ // a document claiming the current schema at boot: this schema wrote that block,
786
+ // so a malformed one means the file is not what it says, and silently replacing
787
+ // it with defaults is how a pending consent turns into a running feature.
788
+ if (sourceVersion >= 3 && sanitized.invalidPaths.some(path => path.startsWith('multiSession'))) {
789
+ throw new Error(`invalid_multi_session_block:${sanitized.invalidPaths.join(',')}`);
790
+ }
791
+ // A v1 document predates the codex-app default, so an absent or unknown
792
+ // cli is normalised to the historical fallback rather than inheriting
793
+ // the new default. A v2 document was written by this schema and must
794
+ // already name a known runtime; if it does not, the file is not
795
+ // trustworthy enough to silently supply one, so it closes the same way
796
+ // a corrupt or future-versioned document does.
797
+ if (sourceVersion >= 2 && !CLI_KEYS.includes(raw["cli"])) {
798
+ throw new Error(`invalid_settings_cli:${String(raw["cli"])}`);
799
+ }
800
+ // A document claiming the current schema must actually carry what this schema
801
+ // writes. Checked before the merge, because after it the absence is gone.
802
+ if (sourceVersion >= 3)
803
+ assertCurrentSchemaSessionShape(raw);
804
+ const legacyCli = sourceVersion === 1 && !CLI_KEYS.includes(raw["cli"])
805
+ ? 'claude'
806
+ : raw["cli"];
807
+ const hadPlanning = !!raw.planning;
477
808
  const defaults = createDefaultSettings();
809
+ // Everything below the current schema predates the session defaults, so absent
810
+ // keys resolve to what they used to mean rather than to the new defaults. The
811
+ // merge runs before migrateSettings, so this is the only place that still knows
812
+ // whether a key was in the document or is about to be invented (110 §4b-1).
813
+ const multiSessionBaseline = sourceVersion < SETTINGS_SCHEMA_VERSION
814
+ ? LEGACY_MULTI_SESSION_BASELINE
815
+ : defaults.multiSession;
478
816
  // Deep merge perCli so new CLI defaults (e.g. copilot) are preserved
479
817
  const mergedPerCli = buildDefaultPerCli();
480
818
  if (raw.perCli) {
@@ -485,12 +823,18 @@ export function loadSettings() {
485
823
  const merged = migrateSettings({
486
824
  ...defaults,
487
825
  ...raw,
826
+ ...(sourceVersion === 1 ? { cli: legacyCli } : {}),
488
827
  perCli: mergedPerCli,
489
828
  tui: { ...defaults.tui, ...(raw.tui || {}) },
490
829
  telegram: { ...defaults.telegram, ...(raw.telegram || {}) },
491
830
  discord: { ...defaults.discord, ...(raw.discord || {}) },
492
831
  slack: { ...defaults.slack, ...(raw.slack || {}) },
493
832
  memory: { ...defaults.memory, ...(raw.memory || {}) },
833
+ search: {
834
+ ...defaults.search,
835
+ ...(raw.search || {}),
836
+ engine: raw.search?.engine === 'fts5' ? 'fts5' : 'like',
837
+ },
494
838
  trace: { ...defaults.trace, ...(raw.trace || {}) },
495
839
  avatar: {
496
840
  agent: { ...defaults.avatar.agent, ...(raw.avatar?.agent || {}) },
@@ -503,17 +847,39 @@ export function loadSettings() {
503
847
  jawCeo: { ...defaults.jawCeo, ...(raw.jawCeo || {}) },
504
848
  pi: { ...defaults.pi, ...(raw.pi || {}) },
505
849
  network: { ...defaults.network, ...(raw.network || {}) },
850
+ runtime: {
851
+ ...defaults.runtime,
852
+ ...(raw.runtime || {}),
853
+ codexApp: {
854
+ ...defaults.runtime.codexApp,
855
+ ...(raw.runtime?.codexApp || {}),
856
+ },
857
+ },
506
858
  code: { ...defaults.code, ...(raw.code || {}) },
507
- });
859
+ multiSession: {
860
+ ...multiSessionBaseline,
861
+ ...(raw.multiSession || {}),
862
+ channels: { ...multiSessionBaseline.channels, ...(raw.multiSession?.channels || {}) },
863
+ },
864
+ wiki: { ...defaults.wiki, ...(raw.wiki || {}) },
865
+ }, sourceVersion);
508
866
  // #64 safety: auto-correct stale workingDir (e.g. copied instance)
509
867
  // but allow valid paths to persist (dynamic project targeting)
868
+ // Any document below the current schema was rewritten by the migration above, so
869
+ // it has to reach disk. Leaving it unsaved would keep memory at v3 while the file
870
+ // stayed older, and the next boot would run the migration again — including
871
+ // recreating a marker the user had already resolved.
872
+ let needsSave = sourceVersion < SETTINGS_SCHEMA_VERSION || hadPlanning;
510
873
  if (typeof merged["workingDir"] === 'string' && merged["workingDir"] !== JAW_HOME && !fs.existsSync(merged["workingDir"])) {
511
874
  console.warn(`[jaw:workingDir] stale path ${merged["workingDir"]}, resetting to JAW_HOME`);
512
875
  merged["workingDir"] = JAW_HOME;
513
- saveSettings(merged);
876
+ needsSave = true;
514
877
  }
515
- if (raw.planning)
516
- saveSettings(merged);
878
+ const candidate = { value: merged, shape: nextShape };
879
+ if (needsSave)
880
+ persistAndCommit(candidate);
881
+ else
882
+ commitCandidate(candidate);
517
883
  // normalize projectDirs on load (reject corrupted/injected values)
518
884
  merged["projectDirs"] = normalizeProjectDirs(merged["projectDirs"]);
519
885
  // env overrides
@@ -531,22 +897,43 @@ export function loadSettings() {
531
897
  }
532
898
  catch { /* best-effort */ }
533
899
  }
534
- settings = merged;
535
900
  return merged;
536
901
  }
537
902
  catch (error) {
538
- const next = createDefaultSettings();
539
- next.cli = pickFirstReadyCli();
540
- applyEnvOverrides(next);
541
- settings = next;
542
903
  const err = error;
543
904
  if (err?.code === 'ENOENT') {
544
- saveSettings(next);
905
+ // ENOENT means no settings file, not necessarily no install. A home that has
906
+ // a database or an uploads directory has been used, so it is treated as an
907
+ // upgrade and asked rather than switched on.
908
+ const next = settingsForHomeWithoutSettingsFile();
909
+ next.cli = pickFirstReadyCli();
910
+ applyEnvOverrides(next);
911
+ persistAndCommit({ value: next, shape: 'absent' });
545
912
  return next;
546
913
  }
914
+ const next = createDefaultSettings();
915
+ next.cli = 'claude';
916
+ // This branch stands in for a state we could not read — corrupt JSON, an
917
+ // unsupported version, a permission error. The new-install defaults are the wrong
918
+ // thing to borrow here: they would turn sessions on for someone whose real
919
+ // settings might have had them off, and who was never asked. Pinned to what the
920
+ // previous schema meant (110 §4c). The ENOENT branch above is genuinely new and
921
+ // keeps the new defaults.
922
+ next.multiSession = { ...next.multiSession, ...LEGACY_MULTI_SESSION_BASELINE };
923
+ applyEnvOverrides(next);
924
+ commitCandidate({ value: next, shape: 'absent' });
547
925
  console.warn(`[jaw:settings] failed to load ${SETTINGS_PATH}: ${err?.message || String(error)}`);
548
926
  if (fs.existsSync(SETTINGS_PATH)) {
549
- const backupPath = `${SETTINGS_PATH}.corrupt-${Date.now()}.bak`;
927
+ // The file is real and we could not read it, so what is in memory now is
928
+ // NOT the user's settings. Latch persistence so no later save clobbers
929
+ // the file; the backup below is a copy, not a license to overwrite.
930
+ settingsPersistenceBlockedReason = err?.message || String(error);
931
+ let backupTimestamp = Date.now();
932
+ let backupPath = `${SETTINGS_PATH}.corrupt-${backupTimestamp}.bak`;
933
+ while (fs.existsSync(backupPath)) {
934
+ backupTimestamp += 1;
935
+ backupPath = `${SETTINGS_PATH}.corrupt-${backupTimestamp}.bak`;
936
+ }
550
937
  try {
551
938
  fs.copyFileSync(SETTINGS_PATH, backupPath);
552
939
  console.warn(`[jaw:settings] backed up unreadable settings to ${backupPath}`);
@@ -561,10 +948,19 @@ export function loadSettings() {
561
948
  // Self-write fingerprint for the settings watcher: external writers (a separate
562
949
  // `cli-jaw project set` process) produce content that won't match this string.
563
950
  let lastSavedSettingsRaw = null;
564
- export function saveSettings(s) {
565
- settings = s;
566
- const raw = JSON.stringify(s, null, 2);
567
- lastSavedSettingsRaw = raw;
951
+ export function serializeSettingsForSave(candidate) {
952
+ const value = structuredClone(candidate.value);
953
+ const runtime = value["runtime"];
954
+ if (candidate.shape === 'absent' && runtime?.codexApp?.multiplex === false) {
955
+ delete runtime.codexApp.multiplex;
956
+ if (Object.keys(runtime.codexApp).length === 0)
957
+ delete runtime.codexApp;
958
+ if (Object.keys(runtime).length === 0)
959
+ delete value["runtime"];
960
+ }
961
+ return JSON.stringify(value, null, 2);
962
+ }
963
+ function writeSettingsRaw(raw) {
568
964
  // settings.json carries live channel tokens (xoxb-/xapp-/bot tokens), so
569
965
  // it must never be group/other-readable. writeFileSync's mode applies
570
966
  // only at creation — chmod covers the existing-file path.
@@ -576,12 +972,45 @@ export function saveSettings(s) {
576
972
  catch { /* best-effort */ }
577
973
  }
578
974
  }
975
+ export function commitCandidate(candidate) {
976
+ settings = candidate.value;
977
+ settingsPersistenceShape = candidate.shape;
978
+ }
979
+ export function persistAndCommit(candidate, write = writeSettingsRaw) {
980
+ if (settingsPersistenceBlockedReason !== null) {
981
+ // Memory-only: the file on disk holds the user's real settings in a shape
982
+ // this binary could not read, and writing would destroy them.
983
+ if (!settingsPersistenceBlockedWarned) {
984
+ settingsPersistenceBlockedWarned = true;
985
+ console.warn(`[jaw:settings] persistence disabled: settings.json was unreadable at boot (${settingsPersistenceBlockedReason}); refusing to overwrite it`);
986
+ }
987
+ commitCandidate(candidate);
988
+ return;
989
+ }
990
+ const raw = serializeSettingsForSave(candidate);
991
+ write(raw);
992
+ lastSavedSettingsRaw = raw;
993
+ commitCandidate(candidate);
994
+ }
995
+ /** Re-save the current settings pair for legacy callers that mutate settings in place. */
996
+ export function saveSettings(s) {
997
+ persistAndCommit({ value: s, shape: settingsPersistenceShape });
998
+ }
999
+ export function snapshotSettingsState() {
1000
+ return {
1001
+ value: structuredClone(settings),
1002
+ shape: settingsPersistenceShape,
1003
+ };
1004
+ }
1005
+ export function getSettingsPersistenceShape() {
1006
+ return settingsPersistenceShape;
1007
+ }
579
1008
  export function getLastSavedSettingsRaw() {
580
1009
  return lastSavedSettingsRaw;
581
1010
  }
582
1011
  /** Replace settings object (for API PUT /api/settings deep merge) */
583
- export function replaceSettings(s) {
584
- settings = s;
1012
+ export function replaceSettings(s, shape) {
1013
+ commitCandidate({ value: s, shape });
585
1014
  }
586
1015
  export function loadHeartbeatFile() {
587
1016
  try {