agent-nuvira 3.3.3 → 3.3.5

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 (366) hide show
  1. package/README.md +13 -5
  2. package/dist/agent-sdk/src/agent.d.ts +2 -0
  3. package/dist/agent-sdk/src/agent.d.ts.map +1 -1
  4. package/dist/agent-sdk/src/define.d.ts +64 -0
  5. package/dist/agent-sdk/src/define.d.ts.map +1 -0
  6. package/dist/agent-sdk/src/define.js +76 -0
  7. package/dist/agent-sdk/src/define.js.map +1 -0
  8. package/dist/agent-sdk/src/index.d.ts +9 -0
  9. package/dist/agent-sdk/src/index.d.ts.map +1 -1
  10. package/dist/agent-sdk/src/index.js +9 -0
  11. package/dist/agent-sdk/src/index.js.map +1 -1
  12. package/dist/agent-sdk/src/scaffold.d.ts +11 -0
  13. package/dist/agent-sdk/src/scaffold.d.ts.map +1 -1
  14. package/dist/agent-sdk/src/scaffold.js +16 -6
  15. package/dist/agent-sdk/src/scaffold.js.map +1 -1
  16. package/dist/agents/long-form-plan.d.ts.map +1 -1
  17. package/dist/agents/long-form-plan.js +2 -1
  18. package/dist/agents/long-form-plan.js.map +1 -1
  19. package/dist/agents/orchestrator.d.ts.map +1 -1
  20. package/dist/agents/orchestrator.js +5 -4
  21. package/dist/agents/orchestrator.js.map +1 -1
  22. package/dist/cli/agent.d.ts +2 -2
  23. package/dist/cli/agent.js +10 -10
  24. package/dist/cli/chat.d.ts +94 -0
  25. package/dist/cli/chat.d.ts.map +1 -1
  26. package/dist/cli/chat.js +354 -20
  27. package/dist/cli/chat.js.map +1 -1
  28. package/dist/cli/cli-program.d.ts.map +1 -1
  29. package/dist/cli/cli-program.js +5 -0
  30. package/dist/cli/cli-program.js.map +1 -1
  31. package/dist/cli/config.d.ts.map +1 -1
  32. package/dist/cli/config.js +9 -1
  33. package/dist/cli/config.js.map +1 -1
  34. package/dist/cli/doctor.d.ts.map +1 -1
  35. package/dist/cli/doctor.js +3 -2
  36. package/dist/cli/doctor.js.map +1 -1
  37. package/dist/cli/edit.js +2 -2
  38. package/dist/cli/eval.d.ts +17 -0
  39. package/dist/cli/eval.d.ts.map +1 -1
  40. package/dist/cli/eval.js +105 -2
  41. package/dist/cli/eval.js.map +1 -1
  42. package/dist/cli/execute.d.ts +16 -1
  43. package/dist/cli/execute.d.ts.map +1 -1
  44. package/dist/cli/execute.js +192 -22
  45. package/dist/cli/execute.js.map +1 -1
  46. package/dist/cli/loop-executor.d.ts +55 -0
  47. package/dist/cli/loop-executor.d.ts.map +1 -1
  48. package/dist/cli/loop-executor.js +212 -13
  49. package/dist/cli/loop-executor.js.map +1 -1
  50. package/dist/cli/model.d.ts.map +1 -1
  51. package/dist/cli/model.js +9 -8
  52. package/dist/cli/model.js.map +1 -1
  53. package/dist/cli/models.d.ts +1 -1
  54. package/dist/cli/models.js +4 -4
  55. package/dist/cli/parity.d.ts +85 -0
  56. package/dist/cli/parity.d.ts.map +1 -0
  57. package/dist/cli/parity.js +506 -0
  58. package/dist/cli/parity.js.map +1 -0
  59. package/dist/cli/plan.d.ts.map +1 -1
  60. package/dist/cli/plan.js +2 -1
  61. package/dist/cli/plan.js.map +1 -1
  62. package/dist/cli/retrieval.d.ts.map +1 -1
  63. package/dist/cli/retrieval.js +5 -4
  64. package/dist/cli/retrieval.js.map +1 -1
  65. package/dist/cli/sdk.js +4 -4
  66. package/dist/cli/sdk.js.map +1 -1
  67. package/dist/cli/trace.d.ts.map +1 -1
  68. package/dist/cli/trace.js +2 -1
  69. package/dist/cli/trace.js.map +1 -1
  70. package/dist/cli/workflow.js +2 -2
  71. package/dist/cli/workflow.js.map +1 -1
  72. package/dist/config/process-env.d.ts +136 -0
  73. package/dist/config/process-env.d.ts.map +1 -0
  74. package/dist/config/process-env.js +217 -0
  75. package/dist/config/process-env.js.map +1 -0
  76. package/dist/config/types.d.ts +44 -0
  77. package/dist/config/types.d.ts.map +1 -1
  78. package/dist/findings/verdicts.d.ts +241 -0
  79. package/dist/findings/verdicts.d.ts.map +1 -0
  80. package/dist/findings/verdicts.js +284 -0
  81. package/dist/findings/verdicts.js.map +1 -0
  82. package/dist/gateway/adapters.d.ts +65 -0
  83. package/dist/gateway/adapters.d.ts.map +1 -1
  84. package/dist/gateway/adapters.js +216 -10
  85. package/dist/gateway/adapters.js.map +1 -1
  86. package/dist/gateway/channel-directory.d.ts +31 -0
  87. package/dist/gateway/channel-directory.d.ts.map +1 -1
  88. package/dist/gateway/channel-directory.js +40 -0
  89. package/dist/gateway/channel-directory.js.map +1 -1
  90. package/dist/gateway/gateway-log.d.ts +1 -1
  91. package/dist/gateway/gateway-log.d.ts.map +1 -1
  92. package/dist/gateway/gateway-log.js.map +1 -1
  93. package/dist/gateway/hooks.d.ts +87 -19
  94. package/dist/gateway/hooks.d.ts.map +1 -1
  95. package/dist/gateway/hooks.js +62 -23
  96. package/dist/gateway/hooks.js.map +1 -1
  97. package/dist/gateway/inbound-media.d.ts +147 -0
  98. package/dist/gateway/inbound-media.d.ts.map +1 -0
  99. package/dist/gateway/inbound-media.js +317 -0
  100. package/dist/gateway/inbound-media.js.map +1 -0
  101. package/dist/gateway/inbox.d.ts +8 -1
  102. package/dist/gateway/inbox.d.ts.map +1 -1
  103. package/dist/gateway/inbox.js.map +1 -1
  104. package/dist/gateway/platform-config.d.ts +14 -0
  105. package/dist/gateway/platform-config.d.ts.map +1 -1
  106. package/dist/gateway/platform-config.js +26 -8
  107. package/dist/gateway/platform-config.js.map +1 -1
  108. package/dist/gateway/realtime.d.ts +114 -0
  109. package/dist/gateway/realtime.d.ts.map +1 -0
  110. package/dist/gateway/realtime.js +402 -0
  111. package/dist/gateway/realtime.js.map +1 -0
  112. package/dist/gateway/registry.d.ts +31 -0
  113. package/dist/gateway/registry.d.ts.map +1 -1
  114. package/dist/gateway/registry.js +224 -4
  115. package/dist/gateway/registry.js.map +1 -1
  116. package/dist/gateway/whatsapp/baileys-bridge.d.ts +11 -1
  117. package/dist/gateway/whatsapp/baileys-bridge.d.ts.map +1 -1
  118. package/dist/gateway/whatsapp/baileys-bridge.js +123 -4
  119. package/dist/gateway/whatsapp/baileys-bridge.js.map +1 -1
  120. package/dist/gateway/whatsapp/bridge.d.ts +6 -2
  121. package/dist/gateway/whatsapp/bridge.d.ts.map +1 -1
  122. package/dist/gateway/whatsapp/bridge.js.map +1 -1
  123. package/dist/index.js +8 -0
  124. package/dist/index.js.map +1 -1
  125. package/dist/inference/factory.d.ts +14 -0
  126. package/dist/inference/factory.d.ts.map +1 -1
  127. package/dist/inference/factory.js +17 -0
  128. package/dist/inference/factory.js.map +1 -1
  129. package/dist/inference/groq-adapter.d.ts +2 -0
  130. package/dist/inference/groq-adapter.d.ts.map +1 -1
  131. package/dist/inference/groq-adapter.js +16 -6
  132. package/dist/inference/groq-adapter.js.map +1 -1
  133. package/dist/inference/tools.d.ts.map +1 -1
  134. package/dist/inference/tools.js +29 -0
  135. package/dist/inference/tools.js.map +1 -1
  136. package/dist/learning/benchmark.d.ts.map +1 -1
  137. package/dist/learning/benchmark.js +3 -2
  138. package/dist/learning/benchmark.js.map +1 -1
  139. package/dist/learning/continuation.d.ts.map +1 -1
  140. package/dist/learning/continuation.js +2 -1
  141. package/dist/learning/continuation.js.map +1 -1
  142. package/dist/learning/cost-tracker.d.ts.map +1 -1
  143. package/dist/learning/cost-tracker.js +2 -1
  144. package/dist/learning/cost-tracker.js.map +1 -1
  145. package/dist/learning/deferred-task.d.ts.map +1 -1
  146. package/dist/learning/deferred-task.js +13 -4
  147. package/dist/learning/deferred-task.js.map +1 -1
  148. package/dist/learning/eval-framework.d.ts.map +1 -1
  149. package/dist/learning/eval-framework.js +2 -1
  150. package/dist/learning/eval-framework.js.map +1 -1
  151. package/dist/learning/long-form.d.ts.map +1 -1
  152. package/dist/learning/long-form.js +2 -1
  153. package/dist/learning/long-form.js.map +1 -1
  154. package/dist/learning/model-reachability.d.ts +95 -0
  155. package/dist/learning/model-reachability.d.ts.map +1 -0
  156. package/dist/learning/model-reachability.js +111 -0
  157. package/dist/learning/model-reachability.js.map +1 -0
  158. package/dist/learning/model-registry.d.ts +22 -0
  159. package/dist/learning/model-registry.d.ts.map +1 -1
  160. package/dist/learning/model-registry.js +26 -1
  161. package/dist/learning/model-registry.js.map +1 -1
  162. package/dist/learning/model-verify-job.d.ts +131 -0
  163. package/dist/learning/model-verify-job.d.ts.map +1 -0
  164. package/dist/learning/model-verify-job.js +221 -0
  165. package/dist/learning/model-verify-job.js.map +1 -0
  166. package/dist/learning/reasoning-cache.d.ts.map +1 -1
  167. package/dist/learning/reasoning-cache.js +2 -1
  168. package/dist/learning/reasoning-cache.js.map +1 -1
  169. package/dist/learning/reasoning-trace.d.ts +37 -1
  170. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  171. package/dist/learning/reasoning-trace.js +66 -0
  172. package/dist/learning/reasoning-trace.js.map +1 -1
  173. package/dist/learning/resilient-call.d.ts.map +1 -1
  174. package/dist/learning/resilient-call.js +2 -1
  175. package/dist/learning/resilient-call.js.map +1 -1
  176. package/dist/learning/retrieval.d.ts.map +1 -1
  177. package/dist/learning/retrieval.js +2 -1
  178. package/dist/learning/retrieval.js.map +1 -1
  179. package/dist/learning/seeded-benchmark.d.ts +160 -0
  180. package/dist/learning/seeded-benchmark.d.ts.map +1 -0
  181. package/dist/learning/seeded-benchmark.js +321 -0
  182. package/dist/learning/seeded-benchmark.js.map +1 -0
  183. package/dist/learning/seeded-bugs.d.ts +142 -0
  184. package/dist/learning/seeded-bugs.d.ts.map +1 -0
  185. package/dist/learning/seeded-bugs.js +535 -0
  186. package/dist/learning/seeded-bugs.js.map +1 -0
  187. package/dist/learning/step-checkpoint.d.ts +127 -0
  188. package/dist/learning/step-checkpoint.d.ts.map +1 -0
  189. package/dist/learning/step-checkpoint.js +244 -0
  190. package/dist/learning/step-checkpoint.js.map +1 -0
  191. package/dist/nlu/intent-confirm.d.ts +23 -1
  192. package/dist/nlu/intent-confirm.d.ts.map +1 -1
  193. package/dist/nlu/intent-confirm.js +85 -2
  194. package/dist/nlu/intent-confirm.js.map +1 -1
  195. package/dist/nlu/learnings.d.ts +7 -0
  196. package/dist/nlu/learnings.d.ts.map +1 -1
  197. package/dist/nlu/learnings.js +7 -0
  198. package/dist/nlu/learnings.js.map +1 -1
  199. package/dist/observability/debug-log.d.ts +250 -0
  200. package/dist/observability/debug-log.d.ts.map +1 -0
  201. package/dist/observability/debug-log.js +500 -0
  202. package/dist/observability/debug-log.js.map +1 -0
  203. package/dist/observability/event-bus.d.ts.map +1 -1
  204. package/dist/observability/event-bus.js +4 -1
  205. package/dist/observability/event-bus.js.map +1 -1
  206. package/dist/observability/otel.d.ts +278 -0
  207. package/dist/observability/otel.d.ts.map +1 -0
  208. package/dist/observability/otel.js +590 -0
  209. package/dist/observability/otel.js.map +1 -0
  210. package/dist/parity/drivers.d.ts +99 -0
  211. package/dist/parity/drivers.d.ts.map +1 -0
  212. package/dist/parity/drivers.js +1362 -0
  213. package/dist/parity/drivers.js.map +1 -0
  214. package/dist/parity/graph.d.ts +73 -0
  215. package/dist/parity/graph.d.ts.map +1 -0
  216. package/dist/parity/graph.js +162 -0
  217. package/dist/parity/graph.js.map +1 -0
  218. package/dist/parity/matrix.d.ts +105 -0
  219. package/dist/parity/matrix.d.ts.map +1 -0
  220. package/dist/parity/matrix.js +352 -0
  221. package/dist/parity/matrix.js.map +1 -0
  222. package/dist/parity/observation.d.ts +444 -0
  223. package/dist/parity/observation.d.ts.map +1 -0
  224. package/dist/parity/observation.js +333 -0
  225. package/dist/parity/observation.js.map +1 -0
  226. package/dist/parity/scenarios.d.ts +229 -0
  227. package/dist/parity/scenarios.d.ts.map +1 -0
  228. package/dist/parity/scenarios.js +175 -0
  229. package/dist/parity/scenarios.js.map +1 -0
  230. package/dist/parity/surfaces.d.ts +122 -0
  231. package/dist/parity/surfaces.d.ts.map +1 -0
  232. package/dist/parity/surfaces.js +190 -0
  233. package/dist/parity/surfaces.js.map +1 -0
  234. package/dist/runtime/fault-injection.d.ts +173 -0
  235. package/dist/runtime/fault-injection.d.ts.map +1 -0
  236. package/dist/runtime/fault-injection.js +281 -0
  237. package/dist/runtime/fault-injection.js.map +1 -0
  238. package/dist/tools/child-agent-entry.d.ts +23 -0
  239. package/dist/tools/child-agent-entry.d.ts.map +1 -0
  240. package/dist/tools/child-agent-entry.js +129 -0
  241. package/dist/tools/child-agent-entry.js.map +1 -0
  242. package/dist/tools/child-agent-runtime.d.ts +124 -0
  243. package/dist/tools/child-agent-runtime.d.ts.map +1 -0
  244. package/dist/tools/child-agent-runtime.js +704 -0
  245. package/dist/tools/child-agent-runtime.js.map +1 -0
  246. package/dist/tools/coding-tools.d.ts.map +1 -1
  247. package/dist/tools/coding-tools.js +82 -10
  248. package/dist/tools/coding-tools.js.map +1 -1
  249. package/dist/tools/delegation-system.d.ts +31 -0
  250. package/dist/tools/delegation-system.d.ts.map +1 -1
  251. package/dist/tools/delegation-system.js +70 -9
  252. package/dist/tools/delegation-system.js.map +1 -1
  253. package/dist/tools/extract/docx.d.ts +27 -0
  254. package/dist/tools/extract/docx.d.ts.map +1 -0
  255. package/dist/tools/extract/docx.js +48 -0
  256. package/dist/tools/extract/docx.js.map +1 -0
  257. package/dist/tools/extract/html-text.d.ts +27 -0
  258. package/dist/tools/extract/html-text.d.ts.map +1 -0
  259. package/dist/tools/extract/html-text.js +86 -0
  260. package/dist/tools/extract/html-text.js.map +1 -0
  261. package/dist/tools/extract/pdf-ocr.d.ts +58 -0
  262. package/dist/tools/extract/pdf-ocr.d.ts.map +1 -0
  263. package/dist/tools/extract/pdf-ocr.js +116 -0
  264. package/dist/tools/extract/pdf-ocr.js.map +1 -0
  265. package/dist/tools/extract/pdf.d.ts +65 -0
  266. package/dist/tools/extract/pdf.d.ts.map +1 -0
  267. package/dist/tools/extract/pdf.js +197 -0
  268. package/dist/tools/extract/pdf.js.map +1 -0
  269. package/dist/tools/extract/pptx.d.ts +32 -0
  270. package/dist/tools/extract/pptx.d.ts.map +1 -0
  271. package/dist/tools/extract/pptx.js +77 -0
  272. package/dist/tools/extract/pptx.js.map +1 -0
  273. package/dist/tools/extract/xlsx.d.ts +47 -0
  274. package/dist/tools/extract/xlsx.d.ts.map +1 -0
  275. package/dist/tools/extract/xlsx.js +111 -0
  276. package/dist/tools/extract/xlsx.js.map +1 -0
  277. package/dist/tools/finding-tool.d.ts +76 -0
  278. package/dist/tools/finding-tool.d.ts.map +1 -0
  279. package/dist/tools/finding-tool.js +125 -0
  280. package/dist/tools/finding-tool.js.map +1 -0
  281. package/dist/tools/messaging-tools.d.ts +41 -11
  282. package/dist/tools/messaging-tools.d.ts.map +1 -1
  283. package/dist/tools/messaging-tools.js +104 -55
  284. package/dist/tools/messaging-tools.js.map +1 -1
  285. package/dist/tools/neutts-synth.d.ts +27 -3
  286. package/dist/tools/neutts-synth.d.ts.map +1 -1
  287. package/dist/tools/neutts-synth.js +57 -13
  288. package/dist/tools/neutts-synth.js.map +1 -1
  289. package/dist/tools/pipeline-tool.d.ts +43 -1
  290. package/dist/tools/pipeline-tool.d.ts.map +1 -1
  291. package/dist/tools/pipeline-tool.js +13 -2
  292. package/dist/tools/pipeline-tool.js.map +1 -1
  293. package/dist/tools/read-extract.d.ts +116 -45
  294. package/dist/tools/read-extract.d.ts.map +1 -1
  295. package/dist/tools/read-extract.js +494 -158
  296. package/dist/tools/read-extract.js.map +1 -1
  297. package/dist/tools/registry.d.ts +2 -2
  298. package/dist/tools/registry.d.ts.map +1 -1
  299. package/dist/tools/registry.js +152 -23
  300. package/dist/tools/registry.js.map +1 -1
  301. package/dist/tools/subagent-refusal.d.ts +15 -0
  302. package/dist/tools/subagent-refusal.d.ts.map +1 -0
  303. package/dist/tools/subagent-refusal.js +18 -0
  304. package/dist/tools/subagent-refusal.js.map +1 -0
  305. package/dist/tools/subagent-spawner.d.ts +122 -0
  306. package/dist/tools/subagent-spawner.d.ts.map +1 -1
  307. package/dist/tools/subagent-spawner.js +249 -28
  308. package/dist/tools/subagent-spawner.js.map +1 -1
  309. package/dist/tools/tool-hooks.d.ts +177 -0
  310. package/dist/tools/tool-hooks.d.ts.map +1 -0
  311. package/dist/tools/tool-hooks.js +427 -0
  312. package/dist/tools/tool-hooks.js.map +1 -0
  313. package/dist/tools/tool-loop.d.ts +82 -0
  314. package/dist/tools/tool-loop.d.ts.map +1 -1
  315. package/dist/tools/tool-loop.js +167 -9
  316. package/dist/tools/tool-loop.js.map +1 -1
  317. package/dist/tools/tool-refusal.d.ts +68 -0
  318. package/dist/tools/tool-refusal.d.ts.map +1 -0
  319. package/dist/tools/tool-refusal.js +78 -0
  320. package/dist/tools/tool-refusal.js.map +1 -0
  321. package/dist/tools/toolsets.d.ts +8 -0
  322. package/dist/tools/toolsets.d.ts.map +1 -1
  323. package/dist/tools/toolsets.js +12 -2
  324. package/dist/tools/toolsets.js.map +1 -1
  325. package/dist/tools/vision-tools.d.ts +88 -83
  326. package/dist/tools/vision-tools.d.ts.map +1 -1
  327. package/dist/tools/vision-tools.js +134 -103
  328. package/dist/tools/vision-tools.js.map +1 -1
  329. package/dist/tools/worktree.d.ts +210 -0
  330. package/dist/tools/worktree.d.ts.map +1 -0
  331. package/dist/tools/worktree.js +374 -0
  332. package/dist/tools/worktree.js.map +1 -0
  333. package/dist/utils/format.d.ts +3 -0
  334. package/dist/utils/format.d.ts.map +1 -0
  335. package/dist/utils/format.js +32 -0
  336. package/dist/utils/format.js.map +1 -0
  337. package/dist/web-dashboard/attachment-extract.d.ts +64 -0
  338. package/dist/web-dashboard/attachment-extract.d.ts.map +1 -0
  339. package/dist/web-dashboard/attachment-extract.js +154 -0
  340. package/dist/web-dashboard/attachment-extract.js.map +1 -0
  341. package/dist/web-dashboard/chat-console.d.ts +108 -1
  342. package/dist/web-dashboard/chat-console.d.ts.map +1 -1
  343. package/dist/web-dashboard/chat-console.js +36 -0
  344. package/dist/web-dashboard/chat-console.js.map +1 -1
  345. package/dist/web-dashboard/hub-data.d.ts +37 -0
  346. package/dist/web-dashboard/hub-data.d.ts.map +1 -1
  347. package/dist/web-dashboard/hub-data.js +61 -1
  348. package/dist/web-dashboard/hub-data.js.map +1 -1
  349. package/dist/web-dashboard/process-env-inventory.d.ts +57 -0
  350. package/dist/web-dashboard/process-env-inventory.d.ts.map +1 -0
  351. package/dist/web-dashboard/process-env-inventory.js +96 -0
  352. package/dist/web-dashboard/process-env-inventory.js.map +1 -0
  353. package/dist/web-dashboard/server.d.ts +14 -0
  354. package/dist/web-dashboard/server.d.ts.map +1 -1
  355. package/dist/web-dashboard/server.js +493 -30
  356. package/dist/web-dashboard/server.js.map +1 -1
  357. package/dist/web-dashboard/src/types.d.ts +261 -48
  358. package/dist/web-dashboard/src/types.d.ts.map +1 -1
  359. package/package.json +18 -3
  360. package/src/web-dashboard/public/assets/{index-Cyd6tIew.css → index-CIJ6FHHZ.css} +1 -1
  361. package/src/web-dashboard/public/assets/index-CsySzR41.js +207 -0
  362. package/src/web-dashboard/public/assets/index-CsySzR41.js.map +1 -0
  363. package/src/web-dashboard/public/index.html +2 -2
  364. package/dist/tools/child-agent-worker.js +0 -212
  365. package/src/web-dashboard/public/assets/index-CxDj7p6i.js +0 -207
  366. package/src/web-dashboard/public/assets/index-CxDj7p6i.js.map +0 -1
@@ -17,6 +17,8 @@ import { fileURLToPath } from 'node:url';
17
17
  import { homedir } from 'node:os';
18
18
  import { parseRbacUsers } from '../enterprise/rbac.js';
19
19
  import { registerDagHandlers } from '../observability/dag-bridge.js';
20
+ import { debugLogDir, debugLoggingEnabled, debugLogsForSession } from '../observability/debug-log.js';
21
+ import { scrubSecrets } from '../gateway/gateway-log.js';
20
22
  import { resolveBuffConfigDir, resolveBuffConfigPath, resolveNuviraHome } from '../config/paths.js';
21
23
  import { loadEnv } from '../utils/env.js';
22
24
  import { ConfigManager } from '../config/manager.js';
@@ -41,6 +43,9 @@ import { clearModelListCache } from '../inference/model-validator.js';
41
43
  import { probeProviderList } from '../inference/model-probe.js';
42
44
  import { CATALOG_PROVIDER_IDS, getCatalogProvider } from '../inference/provider-catalog.js';
43
45
  import { readHubData } from './hub-data.js';
46
+ import { isCuratedProcessEnvVar, normalizeProcessEnvValue, } from '../config/process-env.js';
47
+ import { FRESH_DAYS, FRESHNESS_COPY, REACHABILITY_COPY, classifyFreshness, classifyReachability, } from '../learning/model-reachability.js';
48
+ import { VERIFY_BACKLOG_DEFAULT_PER_RUN, VERIFY_BACKLOG_MAX_PER_RUN, getVerifyBacklogState, startVerifyBacklogRun, } from '../learning/model-verify-job.js';
44
49
  import { setToolsetEnabled } from '../tools/toolsets.js';
45
50
  import { setSkillEnabled } from '../learning/hub-skill-catalog.js';
46
51
  // I11 — dashboard channel send-test: the same registry/adapters `nuvira gateway
@@ -51,7 +56,7 @@ import { setSkillEnabled } from '../learning/hub-skill-catalog.js';
51
56
  // mock node:fs before importing the server (router's package.json read would
52
57
  // hit the mocked readFileSync).
53
58
  import { createConfiguredAdapters } from '../gateway/adapters.js';
54
- import { ChannelDirectory, PLATFORM_ENV_VARS } from '../gateway/channel-directory.js';
59
+ import { ChannelDirectory, PLATFORM_ENV_VARS, platformConfigVars } from '../gateway/channel-directory.js';
55
60
  import { applyEnvToProcess, configurablePlatforms, platformConfigStatus, platformEnvVarMeta, redactValue, writeEnvFile, } from '../gateway/platform-config.js';
56
61
  // ─── Constants ──────────────────────────────────────────────────────────────
57
62
  const PORT = parseInt(envBuff('DASHBOARD_PORT') || '3030', 10);
@@ -469,6 +474,88 @@ function generateZip(files) {
469
474
  parts.push(new Uint8Array(eocd));
470
475
  return Buffer.concat(parts.map((p) => Buffer.from(p.buffer, p.byteOffset, p.byteLength)));
471
476
  }
477
+ // ─── WS2 (#24) — the attachable support bundle ──────────────────────────────
478
+ /** Cap on the transcript the bundle carries, so one long chat cannot make a 50 MB zip. */
479
+ const MAX_SUPPORT_TRANSCRIPT_CHARS = 400_000;
480
+ /**
481
+ * The front page of a support bundle: what is inside, and — just as important —
482
+ * what is deliberately NOT.
483
+ *
484
+ * Load-bearing rather than decorative. A bundle exists to be pasted into a bug
485
+ * report, often a public one, so it has to say plainly that credentials are
486
+ * redacted and that the logs carry previews instead of payloads. A user who
487
+ * cannot tell what they are about to attach either does not attach it or
488
+ * attaches it and regrets it, and both of those make the artifact useless.
489
+ */
490
+ function renderSupportManifest(opts) {
491
+ const named = (values) => [
492
+ ...new Set(values.filter((v) => typeof v === 'string' && v.length > 0)),
493
+ ];
494
+ const surfaces = named(opts.logs.map((l) => l.header.surface));
495
+ const versions = named(opts.logs.map((l) => l.header.version));
496
+ const backends = [
497
+ ...new Set(opts.logs
498
+ .map((l) => [l.header.provider, l.header.model, l.header.transport].filter(Boolean).join(' / '))
499
+ .filter((b) => b.length > 0)),
500
+ ];
501
+ return [
502
+ 'nuvira support bundle',
503
+ `generated: ${opts.generatedAt.toISOString()}`,
504
+ `session: ${opts.sessionId}`,
505
+ `surface(s): ${surfaces.join(', ') || 'unknown'}`,
506
+ `version(s): ${versions.join(', ') || 'unknown'}`,
507
+ `debug logs: ${opts.logs.length} (oldest first)`,
508
+ ...(backends.length > 0 ? [`backend(s) seen: ${backends.join(' | ')}`] : []),
509
+ ...(opts.turns !== null ? [`conversation turns: ${opts.turns}`] : []),
510
+ '',
511
+ 'contents:',
512
+ ...opts.files.map((f) => ` - ${f}`),
513
+ '',
514
+ 'notes:',
515
+ ' - credentials are redacted, with the same scrubber the gateway log uses',
516
+ ' - a session debug log carries bounded previews and event lines, not full payloads',
517
+ ' - each log names the backend that ACTUALLY served its turn, after any failover',
518
+ ...(opts.turns !== null
519
+ ? [' - conversation.txt is your own chat with the agent, included so the reading is unambiguous']
520
+ : []),
521
+ '',
522
+ ].join('\n');
523
+ }
524
+ /**
525
+ * The conversation as text — the other half of a bug report, and the reason the
526
+ * bundle is worth more than the log alone ("it did not answer" needs the ask).
527
+ *
528
+ * Redacted with the same `scrubSecrets` for the same reason as the logs: a user
529
+ * pasting this into a public issue must not leak a key they happened to type
530
+ * into a prompt three turns earlier. Bounded by characters, and it SAYS when it
531
+ * was cut, so a truncated bundle is never mistaken for the whole story.
532
+ */
533
+ function renderSupportTranscript(sessionId, record, generatedAt) {
534
+ const lines = [
535
+ `Conversation with the dashboard agent (session ${sessionId})`,
536
+ `Exported: ${generatedAt.toISOString()}`,
537
+ `Turns: ${record.turns.length}`,
538
+ '─'.repeat(60),
539
+ '',
540
+ ];
541
+ let used = 0;
542
+ let truncated = false;
543
+ for (const turn of record.turns) {
544
+ const body = scrubSecrets(turn.content);
545
+ if (used + body.length > MAX_SUPPORT_TRANSCRIPT_CHARS) {
546
+ truncated = true;
547
+ break;
548
+ }
549
+ used += body.length;
550
+ lines.push(turn.role === 'user' ? 'You:' : 'Agent:');
551
+ lines.push(body);
552
+ lines.push('');
553
+ }
554
+ if (truncated) {
555
+ lines.push(`[truncated at ${MAX_SUPPORT_TRANSCRIPT_CHARS} characters — the full transcript is in the session store]`);
556
+ }
557
+ return lines.join('\n');
558
+ }
472
559
  let activePipeline = null; // goal/description of current pipeline
473
560
  let activeNodes = [];
474
561
  let activeEdges = [];
@@ -2682,6 +2769,32 @@ function writeJson(res, status, data) {
2682
2769
  res.writeHead(status, { 'Content-Type': 'application/json' });
2683
2770
  res.end(JSON.stringify(data));
2684
2771
  }
2772
+ /**
2773
+ * Turn a refused process-environment value into something a user can act on.
2774
+ *
2775
+ * UI copy on purpose: the validator sends a stable machine reason, and the
2776
+ * message is composed here. The two that matter most are the ones a user would
2777
+ * otherwise read as success — a name off the allowlist (the endpoint is curated,
2778
+ * and saying so is friendlier than a bare 400) and a value that is not a flag
2779
+ * (`NUVIRA_STRICT_MODEL=true` reads as OFF, so accepting it would store a
2780
+ * setting that does not do what the page says it does).
2781
+ */
2782
+ function describeProcessEnvRefusal(name, reason) {
2783
+ switch (reason) {
2784
+ case 'unknown-name':
2785
+ return `${name || 'That name'} is not one of the curated process switches. Skill secrets live on the Environment page, and provider credentials in provider setup.`;
2786
+ case 'empty':
2787
+ return `${name} cannot be set to an empty value — use Unset to remove it from the env file.`;
2788
+ case 'multiline':
2789
+ return `${name} was not saved: a value may not contain a newline, or it would add a second variable to the env file.`;
2790
+ case 'too-long':
2791
+ return `${name} was not saved: the value is too long (limit 512 characters).`;
2792
+ case 'not-a-flag':
2793
+ return `${name} was not saved: it is a switch, so its value must be on (1, true, yes, on) or off (0, false, off, no).`;
2794
+ default:
2795
+ return `${name} was not saved.`;
2796
+ }
2797
+ }
2685
2798
  // ─── Request Handler ────────────────────────────────────────────────────────
2686
2799
  function handleRequest(req, res) {
2687
2800
  const url = new URL(req.url || '/', `http://${req.headers.host || HOST}`);
@@ -3447,6 +3560,140 @@ function handleRequest(req, res) {
3447
3560
  })();
3448
3561
  return;
3449
3562
  }
3563
+ // ── Process environment — the curated switches, not a general env editor ─
3564
+ //
3565
+ // `/env` edits SKILL secrets: any well-formed NAME a skill might declare. The
3566
+ // switches that change how a RUN behaves (isolation, resume, the debug log,
3567
+ // OTLP export, tool hooks) are a different thing, and the useful thing to
3568
+ // expose is not "write a variable" but "here is what this switch is currently
3569
+ // set to, what unset means, and whether something outranks it".
3570
+ //
3571
+ // The allowlist is enforced HERE rather than in the UI, so a hand-made request
3572
+ // cannot use this endpoint to append an arbitrary name to the credential
3573
+ // `.env` — the page is curated because the server is. Reads need a session;
3574
+ // writing needs routing.operate (admin or operator), matching every other
3575
+ // route that mutates persisted configuration.
3576
+ if (pathname === '/api/process-env' && req.method === 'GET') {
3577
+ void (async () => {
3578
+ const session = adminSessions.validate(bearerToken(req));
3579
+ if (!session) {
3580
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
3581
+ return;
3582
+ }
3583
+ try {
3584
+ const { readProcessEnvInventory } = await import('./process-env-inventory.js');
3585
+ writeJson(res, 200, { ok: true, vars: readProcessEnvInventory() });
3586
+ }
3587
+ catch (err) {
3588
+ writeJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
3589
+ }
3590
+ })();
3591
+ return;
3592
+ }
3593
+ if (pathname === '/api/process-env' && req.method === 'POST') {
3594
+ void (async () => {
3595
+ const session = adminSessions.validate(bearerToken(req));
3596
+ if (!session) {
3597
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
3598
+ return;
3599
+ }
3600
+ if (!roleCan(session.role, 'routing.operate')) {
3601
+ writeJson(res, 403, {
3602
+ ok: false,
3603
+ error: `Access denied — role '${session.role}' cannot change the process environment (requires admin or operator).`,
3604
+ });
3605
+ return;
3606
+ }
3607
+ const body = await readJsonBody(req);
3608
+ const name = typeof body?.name === 'string' ? body.name.trim() : '';
3609
+ const rawValue = typeof body?.value === 'string' ? body.value : '';
3610
+ const normalized = normalizeProcessEnvValue(name, rawValue);
3611
+ if (!normalized.ok) {
3612
+ writeJson(res, 400, { ok: false, error: describeProcessEnvRefusal(name, normalized.reason), reason: normalized.reason });
3613
+ return;
3614
+ }
3615
+ try {
3616
+ const { saveEnvValue } = await import('../skills/secret-capture.js');
3617
+ const written = saveEnvValue(name, normalized.value);
3618
+ if (!written.success) {
3619
+ writeJson(res, 400, { ok: false, error: `${name} could not be written to the env file (${written.reason ?? 'write-failed'}).` });
3620
+ return;
3621
+ }
3622
+ // Apply to THIS process, so the dashboard's own turns obey the switch
3623
+ // without a restart. It is also why the row stops reading as shadowed:
3624
+ // the process now holds the value that was just written to the file.
3625
+ try {
3626
+ const { applyEnvToProcess } = await import('../gateway/platform-config.js');
3627
+ applyEnvToProcess({ [name]: normalized.value });
3628
+ }
3629
+ catch {
3630
+ /* best-effort */
3631
+ }
3632
+ const { readProcessEnvInventory } = await import('./process-env-inventory.js');
3633
+ writeJson(res, 200, {
3634
+ ok: true,
3635
+ saved: { name, value: normalized.value },
3636
+ row: readProcessEnvInventory().find((r) => r.name === name),
3637
+ });
3638
+ }
3639
+ catch (err) {
3640
+ writeJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
3641
+ }
3642
+ })();
3643
+ return;
3644
+ }
3645
+ if (pathname === '/api/process-env/delete' && req.method === 'POST') {
3646
+ void (async () => {
3647
+ const session = adminSessions.validate(bearerToken(req));
3648
+ if (!session) {
3649
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
3650
+ return;
3651
+ }
3652
+ if (!roleCan(session.role, 'routing.operate')) {
3653
+ writeJson(res, 403, {
3654
+ ok: false,
3655
+ error: `Access denied — role '${session.role}' cannot change the process environment (requires admin or operator).`,
3656
+ });
3657
+ return;
3658
+ }
3659
+ const body = await readJsonBody(req);
3660
+ const name = typeof body?.name === 'string' ? body.name.trim() : '';
3661
+ // The same allowlist the write path uses: deleting an arbitrary name from
3662
+ // the credential file would be a write wearing a different verb.
3663
+ if (!isCuratedProcessEnvVar(name)) {
3664
+ writeJson(res, 400, { ok: false, error: describeProcessEnvRefusal(name, 'unknown-name'), reason: 'unknown-name' });
3665
+ return;
3666
+ }
3667
+ try {
3668
+ const { deleteEnvValue } = await import('../skills/secret-capture.js');
3669
+ const result = deleteEnvValue(name);
3670
+ if (!result.success) {
3671
+ writeJson(res, 400, { ok: false, error: result.reason ?? 'delete failed' });
3672
+ return;
3673
+ }
3674
+ // Drop it from this process too. "Unset" has to mean unset for the run
3675
+ // that is about to happen, or the row would read as unset while the
3676
+ // dashboard kept obeying the value it just removed.
3677
+ try {
3678
+ const { applyEnvToProcess } = await import('../gateway/platform-config.js');
3679
+ applyEnvToProcess({}, [name]);
3680
+ }
3681
+ catch {
3682
+ /* best-effort */
3683
+ }
3684
+ const { readProcessEnvInventory } = await import('./process-env-inventory.js');
3685
+ writeJson(res, 200, {
3686
+ ok: true,
3687
+ removed: result.removed,
3688
+ row: readProcessEnvInventory().find((r) => r.name === name),
3689
+ });
3690
+ }
3691
+ catch (err) {
3692
+ writeJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
3693
+ }
3694
+ })();
3695
+ return;
3696
+ }
3450
3697
  // ── Agent Hub channel send-test (I11) — admin-gated gateway send ────────
3451
3698
  // Mirrors `nuvira gateway send <target> <text>`: resolve the target (alias or
3452
3699
  // platform:channelId) through the SAME ChannelDirectory, then send through
@@ -4234,25 +4481,34 @@ function handleRequest(req, res) {
4234
4481
  const registryData = readJSON(join(MEMORY_DIR, 'model-registry.json'));
4235
4482
  const now = Date.now();
4236
4483
  const entries = Object.values(registryData?.entries ?? {});
4237
- const STALE_DAYS = 7;
4238
- const REMOVED_DAYS = 30;
4239
- let freshCount = 0;
4240
- let staleCount = 0;
4241
- let removedCount = 0;
4484
+ // Two classifications per entry, deliberately separate — see
4485
+ // `model-reachability.ts` for why: "is the provider still listing it"
4486
+ // (probe age) and "would the router use it" (verification) are different
4487
+ // questions, and this endpoint previously answered both with one rule
4488
+ // while the panel's badges answered with another.
4489
+ const counts = {
4490
+ routable: 0,
4491
+ parked: 0,
4492
+ 'proof-expired': 0,
4493
+ 'proven-dead': 0,
4494
+ 'never-verified': 0,
4495
+ };
4496
+ const freshnessCounts = {
4497
+ fresh: 0,
4498
+ stale: 0,
4499
+ 'likely-removed': 0,
4500
+ };
4242
4501
  const timelineEntries = entries.map((e) => {
4243
- const daysSinceProbe = e.lastProbedAt ? (now - e.lastProbedAt) / (24 * 60 * 60 * 1000) : Infinity;
4244
- const isStale = daysSinceProbe > STALE_DAYS;
4245
- const isRemoved = daysSinceProbe > REMOVED_DAYS && e.errorRate > 0.5;
4246
- if (isRemoved)
4247
- removedCount++;
4248
- else if (isStale)
4249
- staleCount++;
4250
- else
4251
- freshCount++;
4502
+ const reachability = classifyReachability(e, now);
4503
+ const freshness = classifyFreshness(e.lastProbedAt, e.errorRate, now);
4504
+ counts[reachability] += 1;
4505
+ freshnessCounts[freshness] += 1;
4252
4506
  return {
4253
4507
  provider: e.provider,
4254
4508
  model: e.model,
4255
4509
  status: e.status,
4510
+ reachability,
4511
+ freshness,
4256
4512
  lastVerifiedAt: e.lastVerifiedAt || 0,
4257
4513
  lastProbedAt: e.lastProbedAt || 0,
4258
4514
  lastUsedAt: e.lastUsedAt || 0,
@@ -4260,6 +4516,10 @@ function handleRequest(req, res) {
4260
4516
  latencyMs: e.latencyMs,
4261
4517
  contextWindowTokens: e.contextWindowTokens,
4262
4518
  firstSeenAt: e.firstSeenAt || e.lastProbedAt || 0,
4519
+ /** How stale this model's PROOF is — the number `isUsable()` gates on. */
4520
+ daysSinceVerify: e.lastVerifiedAt
4521
+ ? (now - e.lastVerifiedAt) / (24 * 60 * 60 * 1000)
4522
+ : null,
4263
4523
  };
4264
4524
  });
4265
4525
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -4267,9 +4527,15 @@ function handleRequest(req, res) {
4267
4527
  entries: timelineEntries,
4268
4528
  lastUpdated: now,
4269
4529
  totalModels: entries.length,
4270
- freshCount,
4271
- staleCount,
4272
- removedCount,
4530
+ counts,
4531
+ freshnessCounts,
4532
+ // The wording travels WITH the data, so the panel cannot drift from the
4533
+ // classifier that produced the state. `reachability-copy` is one small
4534
+ // map, not a per-row string, and it keeps the root learning modules out
4535
+ // of the browser bundle (they are server-side only).
4536
+ reachabilityCopy: REACHABILITY_COPY,
4537
+ freshnessCopy: FRESHNESS_COPY,
4538
+ freshDays: FRESH_DAYS,
4273
4539
  }));
4274
4540
  }
4275
4541
  catch (err) {
@@ -4278,6 +4544,53 @@ function handleRequest(req, res) {
4278
4544
  }
4279
4545
  return;
4280
4546
  }
4547
+ // ── Timeline action: "verify next N" the never-verified backlog ──────────
4548
+ // READ is open, like `/api/model-timeline` above: it carries model ids, counts
4549
+ // and prose, no credentials, and it renders on a page that is already readable
4550
+ // without a session. The WRITE below is where the gate belongs — a spot-check
4551
+ // is a real generation against the user's provider quota.
4552
+ if (pathname === '/api/models/verify-next' && req.method === 'GET') {
4553
+ writeJson(res, 200, {
4554
+ ...getVerifyBacklogState(),
4555
+ defaultCount: VERIFY_BACKLOG_DEFAULT_PER_RUN,
4556
+ maxCount: VERIFY_BACKLOG_MAX_PER_RUN,
4557
+ });
4558
+ return;
4559
+ }
4560
+ // POST /api/models/verify-next — start probing the next N never-verified ids.
4561
+ // Returns as soon as the run is PLANNED; the probes continue in the background
4562
+ // and the panel watches them through the GET above. A run that is refused
4563
+ // (already running, or an empty backlog) is reported with `ok: false` plus the
4564
+ // reason, so "nothing left to do" never has to be inferred from a zero count.
4565
+ if (pathname === '/api/models/verify-next' && req.method === 'POST') {
4566
+ void (async () => {
4567
+ const session = adminSessions.validate(bearerToken(req));
4568
+ if (!session) {
4569
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
4570
+ return;
4571
+ }
4572
+ if (!roleCan(session.role, 'routing.operate')) {
4573
+ writeJson(res, 403, {
4574
+ ok: false,
4575
+ error: `Access denied — role '${session.role}' cannot spend provider quota on model verification.`,
4576
+ });
4577
+ return;
4578
+ }
4579
+ const body = await readJsonBody(req);
4580
+ const requested = typeof body?.count === 'number' ? body.count : VERIFY_BACKLOG_DEFAULT_PER_RUN;
4581
+ const result = startVerifyBacklogRun(new ConfigManager(), requested);
4582
+ writeJson(res, 200, {
4583
+ ok: result.started,
4584
+ started: result.started,
4585
+ error: result.error,
4586
+ refusal: result.state.refusal,
4587
+ state: result.state,
4588
+ defaultCount: VERIFY_BACKLOG_DEFAULT_PER_RUN,
4589
+ maxCount: VERIFY_BACKLOG_MAX_PER_RUN,
4590
+ });
4591
+ })();
4592
+ return;
4593
+ }
4281
4594
  if (pathname === '/api/requests') {
4282
4595
  res.writeHead(200, { 'Content-Type': 'application/json' });
4283
4596
  res.end(JSON.stringify(readRequestsData()));
@@ -4747,6 +5060,9 @@ function handleRequest(req, res) {
4747
5060
  varName: v.varName,
4748
5061
  set: v.set,
4749
5062
  value: canWrite ? v.value : v.set ? redactValue(v.value) : '',
5063
+ // `prompt` carries the "(optional)" marker for additive transport
5064
+ // modes (platformEnvVarMeta), so the form labels them without the UI
5065
+ // having to know which vars are required.
4750
5066
  prompt: m?.prompt ?? v.varName,
4751
5067
  secret: m?.secret ?? false,
4752
5068
  };
@@ -4780,7 +5096,10 @@ function handleRequest(req, res) {
4780
5096
  }
4781
5097
  const body = await readJsonBody(req);
4782
5098
  const values = (body?.values ?? {});
4783
- const allowed = new Set(PLATFORM_ENV_VARS[platform]);
5099
+ // platformConfigVars, not PLATFORM_ENV_VARS: the form renders every managed
5100
+ // var (including the additional transport modes), so a key it displays must
5101
+ // be writable here — otherwise a value the UI offers is rejected as unknown.
5102
+ const allowed = new Set(platformConfigVars(platform));
4784
5103
  const clean = {};
4785
5104
  for (const [key, value] of Object.entries(values)) {
4786
5105
  if (!allowed.has(key)) {
@@ -4819,7 +5138,7 @@ function handleRequest(req, res) {
4819
5138
  writeJson(res, 400, { ok: false, error: `Platform '${platform}' is not env-configurable.` });
4820
5139
  return;
4821
5140
  }
4822
- const keys = PLATFORM_ENV_VARS[platform];
5141
+ const keys = platformConfigVars(platform);
4823
5142
  writeEnvFile({}, keys);
4824
5143
  applyEnvToProcess({}, keys);
4825
5144
  writeJson(res, 200, { ok: true, removed: keys, status: platformConfigStatus(platform) });
@@ -5448,14 +5767,34 @@ function handleRequest(req, res) {
5448
5767
  : newChatSessionId();
5449
5768
  const provider = typeof body?.provider === 'string' ? body.provider : undefined;
5450
5769
  const model = typeof body?.model === 'string' ? body.model : undefined;
5451
- // P8 — turn attachments (file picker / paste-as-attachment / drag-drop).
5452
- // The GUI reads files client-side and sends the text inline; the server
5453
- // injects them into the SAME answerOnce context as [Attachment: name].
5454
- const rawAttachments = Array.isArray(body?.attachments) ? body.attachments : [];
5455
- const attachments = rawAttachments
5456
- .filter((a) => !!a && typeof a.name === 'string' && typeof a.content === 'string' && a.name.trim().length > 0)
5457
- .map((a) => ({ name: a.name.trim().slice(0, 120), content: a.content.slice(0, 300_000) }))
5458
- .slice(0, 10);
5770
+ // WS5 (#27) — per-turn isolation and resume, from the GUI.
5771
+ //
5772
+ // Read as a strict TRI-state, because the three cases are genuinely
5773
+ // different here: `true` asks for it, `false` declines a deployment-wide
5774
+ // `NUVIRA_ISOLATE`/`NUVIRA_RESUME` for this one turn, and ABSENT (`undefined`)
5775
+ // defers to that environment. Collapsing absent into `false` would make the
5776
+ // GUI's silence outrank the deployment's configuration — the same defect the
5777
+ // CLI's option defaults had.
5778
+ const worktree = body?.worktree === true ? true : body?.worktree === false ? false : undefined;
5779
+ const keepWorktree = body?.keepWorktree === true ? true : body?.keepWorktree === false ? false : undefined;
5780
+ const resume = typeof body?.resume === 'string'
5781
+ ? body.resume
5782
+ : body?.resume === true
5783
+ ? true
5784
+ : body?.resume === false
5785
+ ? false
5786
+ : undefined;
5787
+ // P8 + P2 — turn attachments (file picker / paste-as-attachment / drag-drop).
5788
+ //
5789
+ // The GUI sends TEXT inline. For anything it cannot decode as text (PDF, DOCX,
5790
+ // XLSX, PPTX, images) it sends the BYTES as base64 and the extraction happens
5791
+ // here, through the same `read_extract` the agent uses for a file in the project
5792
+ // folder — so a refusal reaches the turn as a refusal instead of as mojibake.
5793
+ // Previously every file was read with `File.text()` client-side, which is how a
5794
+ // PDF arrived as `%PDF-1.4 …` with nothing indicating extraction had failed
5795
+ // (TOOL_TRUTHFULNESS_TRACKER.md findings #1/#7 and P2).
5796
+ const { hydrateAttachments } = await import('./attachment-extract.js');
5797
+ const attachments = await hydrateAttachments(body?.attachments);
5459
5798
  // P3 — an attached project's bounded snapshot rides into the turn as
5460
5799
  // `[Project context]`, so "assess THIS project" works without the user
5461
5800
  // describing the codebase. Cache misses rebuild automatically.
@@ -5500,7 +5839,16 @@ function handleRequest(req, res) {
5500
5839
  // Mark the failover log BEFORE the turn so a failure can report which
5501
5840
  // models were actually tried (same contract as the gateway).
5502
5841
  const attemptMark = markFailoverAttempts();
5503
- const result = await chatConsole.answer(sessionId, message, { provider, model, projectContext, projectPath, attachments });
5842
+ const result = await chatConsole.answer(sessionId, message, {
5843
+ provider,
5844
+ model,
5845
+ projectContext,
5846
+ projectPath,
5847
+ attachments,
5848
+ ...(worktree === undefined ? {} : { worktree }),
5849
+ ...(keepWorktree === undefined ? {} : { keepWorktree }),
5850
+ ...(resume === undefined ? {} : { resume }),
5851
+ });
5504
5852
  // The response is written below — remove the disconnect listener so a
5505
5853
  // post-completion close can never touch the console again.
5506
5854
  res.off('close', onResClose);
@@ -5521,7 +5869,12 @@ function handleRequest(req, res) {
5521
5869
  // retrying on its own — a manual re-send on top of the queued one would
5522
5870
  // run the same ask twice.
5523
5871
  let retryQueued = false;
5524
- if (result.generationFailed === true) {
5872
+ // WS5 (#27) — a REFUSED turn is failed but not retryable: the turn declined
5873
+ // to run (isolation was asked for and could not be made), and the answer is
5874
+ // the reason, already in `content`. The retry machinery below would replace
5875
+ // that reason with a "keep trying" offer for a request that cannot succeed,
5876
+ // and would queue a background re-run of the very ask that refused.
5877
+ if (result.generationFailed === true && result.refused !== true) {
5525
5878
  const report = chatRetryBroker.onFailure(sessionId, message, attemptMark);
5526
5879
  if (report) {
5527
5880
  content = content.trim() ? `${content}\n\n${report}` : report;
@@ -5542,6 +5895,26 @@ function handleRequest(req, res) {
5542
5895
  followups: result.followups ?? [],
5543
5896
  provider: result.provider ?? null,
5544
5897
  model: result.model ?? null,
5898
+ // WS1 — the findings this turn recorded, with the gate's verdicts, so
5899
+ // a dashboard client reads the same wire form the CLI, the gateway and
5900
+ // the subagent report (the GUI card is a rendering decision; the FACT
5901
+ // travels here rather than being dropped at the HTTP boundary).
5902
+ findings: result.findings ?? [],
5903
+ // WS5 — the isolation this turn ACTUALLY had, as the console reported it
5904
+ // (absent when the turn was not isolated), never re-derived from the flags
5905
+ // this request sent: a client that asked for isolation and got a refusal
5906
+ // must be able to see the difference, and the refusal is the turn's own
5907
+ // content. The diff arrives with it — `diff.payload` is the same
5908
+ // `{files, summary}` shape the `git:diff` event and the diff card already
5909
+ // use, so the GUI renders one card for both.
5910
+ ...(result.worktree ? { worktree: result.worktree } : {}),
5911
+ // WS5 — what a resume replayed. Sent for the same reason: a deployment
5912
+ // that sets `NUVIRA_RESUME` gets resumed turns, and a reader who is never
5913
+ // told cannot tell a replayed turn from a fresh one.
5914
+ ...(result.resume ? { resume: result.resume } : {}),
5915
+ // WS5 — whether the turn declined to run, so the client labels the failure
5916
+ // as the refusal it is instead of offering a Retry that cannot help.
5917
+ refused: result.refused === true,
5545
5918
  generationFailed: result.generationFailed === true,
5546
5919
  retryQueued,
5547
5920
  });
@@ -5650,6 +6023,89 @@ function handleRequest(req, res) {
5650
6023
  })();
5651
6024
  return;
5652
6025
  }
6026
+ // GET /api/chat/:sessionId/support-bundle — the debug logs THIS conversation
6027
+ // wrote, plus the conversation and a manifest, as one downloadable ZIP.
6028
+ //
6029
+ // WS2 (#24) taught every surface to write an attachable log; a log the user
6030
+ // cannot FIND is only half an artifact, and `~/.nuvira/debug-logs/` is exactly
6031
+ // the kind of path nobody goes and greps while filing a bug. So the chat the
6032
+ // user is already looking at becomes the way in.
6033
+ //
6034
+ // Selected by the log's OWN `# session:` header — never by recency, which with
6035
+ // a second tab open is somebody else's conversation. And when there is no log
6036
+ // the endpoint REFUSES rather than handing over a bundle without the one thing
6037
+ // a bug report needs, saying which step is missing instead.
6038
+ const chatBundleMatch = /^\/api\/chat\/([^/]+)\/support-bundle$/.exec(pathname);
6039
+ if (chatBundleMatch && req.method === 'GET') {
6040
+ void (async () => {
6041
+ // The `?token=` fallback as well as the header, for the same reason the
6042
+ // events stream has one: this is a plain GET that a browser or curl can be
6043
+ // handed, so it must not depend on a JS fetch that sets headers.
6044
+ const token = bearerToken(req) ?? new URL(req.url ?? '/', 'http://localhost').searchParams.get('token');
6045
+ const admin = adminSessions.validate(token);
6046
+ if (!admin) {
6047
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
6048
+ return;
6049
+ }
6050
+ if (!roleCan(admin.role, 'routing.operate')) {
6051
+ writeJson(res, 403, { ok: false, error: `Access denied — role '${admin.role}' cannot read a chat session.` });
6052
+ return;
6053
+ }
6054
+ const sessionId = decodeURIComponent(chatBundleMatch[1]);
6055
+ try {
6056
+ const logs = debugLogsForSession(sessionId);
6057
+ if (logs.length === 0) {
6058
+ // Two different nothings with two different fixes, so they are NOT
6059
+ // collapsed into one message that fits neither.
6060
+ const enabled = debugLoggingEnabled();
6061
+ writeJson(res, 200, {
6062
+ ok: false,
6063
+ reason: enabled ? 'no_logs' : 'logging_disabled',
6064
+ error: enabled
6065
+ ? 'No debug log for this conversation yet. Logging is on, so the next turn here writes one — send a message (a log is written when the turn ENDS, not while it runs), then download again.'
6066
+ : `Session debug logging is off in the dashboard process, so there is nothing to attach yet. Set NUVIRA_DEBUG_LOG=1, restart the dashboard, send a message, then download again. Logs land in ${debugLogDir()}.`,
6067
+ });
6068
+ return;
6069
+ }
6070
+ const now = new Date();
6071
+ const record = chatConsole.get(sessionId);
6072
+ const safeId = sessionId.replace(/[^\w.-]/g, '_').slice(0, 48) || 'session';
6073
+ const files = [];
6074
+ for (const log of logs) {
6075
+ // The log's own basename carries its surface and start time, so two
6076
+ // turns of one conversation cannot collide inside the archive.
6077
+ files.push({ name: basename(log.path).replace(/[^\w.-]/g, '_'), content: log.text });
6078
+ }
6079
+ if (record) {
6080
+ files.push({ name: 'conversation.txt', content: renderSupportTranscript(sessionId, record, now) });
6081
+ }
6082
+ files.unshift({
6083
+ name: 'manifest.txt',
6084
+ content: renderSupportManifest({
6085
+ sessionId,
6086
+ logs,
6087
+ files: ['manifest.txt', ...files.map((f) => f.name)],
6088
+ turns: record ? record.turns.length : null,
6089
+ generatedAt: now,
6090
+ }),
6091
+ });
6092
+ const zip = generateZip(files);
6093
+ res.writeHead(200, {
6094
+ 'Content-Type': 'application/zip',
6095
+ 'Content-Disposition': `attachment; filename="nuvira-support-${safeId}.zip"`,
6096
+ 'Content-Length': zip.length,
6097
+ });
6098
+ res.end(zip);
6099
+ }
6100
+ catch (err) {
6101
+ writeJson(res, 500, {
6102
+ ok: false,
6103
+ error: `Support bundle failed: ${err instanceof Error ? err.message : String(err)}`,
6104
+ });
6105
+ }
6106
+ })();
6107
+ return;
6108
+ }
5653
6109
  // GET /api/chat/:sessionId/events — SSE stream of the session's LIVE
5654
6110
  // progress (agent working steps) + status while a turn is in flight. The
5655
6111
  // client subscribes FIRST, then POSTs the message; tool-call lines stream
@@ -5726,6 +6182,13 @@ function handleRequest(req, res) {
5726
6182
  updatedAt: event.updatedAt,
5727
6183
  })}\n\n`);
5728
6184
  }
6185
+ else if (event.kind === 'finding') {
6186
+ // WS1 — a recorded finding: the claim, the outcome, the evidence and
6187
+ // the verdict the GATE computed. Rendered as its own card so a
6188
+ // CONFIRMED verdict and a PLAUSIBLE guess never read the same in the
6189
+ // thread — the whole point of the row.
6190
+ res.write(`event: finding\ndata: ${JSON.stringify({ finding: event.finding })}\n\n`);
6191
+ }
5729
6192
  else if (event.kind === 'status') {
5730
6193
  res.write(`event: status\ndata: ${JSON.stringify({ status: event.status })}\n\n`);
5731
6194
  }