talos-code 0.0.1 → 0.2.0

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 (407) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +661 -0
  3. package/README.md +116 -3
  4. package/THIRD_PARTY_NOTICES.md +40 -0
  5. package/dist/archive/zip.js +73 -0
  6. package/dist/args.js +100 -0
  7. package/dist/automations/history-store.js +34 -0
  8. package/dist/automations/policy-store.js +43 -0
  9. package/dist/automations/runner.js +107 -0
  10. package/dist/automations/schedule.js +85 -0
  11. package/dist/automations/windows-task.js +53 -0
  12. package/dist/commands/advanced-cli.js +233 -0
  13. package/dist/commands/checkpoint-cli.js +45 -0
  14. package/dist/commands/config-cli.js +107 -0
  15. package/dist/commands/context.js +17 -0
  16. package/dist/commands/extensions-cli.js +207 -0
  17. package/dist/commands/project-cli.js +74 -0
  18. package/dist/commands/project-commands.js +143 -0
  19. package/dist/commands/provider-cli.js +244 -0
  20. package/dist/commands/services-cli.js +248 -0
  21. package/dist/commands/session-cli.js +171 -0
  22. package/dist/commands/system-cli.js +405 -0
  23. package/dist/config/commands.js +60 -0
  24. package/dist/config/load.js +382 -0
  25. package/dist/config/migrations.js +48 -0
  26. package/dist/config/types.js +11 -0
  27. package/dist/diagnostics/development-log.js +154 -0
  28. package/dist/diagnostics/doctor.js +104 -0
  29. package/dist/diagnostics/redact.js +80 -0
  30. package/dist/diagnostics/zip.js +52 -0
  31. package/dist/errors.js +156 -0
  32. package/dist/events/bridge.js +130 -0
  33. package/dist/extensions/installer.js +178 -0
  34. package/dist/extensions/package-schema.js +24 -0
  35. package/dist/headless/result.js +84 -0
  36. package/dist/headless/run.js +230 -0
  37. package/dist/i18n/en/approval.js +46 -0
  38. package/dist/i18n/en/common.js +9 -0
  39. package/dist/i18n/en/credentials.js +92 -0
  40. package/dist/i18n/en/errors.js +267 -0
  41. package/dist/i18n/en/firstrun.js +101 -0
  42. package/dist/i18n/en/providers.js +37 -0
  43. package/dist/i18n/en/screen.js +774 -0
  44. package/dist/i18n/en/tools.js +127 -0
  45. package/dist/i18n/en.js +14 -0
  46. package/dist/i18n/error-view.js +307 -0
  47. package/dist/i18n/index.js +19 -0
  48. package/dist/i18n/kernel-map.js +139 -0
  49. package/dist/io.js +45 -0
  50. package/dist/main.js +331 -0
  51. package/dist/paths.js +41 -0
  52. package/dist/protocol/v2/codec.js +9 -0
  53. package/dist/protocol/v2/events.js +555 -0
  54. package/dist/protocol/v2/index.js +4 -0
  55. package/dist/protocol/v2/replay.js +28 -0
  56. package/dist/protocol/v2/types.js +1 -0
  57. package/dist/provider/control-plane.js +135 -0
  58. package/dist/provider/environment-keys.js +275 -0
  59. package/dist/provider/health.js +110 -0
  60. package/dist/provider/missing-key.js +48 -0
  61. package/dist/provider/model-catalog.js +168 -0
  62. package/dist/provider/openrouter-login.js +172 -0
  63. package/dist/provider/provider-text.js +13 -0
  64. package/dist/provider/store.js +95 -0
  65. package/dist/provider/system-keyring.js +214 -0
  66. package/dist/runtime/active-run.js +63 -0
  67. package/dist/runtime/agent-tree.js +74 -0
  68. package/dist/runtime/attachments.js +84 -0
  69. package/dist/runtime/brokered-executor.js +395 -0
  70. package/dist/runtime/context-archive.js +69 -0
  71. package/dist/runtime/context-status.js +76 -0
  72. package/dist/runtime/create-runtime.js +111 -0
  73. package/dist/runtime/model-profile.js +361 -0
  74. package/dist/runtime/output-store.js +137 -0
  75. package/dist/runtime/provider-attempts.js +185 -0
  76. package/dist/runtime/replay-buffer.js +153 -0
  77. package/dist/runtime/repo.js +95 -0
  78. package/dist/runtime/session-facade.js +135 -0
  79. package/dist/runtime/session-summary.js +125 -0
  80. package/dist/runtime/supervisor.js +265 -0
  81. package/dist/runtime/talos-composition.js +1011 -0
  82. package/dist/runtime/types.js +5 -0
  83. package/dist/runtime/usage-snapshot.js +82 -0
  84. package/dist/security/auto-classifier.js +38 -0
  85. package/dist/security/credential-free-environment.js +54 -0
  86. package/dist/security/evaluate.js +243 -0
  87. package/dist/security/execution-backends.js +236 -0
  88. package/dist/security/execution-broker.js +102 -0
  89. package/dist/security/forge-scan.js +74 -0
  90. package/dist/security/from-approval.js +39 -0
  91. package/dist/security/mxc-execution-backend.js +281 -0
  92. package/dist/security/permission-engine.js +138 -0
  93. package/dist/security/permission-explanation.js +54 -0
  94. package/dist/security/persist.js +179 -0
  95. package/dist/security/plugin-guard.js +230 -0
  96. package/dist/security/process-tree-evidence-store.js +74 -0
  97. package/dist/security/process-tree-probe.js +554 -0
  98. package/dist/security/project-resource-inventory.js +186 -0
  99. package/dist/security/project-trust-gate.js +38 -0
  100. package/dist/security/project-trust.js +367 -0
  101. package/dist/security/rule-parser.js +201 -0
  102. package/dist/security/shell-segmentation.js +68 -0
  103. package/dist/security/trust-authority.js +324 -0
  104. package/dist/security/types.js +1 -0
  105. package/dist/security/workspace-identity.js +102 -0
  106. package/dist/services/automation-facade.js +59 -0
  107. package/dist/services/forge-facade.js +77 -0
  108. package/dist/services/hook-facade.js +240 -0
  109. package/dist/services/index.js +16 -0
  110. package/dist/services/library-facade.js +174 -0
  111. package/dist/services/mcp-facade.js +348 -0
  112. package/dist/services/memory-facade.js +126 -0
  113. package/dist/services/notes-facade.js +157 -0
  114. package/dist/services/plugin-facade.js +308 -0
  115. package/dist/services/research-facade.js +110 -0
  116. package/dist/services/task-board-facade.js +236 -0
  117. package/dist/services/workflow-catalog.js +246 -0
  118. package/dist/sessions/export-format.js +98 -0
  119. package/dist/sessions/metadata-store.js +160 -0
  120. package/dist/sessions/share.js +122 -0
  121. package/dist/sessions/transfer.js +16 -0
  122. package/dist/subcommands.js +62 -0
  123. package/dist/tui/agent-roster.js +36 -0
  124. package/dist/tui/agent-view.js +51 -0
  125. package/dist/tui/app.js +4098 -0
  126. package/dist/tui/approval.js +36 -0
  127. package/dist/tui/boot/boot-sequence.js +76 -0
  128. package/dist/tui/boot/cinematic.js +243 -0
  129. package/dist/tui/boot/desktop-logo.js +82 -0
  130. package/dist/tui/boot.js +3 -0
  131. package/dist/tui/busy-input.js +58 -0
  132. package/dist/tui/catalog-service.js +560 -0
  133. package/dist/tui/components/assistant-stream.js +1 -0
  134. package/dist/tui/components/command-menu.js +68 -0
  135. package/dist/tui/components/composer.js +219 -0
  136. package/dist/tui/components/diff.js +35 -0
  137. package/dist/tui/components/footer.js +48 -0
  138. package/dist/tui/components/header.js +6 -0
  139. package/dist/tui/components/markdown.js +305 -0
  140. package/dist/tui/components/status-indicator.js +38 -0
  141. package/dist/tui/components/terminal-shell.js +214 -0
  142. package/dist/tui/components/thinking-row.js +19 -0
  143. package/dist/tui/components/tool-row.js +97 -0
  144. package/dist/tui/components/transcript-virtualizer.js +165 -0
  145. package/dist/tui/components/transcript.js +43 -0
  146. package/dist/tui/descendant-approvals.js +104 -0
  147. package/dist/tui/diff-model.js +77 -0
  148. package/dist/tui/editor-history.js +21 -0
  149. package/dist/tui/editor.js +210 -0
  150. package/dist/tui/event-adapter.js +436 -0
  151. package/dist/tui/exit-output.js +58 -0
  152. package/dist/tui/external-editor.js +53 -0
  153. package/dist/tui/file-completion.js +89 -0
  154. package/dist/tui/focus-manager.js +5 -0
  155. package/dist/tui/highlight.js +30 -0
  156. package/dist/tui/input-router.js +18 -0
  157. package/dist/tui/interrupt.js +99 -0
  158. package/dist/tui/keybindings.js +194 -0
  159. package/dist/tui/keymap-resolver.js +91 -0
  160. package/dist/tui/launch-state.js +148 -0
  161. package/dist/tui/line-diff.js +57 -0
  162. package/dist/tui/live-activity.js +56 -0
  163. package/dist/tui/metrics.js +123 -0
  164. package/dist/tui/onboarding.js +36 -0
  165. package/dist/tui/overlays/agent-tree.js +43 -0
  166. package/dist/tui/overlays/approval-dialog.js +642 -0
  167. package/dist/tui/overlays/automation-center.js +51 -0
  168. package/dist/tui/overlays/checkpoint-picker.js +47 -0
  169. package/dist/tui/overlays/context-inspector.js +36 -0
  170. package/dist/tui/overlays/effort-line.js +110 -0
  171. package/dist/tui/overlays/forge-center.js +46 -0
  172. package/dist/tui/overlays/help-dialog.js +16 -0
  173. package/dist/tui/overlays/history-picker.js +37 -0
  174. package/dist/tui/overlays/hook-center.js +70 -0
  175. package/dist/tui/overlays/library-center.js +55 -0
  176. package/dist/tui/overlays/mcp-center.js +60 -0
  177. package/dist/tui/overlays/memory-center.js +63 -0
  178. package/dist/tui/overlays/model-picker.js +77 -0
  179. package/dist/tui/overlays/notes-center.js +53 -0
  180. package/dist/tui/overlays/overlay-host.js +8 -0
  181. package/dist/tui/overlays/plugin-center.js +76 -0
  182. package/dist/tui/overlays/provider-picker.js +171 -0
  183. package/dist/tui/overlays/queue-editor.js +32 -0
  184. package/dist/tui/overlays/research-center.js +59 -0
  185. package/dist/tui/overlays/scroll-window.js +234 -0
  186. package/dist/tui/overlays/session-picker.js +115 -0
  187. package/dist/tui/overlays/tasks-center.js +83 -0
  188. package/dist/tui/overlays/theme-picker.js +21 -0
  189. package/dist/tui/overlays/transcript-search.js +58 -0
  190. package/dist/tui/overlays/trust-center.js +29 -0
  191. package/dist/tui/overlays/workflow-center.js +46 -0
  192. package/dist/tui/project-file-index.js +214 -0
  193. package/dist/tui/project-references.js +85 -0
  194. package/dist/tui/project-trust-prompt.js +289 -0
  195. package/dist/tui/prompt-history-store.js +116 -0
  196. package/dist/tui/queue-store.js +110 -0
  197. package/dist/tui/regions/budget.js +59 -0
  198. package/dist/tui/regions/views.js +96 -0
  199. package/dist/tui/render-coordinator.js +148 -0
  200. package/dist/tui/render-scheduler.js +4 -0
  201. package/dist/tui/run.js +69 -0
  202. package/dist/tui/selection-list.js +29 -0
  203. package/dist/tui/session-controller.js +791 -0
  204. package/dist/tui/session-export.js +54 -0
  205. package/dist/tui/setup-wizard.js +46 -0
  206. package/dist/tui/shell-input.js +35 -0
  207. package/dist/tui/shell-layout.js +48 -0
  208. package/dist/tui/shell-model.js +98 -0
  209. package/dist/tui/slash-commands.js +82 -0
  210. package/dist/tui/state.js +151 -0
  211. package/dist/tui/status-bar.js +125 -0
  212. package/dist/tui/status-view.js +40 -0
  213. package/dist/tui/terminal-capabilities.js +9 -0
  214. package/dist/tui/terminal-session.js +11 -0
  215. package/dist/tui/text-width.js +66 -0
  216. package/dist/tui/theme-catalog.js +27 -0
  217. package/dist/tui/theme-store.js +23 -0
  218. package/dist/tui/theme.js +23 -0
  219. package/dist/tui/tool-display.js +135 -0
  220. package/dist/tui/tool-facts.js +1 -0
  221. package/dist/tui/tools/bash-renderer.js +32 -0
  222. package/dist/tui/tools/change.js +61 -0
  223. package/dist/tui/tools/edit-renderer.js +17 -0
  224. package/dist/tui/tools/generic-renderer.js +13 -0
  225. package/dist/tui/tools/list-renderer.js +14 -0
  226. package/dist/tui/tools/read-renderer.js +13 -0
  227. package/dist/tui/tools/registry.js +20 -0
  228. package/dist/tui/tools/row-format.js +187 -0
  229. package/dist/tui/tools/search-renderer.js +38 -0
  230. package/dist/tui/tools/shared.js +97 -0
  231. package/dist/tui/tools/write-renderer.js +15 -0
  232. package/dist/tui/transcript-model.js +441 -0
  233. package/dist/tui/ui-preferences.js +79 -0
  234. package/dist/tui/usage-view.js +77 -0
  235. package/dist/tui/vim-mode.js +99 -0
  236. package/dist/update/check.js +15 -0
  237. package/dist/update/npm.js +51 -0
  238. package/dist/update/run.js +129 -0
  239. package/dist/version.js +2 -0
  240. package/dist/workspace/checkpoint-store.js +210 -0
  241. package/dist/workspace/checkpoint.js +413 -0
  242. package/dist/workspace/restore.js +232 -0
  243. package/package.json +64 -5
  244. package/vendor/context-engine/package.json +11 -0
  245. package/vendor/context-engine/src/compaction-planner.mjs +57 -0
  246. package/vendor/context-engine/src/contracts.mjs +48 -0
  247. package/vendor/context-engine/src/engine.mjs +445 -0
  248. package/vendor/context-engine/src/node/context-export.mjs +164 -0
  249. package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
  250. package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
  251. package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
  252. package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
  253. package/vendor/context-engine/src/profiles.mjs +10 -0
  254. package/vendor/context-engine/src/retrieval.mjs +104 -0
  255. package/vendor/context-engine/src/summary.mjs +97 -0
  256. package/vendor/context-engine/src/usage.mjs +34 -0
  257. package/vendor/harness-ui/package.json +38 -0
  258. package/vendor/harness-ui/src/acp-agent.mjs +350 -0
  259. package/vendor/harness-ui/src/agent-service.mjs +2188 -0
  260. package/vendor/harness-ui/src/agui-events.mjs +452 -0
  261. package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
  262. package/vendor/harness-ui/src/artifact-store.mjs +39 -0
  263. package/vendor/harness-ui/src/assistenza.mjs +123 -0
  264. package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
  265. package/vendor/harness-ui/src/automation-store.mjs +145 -0
  266. package/vendor/harness-ui/src/browser-annota.mjs +639 -0
  267. package/vendor/harness-ui/src/browser-frame.mjs +211 -0
  268. package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
  269. package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
  270. package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
  271. package/vendor/harness-ui/src/browser-stream.mjs +445 -0
  272. package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
  273. package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
  274. package/vendor/harness-ui/src/config.mjs +697 -0
  275. package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
  276. package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
  277. package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
  278. package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
  279. package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
  280. package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
  281. package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
  282. package/vendor/harness-ui/src/context-runtime.mjs +118 -0
  283. package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
  284. package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
  285. package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
  286. package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
  287. package/vendor/harness-ui/src/custom-task.mjs +171 -0
  288. package/vendor/harness-ui/src/doctor.mjs +142 -0
  289. package/vendor/harness-ui/src/document-filename.mjs +97 -0
  290. package/vendor/harness-ui/src/document-generator.mjs +493 -0
  291. package/vendor/harness-ui/src/document-report.mjs +331 -0
  292. package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
  293. package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
  294. package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
  295. package/vendor/harness-ui/src/forge-contract.mjs +221 -0
  296. package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
  297. package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
  298. package/vendor/harness-ui/src/generation-idle.mjs +332 -0
  299. package/vendor/harness-ui/src/gguf-header.mjs +207 -0
  300. package/vendor/harness-ui/src/git-service.mjs +626 -0
  301. package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
  302. package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
  303. package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
  304. package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
  305. package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
  306. package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
  307. package/vendor/harness-ui/src/hook-registry.mjs +186 -0
  308. package/vendor/harness-ui/src/http-app.mjs +6259 -0
  309. package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
  310. package/vendor/harness-ui/src/id-archivio.mjs +27 -0
  311. package/vendor/harness-ui/src/image-generator.mjs +143 -0
  312. package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
  313. package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
  314. package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
  315. package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
  316. package/vendor/harness-ui/src/library-store.mjs +652 -0
  317. package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
  318. package/vendor/harness-ui/src/local-model-store.mjs +339 -0
  319. package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
  320. package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
  321. package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
  322. package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
  323. package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
  324. package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
  325. package/vendor/harness-ui/src/mcp-client.mjs +98 -0
  326. package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
  327. package/vendor/harness-ui/src/mcp-session.mjs +177 -0
  328. package/vendor/harness-ui/src/memory-store.mjs +227 -0
  329. package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
  330. package/vendor/harness-ui/src/model-catalog.mjs +129 -0
  331. package/vendor/harness-ui/src/model-destination.mjs +189 -0
  332. package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
  333. package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
  334. package/vendor/harness-ui/src/notes-store.mjs +250 -0
  335. package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
  336. package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
  337. package/vendor/harness-ui/src/path-policy.mjs +442 -0
  338. package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
  339. package/vendor/harness-ui/src/plugin-session.mjs +180 -0
  340. package/vendor/harness-ui/src/process-policy.mjs +345 -0
  341. package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
  342. package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
  343. package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
  344. package/vendor/harness-ui/src/provider-probe.mjs +582 -0
  345. package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
  346. package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
  347. package/vendor/harness-ui/src/public-problem.mjs +109 -0
  348. package/vendor/harness-ui/src/research/card.mjs +235 -0
  349. package/vendor/harness-ui/src/research/citations.mjs +142 -0
  350. package/vendor/harness-ui/src/research/collector.mjs +275 -0
  351. package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
  352. package/vendor/harness-ui/src/research/dossier.mjs +114 -0
  353. package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
  354. package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
  355. package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
  356. package/vendor/harness-ui/src/research/independence.mjs +159 -0
  357. package/vendor/harness-ui/src/research/ledger.mjs +166 -0
  358. package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
  359. package/vendor/harness-ui/src/research/narration.mjs +181 -0
  360. package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
  361. package/vendor/harness-ui/src/research/opposing.mjs +305 -0
  362. package/vendor/harness-ui/src/research/outline.mjs +111 -0
  363. package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
  364. package/vendor/harness-ui/src/research/pdf.mjs +291 -0
  365. package/vendor/harness-ui/src/research/plan.mjs +301 -0
  366. package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
  367. package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
  368. package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
  369. package/vendor/harness-ui/src/research/recheck.mjs +194 -0
  370. package/vendor/harness-ui/src/research/report.mjs +203 -0
  371. package/vendor/harness-ui/src/research/run.mjs +527 -0
  372. package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
  373. package/vendor/harness-ui/src/research/verification.mjs +572 -0
  374. package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
  375. package/vendor/harness-ui/src/research-store.mjs +1133 -0
  376. package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
  377. package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
  378. package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
  379. package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
  380. package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
  381. package/vendor/harness-ui/src/search-source-store.mjs +172 -0
  382. package/vendor/harness-ui/src/session-registry.mjs +6095 -0
  383. package/vendor/harness-ui/src/session-store.mjs +220 -0
  384. package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
  385. package/vendor/harness-ui/src/setup-stato.mjs +31 -0
  386. package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
  387. package/vendor/harness-ui/src/skill-registry.mjs +120 -0
  388. package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
  389. package/vendor/harness-ui/src/static-files.mjs +96 -0
  390. package/vendor/harness-ui/src/stream-partition.mjs +123 -0
  391. package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
  392. package/vendor/harness-ui/src/task-catalog.mjs +65 -0
  393. package/vendor/harness-ui/src/tasks-store.mjs +220 -0
  394. package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
  395. package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
  396. package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
  397. package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
  398. package/vendor/harness-ui/src/usage-cache.mjs +315 -0
  399. package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
  400. package/vendor/harness-ui/src/workspace-context.mjs +124 -0
  401. package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
  402. package/vendor/harness-ui/src/workspace-files.mjs +589 -0
  403. package/vendor/harness-ui/src/workspace-info.mjs +189 -0
  404. package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
  405. package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
  406. package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
  407. package/vendor/manifest.json +170 -0
@@ -0,0 +1,1348 @@
1
+ /**
2
+ * Adapter verso il runtime proprietario configurato dall'operatore.
3
+ *
4
+ * Il desktop non importa più moduli da checkout fratelli a tempo di build o
5
+ * di avvio. Se serve il runtime completo, il server deve indicare un modulo
6
+ * assoluto con `TALOS_OWNER_RUNTIME_MODULE`; il caricamento resta ritardato e
7
+ * fallisce in modo esplicito quando la dipendenza non è disponibile. Le
8
+ * funzioni pure minime per la compattazione restano qui, per mantenere il
9
+ * comportamento già coperto dai test senza nascondere una dipendenza.
10
+ */
11
+ import { pathToFileURL, fileURLToPath } from 'node:url';
12
+ import { isAbsolute } from 'node:path';
13
+ import { statSync } from 'node:fs';
14
+ import { createParser } from 'eventsource-parser';
15
+ import { eseguiFlowForgeLocale, FORGE_PREFISSO_NOME_TOOL, validaManifestForgeLocale } from './forge-contract.mjs';
16
+ import { parseRuntimeOwnerSnapshot } from './runtime-owner-contract.mjs';
17
+ import { risolviDestinazioneModello, separaFonteModello, FONTI_MODELLO, validaFallbackProviders } from './model-destination.mjs';
18
+ import { REGISTRO_FORNITORI } from './provider-registry.mjs';
19
+ import { classificaErroreDiCorsa } from './research-orchestrator.mjs';
20
+ import { normalizzaUsage, scontoDaCache } from './usage-cache.mjs'; // 12/09, P-B: i nomi della cache sono uno per fornitore, il lettore uno solo
21
+ import { nativeProviderResponse, stripNativeMetadata } from './native-provider-adapter.mjs';
22
+ import { preparaRichiestaCompatibile, OpenAiCompatibleRuntimeError } from './openai-compatible-runtime.mjs'; // P-D (12/09): Z.AI accetta solo alcuni livelli di ragionamento
23
+ // P-L · ponte locale senza listener, sessione esterna posseduta dalla Response.
24
+ import { rispostaAgenteAcp } from './acp-agent.mjs';
25
+ // P-L · fine import instradamento.
26
+ // BC-48 A · sezioni di progetto nel canale degli originali, prima della richiesta.
27
+ import { trovaIstruzioniDiProgetto, trovaRadiceProgetto } from './istruzioni-di-progetto.mjs';
28
+ import { collegaSezioniAiContextHooks } from './context-provider-adapter.mjs';
29
+ // P0 · punto 7 (16/09): il failsafe di inattività e il dispatcher stanno in una porta sola.
30
+ import {
31
+ SilenzioDelFornitoreError,
32
+ dispatcherDiGenerazione,
33
+ leggiInattivitaGenerazioneMs,
34
+ sorvegliaCorpoDiGenerazione,
35
+ sorvegliaInattivita,
36
+ } from './generation-idle.mjs';
37
+
38
+ const ENDPOINT_OPENROUTER = 'https://openrouter.ai/api/v1/chat/completions';
39
+ const RICHIESTA_DI_RIASSUNTO = 'Riassumi la conversazione mantenendo decisioni, file e risultati utili al lavoro.';
40
+ const GIRI_PRIMA_DI_COMPATTARE = 12;
41
+ const OPENROUTER_IDLE_MS_PREDEFINITO = 60_000;
42
+ const SSE_BUFFER_MASSIMO = 1_048_576;
43
+ const SCHEMA_DESCRIZIONE_COMANDO = Object.freeze({
44
+ type: 'string',
45
+ description: 'Breve descrizione in italiano, al presente e comprensibile all’utente, dell’obiettivo di questo comando. Non copiare il comando tecnico.',
46
+ minLength: 3,
47
+ maxLength: 120,
48
+ });
49
+
50
+ export class OwnerRuntimeUnavailableError extends Error {
51
+ constructor(message, code = 'OWNER_RUNTIME_NOT_CONFIGURED', options = {}) {
52
+ super(message);
53
+ this.name = 'OwnerRuntimeUnavailableError';
54
+ this.code = code;
55
+ if (options.cause) this.cause = options.cause;
56
+ }
57
+ }
58
+
59
+ class OpenRouterIdleTimeoutError extends Error {
60
+ constructor(timeoutMs) {
61
+ super(`OpenRouter non ha inviato attività per ${Math.max(1, Math.round(timeoutMs / 1_000))} secondi.`);
62
+ this.name = 'OpenRouterIdleTimeoutError';
63
+ this.code = 'OPENROUTER_IDLE_TIMEOUT';
64
+ }
65
+ }
66
+
67
+ class OpenRouterStreamError extends Error {
68
+ constructor(error) {
69
+ const message = typeof error?.message === 'string' && error.message.trim()
70
+ ? error.message.trim()
71
+ : 'OpenRouter ha interrotto la risposta in corso.';
72
+ super(message);
73
+ this.name = 'OpenRouterStreamError';
74
+ this.code = error?.code ?? error?.metadata?.error_type ?? 'OPENROUTER_STREAM_ERROR';
75
+ this.providerError = error ?? null;
76
+ }
77
+ }
78
+
79
+ function rispostaRitentabile(status) {
80
+ return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
81
+ }
82
+
83
+ function attesaEsponenziale(tentativo) {
84
+ return Math.min(2_000, 200 * (2 ** tentativo));
85
+ }
86
+
87
+ /**
88
+ * Chiamata testuale OpenRouter usata solo dal riassuntore locale. Il runtime
89
+ * principale, quando configurato, resta la fonte autoritativa per il ciclo
90
+ * agente e per i tool.
91
+ */
92
+ export async function chiamaConRitentaLocale({
93
+ modello, chiave, messaggi, attrezzi, tentativiMassimi = 4,
94
+ fetchDiRete = fetch, dormi = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
95
+ }) {
96
+ let ultimoStato = null;
97
+ let ultimoTesto = '';
98
+ for (let tentativo = 0; tentativo < tentativiMassimi; tentativo += 1) {
99
+ const risposta = await fetchDiRete(ENDPOINT_OPENROUTER, {
100
+ method: 'POST',
101
+ headers: { Authorization: `Bearer ${chiave}`, 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ model: modello, messages: messaggi, tools: attrezzi, tool_choice: 'auto' }),
103
+ /*
104
+ * ⛔ P0 · punto 7 (16/09/2026) — anche il riassuntore locale aveva i suoi 180 s scritti a mano.
105
+ * Un riassunto è una chiamata al MODELLO come le altre: se il contesto da comprimere è enorme,
106
+ * il prefill può tacere per minuti (ggml-org/llama.cpp#22997). Il numero non si alza: si usa
107
+ * lo STESSO failsafe di tutto il resto, così esiste UNA soglia da conoscere invece di quattro.
108
+ */
109
+ signal: AbortSignal.timeout(leggiInattivitaGenerazioneMs() || 1_800_000),
110
+ });
111
+ if (risposta.ok) {
112
+ const corpo = await risposta.json();
113
+ const scelta = corpo?.choices?.[0]?.message;
114
+ if (!scelta) throw new Error('Il fornitore non ha restituito una risposta utilizzabile.');
115
+ return { scelta, usage: corpo?.usage ?? null, tentativi: tentativo + 1 };
116
+ }
117
+ ultimoStato = risposta.status;
118
+ ultimoTesto = typeof risposta.text === 'function' ? String(await risposta.text()).slice(0, 300) : '';
119
+ if (!rispostaRitentabile(risposta.status)) break;
120
+ if (tentativo < tentativiMassimi - 1) await dormi(attesaEsponenziale(tentativo));
121
+ }
122
+ const errore = new Error(`Il fornitore non risponde (stato ${ultimoStato ?? 'sconosciuto'}). ${ultimoTesto}`.trim());
123
+ errore.stato = ultimoStato;
124
+ errore.limitatoDalFornitore = rispostaRitentabile(ultimoStato);
125
+ throw errore;
126
+ }
127
+
128
+ export async function compattaConversazioneLocale(messaggi, chiamaModello) {
129
+ let risposta;
130
+ let usage = null;
131
+ try {
132
+ ({ scelta: risposta, usage } = await chiamaModello([...messaggi, { role: 'user', content: RICHIESTA_DI_RIASSUNTO }]));
133
+ } catch {
134
+ return { messaggi, compattato: false, usage: null };
135
+ }
136
+ const riassunto = String(risposta?.content ?? '').trim();
137
+ if (!riassunto) return { messaggi, compattato: false, usage };
138
+ return {
139
+ messaggi: [
140
+ messaggi[0],
141
+ messaggi[1],
142
+ { role: 'user', content: `[conversazione compattata al giro ${GIRI_PRIMA_DI_COMPATTARE}: quanto segue è un riassunto, non la cronologia originale]\n\n${riassunto}` },
143
+ ],
144
+ compattato: true,
145
+ usage,
146
+ };
147
+ }
148
+
149
+ function normalizzaModuloPath(modulePath) {
150
+ if (modulePath === null || modulePath === undefined || modulePath === '') return null;
151
+ if (typeof modulePath !== 'string' || !isAbsolute(modulePath) || modulePath.includes('\0')) {
152
+ throw new OwnerRuntimeUnavailableError('Il modulo runtime deve essere un percorso assoluto.', 'OWNER_RUNTIME_PATH_INVALID');
153
+ }
154
+ return pathToFileURL(modulePath).href;
155
+ }
156
+
157
+ /**
158
+ * Il runtime owner resta read-only e provider-neutral. Sul confine desktop
159
+ * arricchiamo soltanto il tool AVM `shell`: un comando arbitrario non ha un
160
+ * titolo umano ricavabile senza inventarne l'intento, quindi lo deve fornire
161
+ * il modello nello stesso JSON della tool-call. Gli schemi MCP/plugin/Forge
162
+ * non vengono mai toccati: possono essere strict e rifiutare campi estranei.
163
+ *
164
+ * @param {unknown} body
165
+ * @returns {unknown}
166
+ */
167
+ export function adattaRichiestaConDescrizioneComando(body) {
168
+ if (!body || typeof body !== 'object' || !Array.isArray(body.tools)) return body;
169
+ let modificata = false;
170
+ const tools = body.tools.map((tool) => {
171
+ const funzione = tool?.type === 'function' ? tool.function : null;
172
+ const parametri = funzione?.name === 'shell' ? funzione.parameters : null;
173
+ if (!parametri || typeof parametri !== 'object' || parametri.type !== 'object') return tool;
174
+ const proprieta = parametri.properties && typeof parametri.properties === 'object' ? parametri.properties : {};
175
+ const richiesti = Array.isArray(parametri.required) ? parametri.required : [];
176
+ const descrizione = proprieta.descrizione ?? SCHEMA_DESCRIZIONE_COMANDO;
177
+ const required = richiesti.includes('descrizione') ? richiesti : [...richiesti, 'descrizione'];
178
+ if (proprieta.descrizione === descrizione && required === richiesti) return tool;
179
+ modificata = true;
180
+ return {
181
+ ...tool,
182
+ function: {
183
+ ...funzione,
184
+ parameters: {
185
+ ...parametri,
186
+ properties: { ...proprieta, descrizione },
187
+ required,
188
+ },
189
+ },
190
+ };
191
+ });
192
+ return modificata ? { ...body, tools } : body;
193
+ }
194
+
195
+ /**
196
+ * Adatta il body JSON senza cambiare trasporto, credenziali, signal o forma
197
+ * della Response. Un body non JSON/non-tool attraversa il confine invariato.
198
+ *
199
+ * @param {typeof fetch} fetchDiRete
200
+ * @returns {typeof fetch}
201
+ */
202
+ export function creaFetchConDescrizioneComando(fetchDiRete = fetch) {
203
+ if (typeof fetchDiRete !== 'function') throw new TypeError('fetchDiRete deve essere una funzione.');
204
+ return async (url, init = undefined) => {
205
+ if (typeof init?.body !== 'string') return fetchDiRete(url, init);
206
+ let body;
207
+ try { body = JSON.parse(init.body); } catch { return fetchDiRete(url, init); }
208
+ const adattato = adattaRichiestaConDescrizioneComando(body);
209
+ if (adattato === body) return fetchDiRete(url, init);
210
+ return fetchDiRete(url, { ...init, body: JSON.stringify(adattato) });
211
+ };
212
+ }
213
+
214
+ /**
215
+ * ⛔⛔ P0 · punto 7 (16/09/2026) — I 300 SECONDI CHE NESSUNO AVEVA DICHIARATO.
216
+ *
217
+ * Sotto `fetch` c'è undici, con `headersTimeout` e `bodyTimeout` a **300 s di serie**: togliere i
218
+ * tetti dal nostro codice senza toccare questo lascerebbe il muro dov'era, solo più difficile da
219
+ * vedere (è la firma di openai/codex#23807: stalli di ESATTAMENTE 300 s). Misurato con `grep`:
220
+ * `setGlobalDispatcher` non compare in nessun file del repository.
221
+ *
222
+ * ⛔ Si aggiunge SOLO sulle chiamate ai fornitori, mai globalmente: ricerca web, hub dei modelli,
223
+ * proxy delle immagini e MCP devono restare impazienti. E il dispatcher globale avrebbe voluto
224
+ * il pacchetto npm `undici`, che NON è una dipendenza dichiarata di harness-ui.
225
+ * ⭐ Un oggetto vuoto quando non si può costruire: la chiamata parte identica a prima, e il
226
+ * `README` dice cosa resta in quel caso. Nessun silenzio.
227
+ */
228
+ function dispatcherDiRichiesta(inattivitaMs) {
229
+ const dispatcher = dispatcherDiGenerazione({ limiteMs: inattivitaMs });
230
+ return dispatcher ? { dispatcher } : {};
231
+ }
232
+
233
+ function urlOpenRouterChat(url) {
234
+ try {
235
+ const parsed = new URL(typeof url === 'string' || url instanceof URL ? url : url?.url);
236
+ return parsed.hostname === 'openrouter.ai' && parsed.pathname.endsWith('/chat/completions');
237
+ } catch {
238
+ return false;
239
+ }
240
+ }
241
+
242
+ function capabilityReasoning(capability) {
243
+ const reasoning = capability?.reasoning;
244
+ return reasoning && typeof reasoning === 'object' ? reasoning : null;
245
+ }
246
+
247
+ /**
248
+ * Applica esclusivamente capacità dichiarate dal catalogo OpenRouter. Non
249
+ * inventa effort: se un modello mandatory non espone un valore utilizzabile,
250
+ * rimuove `none` e abilita il ragionamento lasciando la scelta al provider.
251
+ */
252
+ export function normalizzaReasoningPerModello(reasoning, capability) {
253
+ const regole = capabilityReasoning(capability);
254
+ if (!regole) return reasoning;
255
+ const supported = Array.isArray(regole.supportedEfforts)
256
+ ? regole.supportedEfforts.filter((value) => typeof value === 'string' && value !== 'none')
257
+ : null;
258
+ const defaultEffort = typeof regole.defaultEffort === 'string' && regole.defaultEffort !== 'none'
259
+ && (!supported || supported.includes(regole.defaultEffort))
260
+ ? regole.defaultEffort
261
+ : supported?.[0] ?? null;
262
+ if (reasoning == null) {
263
+ if (regole.mandatory !== true) return reasoning;
264
+ return defaultEffort ? { effort: defaultEffort } : { enabled: true };
265
+ }
266
+ if (typeof reasoning !== 'object' || Array.isArray(reasoning)) return reasoning;
267
+ const result = { ...reasoning };
268
+ const effort = typeof result.effort === 'string' ? result.effort : null;
269
+ const nonSupportato = effort && effort !== 'none' && supported && !supported.includes(effort);
270
+ if ((regole.mandatory === true && effort === 'none') || nonSupportato) {
271
+ if (defaultEffort) result.effort = defaultEffort;
272
+ else delete result.effort;
273
+ }
274
+ if (regole.mandatory === true && !('effort' in result) && !('enabled' in result)) result.enabled = true;
275
+ return result;
276
+ }
277
+
278
+ function statusPerErroreStream(error) {
279
+ const numeric = Number(error?.code);
280
+ if (Number.isInteger(numeric) && numeric >= 400 && numeric <= 599) return numeric;
281
+ const tipo = String(error?.metadata?.error_type ?? error?.code ?? '').toLowerCase();
282
+ if (tipo.includes('timeout')) return 408;
283
+ if (tipo.includes('rate_limit')) return 429;
284
+ if (tipo.includes('overloaded') || tipo.includes('unavailable') || tipo.includes('server')) return 503;
285
+ if (tipo.includes('authentication')) return 401;
286
+ return 502;
287
+ }
288
+
289
+ function rispostaErrore(status, error) {
290
+ const message = typeof error?.message === 'string' && error.message.trim()
291
+ ? error.message.trim()
292
+ : 'Il fornitore non ha completato la risposta.';
293
+ return new Response(JSON.stringify({ error: { code: status, message } }), {
294
+ status,
295
+ headers: { 'content-type': 'application/json; charset=utf-8' },
296
+ });
297
+ }
298
+
299
+ /**
300
+ * ⛔ P0 · punto 7 (16/09/2026) — UNA SOLA IMPLEMENTAZIONE DEL GUARDIANO.
301
+ *
302
+ * Il corpo di questa funzione è diventato `sorvegliaInattivita` in `generation-idle.mjs`: era già
303
+ * il guardiano GIUSTO (inattività, non durata), ma esisteva solo per OpenRouter, e nessun altro
304
+ * fornitore — né il motore locale — poteva usarlo. Qui resta la firma che il trasporto OpenRouter
305
+ * usa già, con l'errore che QUESTO strato deve produrre (`OpenRouterIdleTimeoutError` → 408).
306
+ * ⛔ Due guardiani con due corpi diversi divergono al primo tocco: uno solo, e iniettabile.
307
+ */
308
+ function promessaConInattivita(promise, { timeoutMs, controller, userSignal, creaErrore }) {
309
+ return sorvegliaInattivita(promise, {
310
+ limiteMs: timeoutMs,
311
+ controller,
312
+ userSignal,
313
+ creaErrore: creaErrore ?? ((ms) => new OpenRouterIdleTimeoutError(ms)),
314
+ });
315
+ }
316
+
317
+ function eventoConOutput(packet) {
318
+ const delta = packet?.choices?.[0]?.delta;
319
+ if (!delta || typeof delta !== 'object') return false;
320
+ return Boolean(delta.content || delta.reasoning || delta.reasoning_content || (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0));
321
+ }
322
+
323
+ /*
324
+ * ⛔ P0 · punto 7 (16/09/2026): qui il limite NON è più `timeoutMs` (il tempo del fornitore) ma
325
+ * `inattivitaMs`, il failsafe di generazione. Dopo gli header il tempo del fornitore ha finito il
326
+ * suo mestiere; da lì in poi conta solo da quanto tempo il canale TACE. Il conteggio si azzera a
327
+ * ogni `reader.read()` che porta byte — commenti SSE compresi, che infatti il parser riemette.
328
+ */
329
+ async function preparaRispostaSse(response, { inattivitaMs, controller, userSignal }) {
330
+ if (!response.body || typeof response.body.getReader !== 'function') return response;
331
+ const reader = response.body.getReader();
332
+ const decoder = new TextDecoder();
333
+ const encoder = new TextEncoder();
334
+ const pending = [];
335
+ let outputVisibile = false;
336
+ let earlyError = null;
337
+ let streamController = null;
338
+ let streamError = null;
339
+ let done = false;
340
+
341
+ const emetti = (text) => {
342
+ const bytes = encoder.encode(text);
343
+ if (streamController) streamController.enqueue(bytes);
344
+ else pending.push(bytes);
345
+ };
346
+ const parser = createParser({
347
+ maxBufferSize: SSE_BUFFER_MASSIMO,
348
+ onComment(comment) {
349
+ emetti(`:${comment}\n\n`);
350
+ },
351
+ onEvent(event) {
352
+ if (event.data === '[DONE]') {
353
+ emetti('data: [DONE]\n\n');
354
+ return;
355
+ }
356
+ let packet;
357
+ try { packet = JSON.parse(event.data); } catch {
358
+ emetti(`data: ${event.data}\n\n`);
359
+ return;
360
+ }
361
+ if (packet?.error) {
362
+ if (!outputVisibile) earlyError = packet.error;
363
+ else streamError = new OpenRouterStreamError(packet.error);
364
+ return;
365
+ }
366
+ if (eventoConOutput(packet)) outputVisibile = true;
367
+ emetti(`data: ${event.data}\n\n`);
368
+ },
369
+ onError(error) {
370
+ streamError = error;
371
+ },
372
+ });
373
+
374
+ const leggi = async () => {
375
+ const result = await promessaConInattivita(reader.read(), {
376
+ timeoutMs: inattivitaMs, controller, userSignal,
377
+ creaErrore: (ms) => new SilenzioDelFornitoreError(ms),
378
+ });
379
+ if (result.done) {
380
+ done = true;
381
+ parser.reset({ consume: true });
382
+ return;
383
+ }
384
+ parser.feed(decoder.decode(result.value, { stream: true }));
385
+ };
386
+
387
+ try {
388
+ while (!outputVisibile && !earlyError && !streamError && !done) await leggi();
389
+ } catch (error) {
390
+ await reader.cancel(error).catch(() => {});
391
+ if (userSignal?.aborted) throw userSignal.reason ?? error;
392
+ /* ⛔ Il silenzio NON si traveste da risposta HTTP: deve arrivare a `classificaGuasto` col suo
393
+ codice, o diventerebbe un «502» generico e perderebbe la classe `rete` che lo rende ripreso. */
394
+ if (error instanceof SilenzioDelFornitoreError) throw error;
395
+ if (error instanceof OpenRouterIdleTimeoutError) return rispostaErrore(408, { message: 'OpenRouter è rimasto inattivo oltre il limite configurato.' });
396
+ return rispostaErrore(502, { message: error instanceof Error ? error.message : String(error) });
397
+ }
398
+
399
+ if (earlyError) {
400
+ await reader.cancel(new OpenRouterStreamError(earlyError)).catch(() => {});
401
+ return rispostaErrore(statusPerErroreStream(earlyError), earlyError);
402
+ }
403
+ if (streamError && !outputVisibile) {
404
+ await reader.cancel(streamError).catch(() => {});
405
+ return rispostaErrore(502, { message: streamError.message });
406
+ }
407
+
408
+ const body = new ReadableStream({
409
+ start(controllerOut) {
410
+ streamController = controllerOut;
411
+ for (const bytes of pending.splice(0)) controllerOut.enqueue(bytes);
412
+ if (streamError) {
413
+ controllerOut.error(streamError);
414
+ return;
415
+ }
416
+ if (done) {
417
+ controllerOut.close();
418
+ return;
419
+ }
420
+ void (async () => {
421
+ try {
422
+ while (!done) {
423
+ await leggi();
424
+ if (streamError) throw streamError;
425
+ }
426
+ controllerOut.close();
427
+ } catch (error) {
428
+ await reader.cancel(error).catch(() => {});
429
+ controllerOut.error(userSignal?.aborted ? (userSignal.reason ?? error) : error);
430
+ }
431
+ })();
432
+ },
433
+ cancel(reason) {
434
+ controller.abort(reason);
435
+ return reader.cancel(reason);
436
+ },
437
+ });
438
+ return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
439
+ }
440
+
441
+ /**
442
+ * Trasporto OpenRouter desktop: sostituisce il timeout totale hard-coded del
443
+ * runtime owner con un limite di inattività osservabile sullo stream SSE.
444
+ * Il fetch resta provider-specifico e confinato in questo adapter.
445
+ */
446
+ export function creaFetchOpenRouterResiliente(fetchDiRete = fetch, {
447
+ timeoutMsFn = () => OPENROUTER_IDLE_MS_PREDEFINITO,
448
+ /*
449
+ * ⛔ P0 · punto 7 (16/09/2026) — DUE tempi, non uno. `timeoutMsFn` è il tempo del fornitore e
450
+ * vale fino agli header; `inattivitaMsFn` è il failsafe di generazione e vale sul flusso. Prima
451
+ * erano lo stesso numero, e bastava che l'operatore scrivesse 60 s nella scheda Fornitori perché
452
+ * un ragionamento lungo di OpenRouter morisse dopo un minuto di silenzio legittimo.
453
+ */
454
+ inattivitaMsFn = () => leggiInattivitaGenerazioneMs(),
455
+ modelCapabilityFn = async () => null,
456
+ userSignal = null,
457
+ } = {}) {
458
+ if (typeof fetchDiRete !== 'function') throw new TypeError('fetchDiRete deve essere una funzione.');
459
+ return async (url, init = undefined) => {
460
+ if (!urlOpenRouterChat(url)) return fetchDiRete(url, init);
461
+ const timeoutCandidate = Number(await timeoutMsFn());
462
+ const timeoutMs = Number.isFinite(timeoutCandidate) && timeoutCandidate > 0
463
+ ? Math.max(1, Math.round(timeoutCandidate))
464
+ : OPENROUTER_IDLE_MS_PREDEFINITO;
465
+ const inattivitaCandidata = Number(await inattivitaMsFn());
466
+ const inattivitaMs = Number.isFinite(inattivitaCandidata) && inattivitaCandidata >= 0
467
+ ? Math.round(inattivitaCandidata)
468
+ : leggiInattivitaGenerazioneMs();
469
+ let nextInit = init;
470
+ if (typeof init?.body === 'string') {
471
+ try {
472
+ const body = JSON.parse(init.body);
473
+ const capability = typeof body?.model === 'string'
474
+ ? await Promise.resolve(modelCapabilityFn(body.model)).catch(() => null)
475
+ : null;
476
+ const reasoning = normalizzaReasoningPerModello(body?.reasoning, capability);
477
+ if (reasoning !== body?.reasoning) nextInit = { ...init, body: JSON.stringify({ ...body, reasoning }) };
478
+ } catch {
479
+ // Body non JSON: il confine Fetch resta trasparente.
480
+ }
481
+ }
482
+ const controller = new AbortController();
483
+ try {
484
+ /* Fino agli header comanda il tempo del FORNITORE; dal corpo in poi, il failsafe. */
485
+ const response = await promessaConInattivita(
486
+ fetchDiRete(url, { ...nextInit, signal: controller.signal, ...dispatcherDiRichiesta(inattivitaMs) }),
487
+ { timeoutMs, controller, userSignal },
488
+ );
489
+ const contentType = response.headers?.get?.('content-type') ?? '';
490
+ if (!response.ok || !contentType.toLowerCase().includes('text/event-stream')) return response;
491
+ return preparaRispostaSse(response, { inattivitaMs, controller, userSignal });
492
+ } catch (error) {
493
+ if (userSignal?.aborted) throw userSignal.reason ?? error;
494
+ if (error instanceof SilenzioDelFornitoreError) throw error;
495
+ if (error instanceof OpenRouterIdleTimeoutError) return rispostaErrore(408, { message: 'OpenRouter è rimasto inattivo oltre il limite configurato.' });
496
+ return rispostaErrore(502, { message: error instanceof Error ? error.message : String(error) });
497
+ }
498
+ };
499
+ }
500
+
501
+ /**
502
+ * @param {{modulePath?:string|null, importFn?:Function}} [options]
503
+ */
504
+
505
+ /**
506
+ * ⭐⭐⭐ 03/9 — I MODELLI DI OGNI PROVIDER, FATTI GIRARE DAVVERO.
507
+ *
508
+ * Owner: «se non riesco ad aggiungere più provider oltre a OpenRouter e
509
+ * soprattutto usare i modelli locali, l'applicazione è spacciata». E, sulla
510
+ * mia risposta precedente: «non è un limite quello che mi hai detto tu, è un
511
+ * finto limite». Aveva ragione.
512
+ *
513
+ * ## Perché QUI e non nel kernel
514
+ *
515
+ * Il kernel ha UNA riga cablata su OpenRouter — misurato: riga 406 della copia
516
+ * che il desktop carica, 392 di quella mobile. Ma prende `fetchDiRete` come
517
+ * dipendenza, e questo adattatore gliela costruisce già a strati
518
+ * (`creaFetchConDescrizioneComando` → `creaFetchOpenRouterResiliente`).
519
+ *
520
+ * ⇒ Il varco giusto era già lì. Il kernel dice «fai un completamento per il
521
+ * modello X»; DOVE vive X è una decisione del trasporto, non sua. Così:
522
+ * · zero righe modificate nei kernel — e sono DUE file diversi, 3.203 e
523
+ * 6.226 righe, in due repository, che divergerebbero al primo tocco;
524
+ * · zero collisione con la sessione mobile, che sullo stesso file sta
525
+ * lavorando in queste ore;
526
+ * · un posto solo da provare, in questo repository.
527
+ *
528
+ * ## Cosa fa, esattamente
529
+ *
530
+ * Guarda il `model` del corpo uscente. Senza prefisso di fonte non tocca
531
+ * NIENTE — la richiesta parte come è sempre partita, e nessuna sessione
532
+ * esistente cambia comportamento. Con un prefisso (`local:`, `ollama:`,
533
+ * `openai:`, `deepseek:`) riscrive indirizzo e intestazioni, e rimette nel
534
+ * corpo il nome vero del modello senza prefisso: il provider non deve sapere
535
+ * niente della nostra convenzione.
536
+ *
537
+ * ⛔ Se la fonte non è servibile — chiave mancante, motore locale spento,
538
+ * provider che vuole un altro formato — NON parte nessuna richiesta: si
539
+ * solleva l'errore con il motivo vero. Partire e prendersi un 404 farebbe
540
+ * sembrare rotta una credenziale che è buona.
541
+ */
542
+ /*
543
+ * ⛔ 17/09 — `sorvegliaCorpo` è INIETTABILE, e non per comodità: due contratti veri si contraddicono
544
+ * per costruzione. Il guardiano dell'inattività (P0 · punto 7) deve VEDERE i byte che passano, e
545
+ * l'unico modo che la piattaforma dà è `body.pipeThrough(...)` dentro una `Response` nuova — il
546
+ * corpo è un getter di sola lettura e il piping «locks the stream for the duration of the pipe»
547
+ * (MDN, «ReadableStream: pipeThrough() method» e «Using readable streams», lette il 17/09/2026).
548
+ * Lo strato della cache (P-B) promette invece che un flusso SSE o un errore escano «LA STESSA
549
+ * risposta, non una ricostruita» (PG-12, CACHE-08): quel contratto vale per la CACHE, che non deve
550
+ * rimontare un flusso che non ha prodotto — non per il guardiano, che lo attraversa intatto
551
+ * (P0-D-20: stessi byte, stesso stato, stessi header). ⇒ Le prove della cache iniettano il
552
+ * guardiano identità e misurano il loro contratto; il guardiano vero si prova da solo (P0-D-15/16).
553
+ */
554
+ function creaFetchInstradata(fetchDiRete = fetch, { risolvi = risolviDestinazioneModello, dipendenze = null, onAvviso = null, instradaOpenRouter = false, inattivitaGenerazioneMs = null, sorvegliaCorpo = sorvegliaCorpoDiGenerazione } = {}) {
555
+ if (!dipendenze) return fetchDiRete;
556
+ return async function fetchMultiProvider(url, opzioni = {}) {
557
+ let corpo = null;
558
+ try {
559
+ corpo = typeof opzioni.body === 'string' ? JSON.parse(opzioni.body) : null;
560
+ } catch {
561
+ corpo = null;
562
+ }
563
+ /*
564
+ * ⛔ Si interviene solo su una richiesta di completamento riconoscibile:
565
+ * il kernel usa questa stessa fetch anche per la ricerca web e per gli
566
+ * attrezzi, e dirottare quelle sarebbe un guasto silenzioso.
567
+ */
568
+ if (!corpo || typeof corpo.model !== 'string' || !String(url).includes('/chat/completions')) {
569
+ return fetchDiRete(url, opzioni);
570
+ }
571
+ /*
572
+ * ⭐⭐⭐ 3/9 — owner, dal vivo: «[internal-error] Il motore locale non è
573
+ * acceso: caricalo dal Laboratorio modelli prima di usarlo in chat…
574
+ * non è così che si deve fare». Ricerca fatta (LM Studio: JIT loading,
575
+ * "you don't need to manually load the model first… it'll be loaded
576
+ * before your request returns", ON di default dalle nuove
577
+ * installazioni; Ollama: "the platform loads the specified model into
578
+ * memory" alla prima richiesta, nessun passo separato — c'è ancora chi
579
+ * non lo risolve e richiede Ollama configurato a mano: qui lo
580
+ * battiamo). Owner: «deve partire tutto in automatico, anche con un
581
+ * loading nella chat o qualcosa del genere».
582
+ *
583
+ * ⇒ Nessun nuovo canale di eventi per il "loading": questa fetch è già
584
+ * dentro la richiesta di completamento che il kernel sta aspettando —
585
+ * la ruota "in attesa di risposta" che la chat mostra già copre
586
+ * l'attesa dell'avvio, non serve altro. Un solo tentativo di avvio
587
+ * automatico, poi si riprova UNA volta sola: se fallisce anche dopo
588
+ * l'avvio, l'errore vero (disco pieno, GGUF corrotto…) deve arrivare
589
+ * all'utente, non un secondo giro silenzioso all'infinito.
590
+ */
591
+ let destinazione;
592
+ try {
593
+ destinazione = risolvi(corpo.model, dipendenze);
594
+ } catch (erroreRisoluzione) {
595
+ if (erroreRisoluzione?.code !== 'LOCAL_RUNTIME_NOT_READY' || typeof dipendenze.avviaLocale !== 'function') throw erroreRisoluzione;
596
+ const { modelloRemoto } = separaFonteModello(corpo.model);
597
+ await dipendenze.avviaLocale(modelloRemoto); // ⛔ se l'avvio stesso fallisce, il SUO errore (non quello generico "non acceso") arriva a chi ha chiamato
598
+ destinazione = risolvi(corpo.model, dipendenze); // dopo un avvio riuscito questo non deve più lanciare: se lancia ancora, è un errore vero da mostrare, non da inghiottire
599
+ }
600
+ /*
601
+ * ⛔⛔⛔ P0 · punto 7 (16/09/2026) — IL FAILSAFE, IN UN PUNTO SOLO PER TUTTI I FORNITORI.
602
+ *
603
+ * Dichiarato QUI, sopra tutti i rami, perché è il solo confine che vede ogni destinazione di un
604
+ * completamento: gli SDK nativi, i cloud, deepseek/z.ai/openai, e soprattutto il motore LOCALE,
605
+ * che passa dal ponte del supervisore e non da una `fetch` nuda (quindi nessun `bodyTimeout` di
606
+ * undici lo coprirebbe). Una regola sola invece di cinque copie che divergono.
607
+ *
608
+ * ⛔ DUE eccezioni, ed è giusto dirle per nome invece di lasciar credere che non ci siano:
609
+ * · **OpenRouter** — il suo trasporto resiliente sorveglia già il flusso e ne riemette i
610
+ * commenti; una seconda guardia sopra la prima non aggiunge niente e raddoppierebbe i
611
+ * lettori sullo stesso corpo.
612
+ * · **l'agente esterno ACP** — non è un flusso di byte HTTP ma un protocollo a messaggi con
613
+ * la sua cancellazione e la sua scadenza (`acp-agent.mjs`). Trasformarla in inattività
614
+ * vuole toccare il ciclo delle notifiche, che è fuori da questa corsia.
615
+ *
616
+ * ⛔ Il conteggio si azzera sui BYTE, non sui token: i commenti SSE contano come vita.
617
+ */
618
+ const failsafe = Number.isFinite(inattivitaGenerazioneMs) ? inattivitaGenerazioneMs : leggiInattivitaGenerazioneMs();
619
+ const sorveglia = (risposta) => (destinazione.fonte === 'openrouter'
620
+ ? risposta
621
+ : sorvegliaCorpo(risposta, { limiteMs: failsafe, userSignal: opzioni.signal ?? null }));
622
+ const conDispatcher = dispatcherDiRichiesta(failsafe);
623
+
624
+ // P-L · il corpo del kernel incontra ACP solo qui; stop e chiusura seguono la risposta.
625
+ if (destinazione.esterno) return rispostaAgenteAcp({ runtime: destinazione.runtime, body: corpo, signal: opzioni.signal });
626
+ // P-L · fine instradamento agente esterno.
627
+ if (destinazione.native) return sorveglia(await nativeProviderResponse({ provider: destinazione.fonte, model: destinazione.modelloRemoto, apiKey: destinazione.apiKey, baseURL: destinazione.baseURL, body: corpo, fetchFn: fetchDiRete, signal: opzioni.signal }));
628
+ if (corpo.messages?.some(m => m.talos_provider_state)) {
629
+ corpo = { ...corpo, messages: stripNativeMetadata(corpo.messages) };
630
+ opzioni = { ...opzioni, body: JSON.stringify(corpo) };
631
+ }
632
+ if (destinazione.fonte === 'openrouter' && !instradaOpenRouter) return fetchDiRete(url, opzioni);
633
+ if (destinazione.fonte === 'openai' && corpo.reasoning) {
634
+ const { reasoning, ...resto } = corpo;
635
+ corpo = { ...resto, ...(typeof reasoning.effort === 'string' ? { reasoning_effort: reasoning.effort } : {}) };
636
+ }
637
+ /* P-D (12/09, Astra): il traduttore del fornitore toglie ciò che il fornitore non accetta (es.
638
+ `reasoning_effort: low` per Z.AI, che ammette solo high e max) e lo DICE. Senza un canale per
639
+ dirlo (`onAvviso`) si preferisce fermarsi prima della rete con una frase in italiano, invece di
640
+ spedire in silenzio una richiesta diversa da quella chiesta (fail-closed, come nel rapporto). */
641
+ const adattata = preparaRichiestaCompatibile(destinazione.fonte, { ...corpo, model: destinazione.modelloRemoto });
642
+ for (const avviso of adattata.avvisi) {
643
+ if (typeof onAvviso !== 'function') throw new OpenAiCompatibleRuntimeError(avviso, 'PROVIDER_REASONING_UNSUPPORTED');
644
+ await onAvviso(avviso);
645
+ }
646
+ const corpoRiscritto = JSON.stringify(adattata.corpo);
647
+ /*
648
+ * ⛔ Il motore locale si chiama attraverso il SUO supervisore, non con una
649
+ * fetch nuda: llama-server parte con `--api-key randomBytes(32)` e quella
650
+ * chiave vive solo dentro il supervisore (`status()` non la espone,
651
+ * perché quella risposta arriva al browser). Misurato costruendo l'URL a
652
+ * mano: HTTP 401 «Invalid API Key» in 4 ms.
653
+ */
654
+ if (destinazione.locale) {
655
+ if (typeof dipendenze.chiamaLocale !== 'function') {
656
+ const errore = new Error('Il motore locale non è collegato a questo server.');
657
+ errore.code = 'LOCAL_RUNTIME_NOT_READY';
658
+ throw errore;
659
+ }
660
+ return sorveglia(await dipendenze.chiamaLocale(destinazione.percorso, { ...opzioni, ...conDispatcher, headers: { 'Content-Type': 'application/json' }, body: corpoRiscritto }));
661
+ }
662
+ /*
663
+ * ⛔⛔⛔ 16/09/2026, GIRO DI RIPARAZIONE — L'ORDINE DI QUESTE DUE FUNZIONI È LA CURA.
664
+ *
665
+ * Prima era `sorveglia(conCacheDichiarata(await fetch(...)))`, e aveva DUE difetti in una riga:
666
+ * 1. `conCacheDichiarata` è `async` ⇒ `sorveglia` riceveva una **Promise**, non una
667
+ * `Response`, e la restituiva intatta: il failsafe non era attaccato a niente su
668
+ * deepseek / z.ai / openai / cloud, in streaming e non (ora `sorvegliaCorpoDiGenerazione`
669
+ * LANCIA se gli si passa una Promise, così non può più succedere in silenzio);
670
+ * 2. anche con l'`await` al posto giusto, `conCacheDichiarata` legge il corpo
671
+ * (`risposta.clone().text()`) per dichiarare la cache: sorvegliare DOPO vuol dire
672
+ * sorvegliare un corpo già bevuto, e su una risposta `stream:false` che si pianta a metà
673
+ * JSON quella lettura non finisce mai.
674
+ *
675
+ * ⇒ Si sorveglia PRIMA e si dichiara la cache DOPO: `conCacheDichiarata` clona un corpo già
676
+ * sorvegliato, quindi anche la sua lettura è coperta. Misurato il 16/09/2026 con un fornitore
677
+ * che manda gli header e poi tace: prima **1506 ms** e sempre `UND_ERR_BODY_TIMEOUT` (cioè il
678
+ * trasporto a 1,2×, mai il guardiano) o nessuna uscita affatto senza dispatcher; dopo
679
+ * `PROVIDER_SILENCE` al limite chiesto. Prove `P0-D-15`/`P0-D-16`/`P0-D-17`.
680
+ */
681
+ // P-K — nessun redirect con credenziali cloud, anche senza pool collegato.
682
+ if (destinazione.cloud) return conCacheDichiarata(sorveglia(await inviaCloudProtetta(fetchDiRete, destinazione.url, {
683
+ ...opzioni, ...conDispatcher, headers: { ...destinazione.headers }, body: corpoRiscritto, redirect: 'error',
684
+ })), destinazione.fonte);
685
+ // P-K — fine
686
+ return conCacheDichiarata(sorveglia(await fetchDiRete(destinazione.url, {
687
+ ...opzioni,
688
+ ...conDispatcher,
689
+ headers: { ...destinazione.headers },
690
+ body: corpoRiscritto,
691
+ })), destinazione.fonte);
692
+ };
693
+ }
694
+
695
+ // P-K — errori upstream non attendibili: il testo non deve attraversare il confine pubblico.
696
+ async function inviaCloudProtetta(rete, url, opzioni) {
697
+ let risposta;
698
+ try { risposta = await rete(url, opzioni); }
699
+ catch {
700
+ if (opzioni.signal?.aborted) {
701
+ const errore = new Error('La richiesta al fornitore è stata interrotta.');
702
+ errore.name = opzioni.signal.reason?.name === 'TimeoutError' ? 'TimeoutError' : 'AbortError';
703
+ throw errore;
704
+ }
705
+ throw Object.assign(new Error('Non è stato possibile raggiungere il fornitore.'), { code: 'PROVIDER_NETWORK_ERROR' });
706
+ }
707
+ if (risposta.ok) return risposta;
708
+ await risposta.body?.cancel().catch(() => {});
709
+ const stato = risposta.status;
710
+ const message = stato === 401 ? 'Credenziale non accettata dal fornitore.'
711
+ : stato === 403 ? 'Accesso negato: controlla i permessi della credenziale e del modello.'
712
+ : stato === 404 ? 'Modello o indirizzo non trovato: controlla la configurazione del fornitore.'
713
+ : `Il fornitore ha risposto HTTP ${stato}.`;
714
+ return new Response(JSON.stringify({ error: { message } }), { status: stato, headers: { 'Content-Type': 'application/json' } });
715
+ }
716
+ // P-K — fine
717
+
718
+ function erroreFornitorePubblico(classificazione, stato = null) {
719
+ const messaggi = {
720
+ traffico: 'Troppo traffico presso il fornitore.', credenziale: 'Credenziale rifiutata dal fornitore.',
721
+ credito: 'Credito non disponibile presso il fornitore.', rete: 'Connessione con il fornitore interrotta.',
722
+ 'timeout-fornitore': 'Il fornitore ha superato il tempo massimo.', 'guasto-fornitore': 'Il fornitore non risponde.',
723
+ 'flusso-interrotto': 'La risposta del fornitore si è interrotta.',
724
+ };
725
+ const e = new Error(messaggi[classificazione.classe] ?? 'Il fornitore non ha accettato la richiesta.');
726
+ return Object.assign(e, { code: 'PROVIDER_REQUEST_ERROR', stato, ...classificazione, limitatoDalFornitore: classificazione.classe === 'traffico' });
727
+ }
728
+
729
+ /**
730
+ * ⛔⛔ P0 · punto 7 (16/09/2026) — LA SCADENZA ALLA PRIMA RISPOSTA.
731
+ *
732
+ * Un `AbortSignal.timeout` non si può disarmare: una volta acceso conta fino in fondo, e dopo gli
733
+ * header continua a contare sul corpo. Qui il timer è NOSTRO, e si spegne appena la risposta
734
+ * arriva — è la differenza fra «il fornitore non risponde» (un guasto vero) e «il modello sta
735
+ * pensando» (il lavoro).
736
+ *
737
+ * ⛔ Il segnale composto resta quello passato dal chiamante: lo STOP della persona continua ad
738
+ * attraversare la fetch e il corpo, esattamente come prima.
739
+ * ⛔ Il motivo dell'aborto porta un `code` PROPRIO invece di affidarsi alla parola «timeout»
740
+ * dentro un messaggio: un filtro che riconosce la menzione non riconosce la cosa.
741
+ *
742
+ * @param {number|undefined} timeoutSeconds
743
+ * @param {AbortSignal|undefined} segnaleUtente
744
+ */
745
+ function scadenzaPrimaRisposta(timeoutSeconds, segnaleUtente) {
746
+ const ms = Number(timeoutSeconds) > 0 ? Math.round(Number(timeoutSeconds) * 1_000) : 0;
747
+ if (!ms) return { signal: segnaleUtente, disarma: () => {} };
748
+ const controllore = new AbortController();
749
+ const timer = setTimeout(() => controllore.abort(Object.assign(
750
+ new Error(`Il fornitore non ha risposto entro ${Math.round(ms / 1_000)} secondi.`),
751
+ { name: 'TimeoutError', code: 'PROVIDER_FIRST_RESPONSE_TIMEOUT' },
752
+ )), ms);
753
+ timer.unref?.();
754
+ return {
755
+ signal: segnaleUtente ? AbortSignal.any([segnaleUtente, controllore.signal]) : controllore.signal,
756
+ disarma: () => clearTimeout(timer),
757
+ };
758
+ }
759
+
760
+ /**
761
+ * ⛔ I guasti del TRASPORTO hanno un codice, e il codice si legge per primo.
762
+ *
763
+ * `classificaErroreDiCorsa` (BC-44) resta l'unica tabella, ma legge il MESSAGGIO: i suoi segni sono
764
+ * in inglese (`idle timeout`, `terminated`…) e non possono riconoscere né il nostro silenzio né i
765
+ * codici di undici. Indovinare dal testo sarebbe la «cura che passa da un filtro di menzione».
766
+ * ⭐ `PROVIDER_SILENCE` e `UND_ERR_BODY_TIMEOUT` sono `rete` — «la connessione con il fornitore è
767
+ * caduta» — e NON `timeout-fornitore`: quando scattano il canale è morto, non lento.
768
+ */
769
+ const CLASSI_PER_CODICE_DI_TRASPORTO = new Map([
770
+ ['PROVIDER_SILENCE', 'rete'],
771
+ ['UND_ERR_BODY_TIMEOUT', 'rete'],
772
+ ['UND_ERR_HEADERS_TIMEOUT', 'timeout-fornitore'],
773
+ ['PROVIDER_FIRST_RESPONSE_TIMEOUT', 'timeout-fornitore'],
774
+ ]);
775
+
776
+ /**
777
+ * ⛔ 16/09/2026 — i due modi in cui un corpo può NON FINIRE MAI, per nome.
778
+ *
779
+ * `PROVIDER_SILENCE` è il nostro failsafe di inattività; `UND_ERR_BODY_TIMEOUT` è la rete di
780
+ * sicurezza del trasporto (undici: un timer fra un chunk e il successivo, 300 s di serie —
781
+ * documentazione `Client.md`, letta il 16/09/2026). Chi legge un corpo per misurarlo deve
782
+ * RILANCIARE questi due invece di degradare, o il guasto arriva senza il suo nome.
783
+ */
784
+ const CODICI_DI_CORPO_MAI_FINITO = new Set(['PROVIDER_SILENCE', 'UND_ERR_BODY_TIMEOUT']);
785
+
786
+ function classificaGuasto(error, stato = null) {
787
+ // BC-44 rimane l'unica tabella. Si normalizzano solo i campi strutturati del trasporto.
788
+ const codice = String(error?.code ?? error?.cause?.code ?? '');
789
+ /*
790
+ * ⛔ 16/09/2026 — `causaDiTrasporto` VIAGGIA fino all'errore pubblico, e non è un dettaglio da
791
+ * collezionisti: `PROVIDER_SILENCE` e `UND_ERR_BODY_TIMEOUT` producono la stessa classe
792
+ * (`rete`) e la stessa frase in chat, ma sono DUE STRATI diversi — il nostro guardiano a 1,0×
793
+ * il failsafe, il trasporto a 1,2×. Senza questo campo, chi legge un registro (o un test) non
794
+ * può distinguere «la mia guardia ha funzionato» da «la mia guardia era staccata e mi ha
795
+ * salvato undici»: è esattamente l'inganno in cui questa corsia è caduta al primo giro.
796
+ */
797
+ if (CLASSI_PER_CODICE_DI_TRASPORTO.has(codice)) return { classe: CLASSI_PER_CODICE_DI_TRASPORTO.get(codice), transitorio: true, causaDiTrasporto: codice };
798
+ const messaggio = `${codice} ${error?.name ?? ''} ${error?.message ?? ''} ${error?.cause?.message ?? ''} ${stato ? `HTTP ${stato}` : ''}`;
799
+ const esito = classificaErroreDiCorsa({ codice, messaggio });
800
+ if (esito.classe !== 'ignoto' || !(stato >= 500 && stato <= 599)) return esito;
801
+ return classificaErroreDiCorsa({ messaggio: 'upstream error' });
802
+ }
803
+
804
+ function consumoPubblico(usage) {
805
+ if (!usage || typeof usage !== 'object' || Array.isArray(usage)) return null;
806
+ const risultato = {};
807
+ const copiaNumeri = (da, campi) => Object.fromEntries(campi.filter(k => typeof da?.[k] === 'number' && Number.isFinite(da[k]) && da[k] >= 0).map(k => [k, da[k]]));
808
+ Object.assign(risultato, copiaNumeri(usage, ['prompt_tokens','completion_tokens','total_tokens','input_tokens','output_tokens','cost','cache_read_input_tokens','cache_creation_input_tokens','prompt_cache_hit_tokens','prompt_cache_miss_tokens']));
809
+ for (const [campo, campi] of Object.entries({prompt_tokens_details:['cached_tokens','cache_write_tokens'],completion_tokens_details:['reasoning_tokens']})) {
810
+ const v = copiaNumeri(usage[campo], campi); if (Object.keys(v).length) risultato[campo] = v;
811
+ }
812
+ if (typeof usage.cache_discount === 'number' && Number.isFinite(usage.cache_discount)) risultato.cache_discount = usage.cache_discount;
813
+ return Object.keys(risultato).length ? risultato : null;
814
+ }
815
+
816
+ /** P-H: la fetch conosce la chiave; il kernel resta proprietario dei ritentativi.
817
+ * eseguiConFallback avvolge UNA chiamata del kernel, mai il ciclo degli attrezzi.
818
+ * I callback del cambio e del consumo devono essere durabili prima della nuova chiamata.
819
+ */
820
+ export function creaFetchMultiProvider(fetchDiRete = fetch, {
821
+ risolvi = risolviDestinazioneModello, dipendenze = null, onAvviso = null,
822
+ providerStore = null, fallbackProviders = [], onCambioFornitore = null, onConsumoFornitore = null,
823
+ modelloSessione = null,
824
+ /* P0 · punto 7 (16/09): iniettabile perché una prova non deve aspettare mezz'ora per provarla. */
825
+ inattivitaGenerazioneMs = null,
826
+ /* 17/09: il guardiano del corpo, iniettabile per le prove dello strato cache (vedi `creaFetchInstradata`). */
827
+ sorvegliaCorpo = sorvegliaCorpoDiGenerazione,
828
+ } = {}) {
829
+ const catena = validaFallbackProviders(fallbackProviders);
830
+ if (!providerStore && !catena.length) return creaFetchInstradata(fetchDiRete, { risolvi, dipendenze, onAvviso, inattivitaGenerazioneMs, sorvegliaCorpo });
831
+ if (!dipendenze || (catena.length && (!providerStore || typeof onCambioFornitore !== 'function' || typeof onConsumoFornitore !== 'function'))) {
832
+ throw new OwnerRuntimeUnavailableError('Per continuare con un altro fornitore occorrono accessi, avvisi in chat e registrazione dei consumi.', 'PROVIDER_FALLBACK_NOT_CONNECTED');
833
+ }
834
+ let effettivo = null, indice = -1, occupato = false;
835
+
836
+ async function invia(url, opzioni = {}, contesto = {}) {
837
+ let corpo;
838
+ try { corpo = typeof opzioni.body === 'string' ? JSON.parse(opzioni.body) : null; } catch { /* altre fetch intatte */ }
839
+ if (!corpo || typeof corpo.model !== 'string' || !String(url).includes('/chat/completions')) return fetchDiRete(url, opzioni);
840
+ opzioni.signal?.throwIfAborted();
841
+ const { fonte } = separaFonteModello(corpo.model);
842
+ const record = REGISTRO_FORNITORI[fonte];
843
+ const scelta = record.credenziale ? providerStore?.scegliChiave(fonte) : null;
844
+ if (providerStore && !scelta && record.chiaveObbligatoria) {
845
+ if (!providerStore.hasKey(fonte)) throw new OwnerRuntimeUnavailableError('Manca la chiave del fornitore scelto.', 'PROVIDER_KEY_MISSING');
846
+ const panchina = providerStore.elencaPool(fonte).find(v => v.causa);
847
+ const classificazione = classificaErroreDiCorsa({ messaggio: ({traffico:'HTTP 429',credenziale:'HTTP 401',credito:'insufficient credit',rete:'network', 'timeout-fornitore':'timeout', 'guasto-fornitore':'upstream error', 'flusso-interrotto':'unexpected eof'})[panchina?.causa] ?? '' });
848
+ contesto.errore = erroreFornitorePubblico(classificazione, classificazione.classe === 'traffico' ? 429 : classificazione.transitorio ? 503 : 401);
849
+ return new Response(contesto.errore.message, { status: contesto.errore.stato });
850
+ }
851
+ contesto.errore = null;
852
+ contesto.scelta = scelta; contesto.fonte = fonte;
853
+ const segnala = async (classificazione, headers, stato) => {
854
+ contesto.errore = erroreFornitorePubblico(classificazione, stato);
855
+ if (scelta) providerStore.mettiInPanchina(fonte, scelta.impronta, { classe: classificazione.classe, headers });
856
+ if (classificazione.classe === 'credenziale' && typeof onAvviso === 'function') {
857
+ await onAvviso(`Una chiave di ${record.etichetta} è stata rifiutata: controlla Fornitori e accessi.`);
858
+ }
859
+ };
860
+ const rete = async (target, init) => {
861
+ try {
862
+ /*
863
+ * ⛔⛔⛔ P0 · punto 7 (16/09/2026) — QUI C'ERA UNA DEADLINE TOTALE, ED È STATA TOLTA.
864
+ *
865
+ * Prima: `AbortSignal.timeout(timeoutSeconds * 1000)` composto con `init.signal` e passato
866
+ * alla fetch. Quel segnale non smette di contare quando la risposta arriva: continua
867
+ * mentre il modello STA PARLANDO, e al minuto esatto uccide lo stream.
868
+ * Misurato il 16/09/2026 con un fornitore finto che emette un token ogni 2 s per 90 s, col
869
+ * default di 60 s: **tagliata a 60.002 ms, ultimo token a 58.023 ms** — cioè il canale era
870
+ * vivo due secondi prima, e l'errore diceva «Il fornitore ha superato il tempo massimo».
871
+ * Verso OpenRouter non si vedeva (il trasporto resiliente scarta `init.signal`), verso
872
+ * deepseek / z.ai / openai / cloud / motore LOCALE sì.
873
+ *
874
+ * ⇒ `timeoutSeconds` CAMBIA SEMANTICA, non sparisce: è il tempo massimo alla **prima
875
+ * risposta** — cioè fino agli header. Un valore salvato ieri continua a proteggere dal
876
+ * fornitore che non risponde affatto, e non può più tagliare un ragionamento in corso.
877
+ * Dopo gli header comanda il solo failsafe di INATTIVITÀ (`generation-idle.mjs`).
878
+ */
879
+ const scadenza = scadenzaPrimaRisposta(providerStore?.getRuntime(fonte)?.timeoutSeconds, init.signal);
880
+ let response;
881
+ try { response = await fetchDiRete(target, { ...init, signal: scadenza.signal }); }
882
+ finally { scadenza.disarma(); }
883
+ if (response.ok) return response;
884
+ // Il corpo originale non viene mai restituito al logger/kernel: può contenere la chiave.
885
+ let testo = '';
886
+ try { testo = (await response.text()).slice(0, 16_384); } catch { /* lo stato resta disponibile */ }
887
+ const classificazione = classificaGuasto({ message: testo }, response.status);
888
+ await segnala(classificazione, response.headers, response.status);
889
+ return new Response(JSON.stringify({ error: { message: contesto.errore.message } }), { status: response.status, headers: { 'Content-Type': 'application/json' } });
890
+ } catch (error) {
891
+ if (opzioni.signal?.aborted && opzioni.signal.reason?.name !== 'TimeoutError') throw opzioni.signal.reason;
892
+ const classificazione = classificaGuasto(error);
893
+ await segnala(classificazione, null, classificazione.transitorio ? 503 : null);
894
+ if (!classificazione.transitorio) throw contesto.errore;
895
+ return new Response(contesto.errore.message, { status: 503 });
896
+ }
897
+ };
898
+ const instradata = creaFetchInstradata(rete, { risolvi, dipendenze: {
899
+ ...dipendenze, leggiChiave: p => p === fonte && scelta ? scelta.chiave : dipendenze.leggiChiave(p),
900
+ }, onAvviso, instradaOpenRouter: true, inattivitaGenerazioneMs, sorvegliaCorpo });
901
+ try { return await instradata(url, opzioni); }
902
+ catch (error) {
903
+ // P-K — token scaduto o involucro malformato: panchina senza partire in rete.
904
+ if (record.cloud && scelta && ['PROVIDER_CLOUD_TOKEN_EXPIRED', 'PROVIDER_CLOUD_CREDENTIAL_INVALID'].includes(error?.code)) {
905
+ providerStore.mettiInPanchina(fonte, scelta.impronta, { classe: 'credenziale' });
906
+ }
907
+ // P-K — fine
908
+ // Gli SDK nativi lanciano sugli HTTP non riusciti: ricondurli alla stessa
909
+ // risposta permette al kernel di esaurire il proprio budget anche qui.
910
+ if (contesto.errore?.stato) return new Response(contesto.errore.message, { status: contesto.errore.stato });
911
+ if (contesto.errore) throw contesto.errore;
912
+ throw error;
913
+ }
914
+ }
915
+
916
+ const fetchMultiProvider = (url, opzioni) => invia(url, opzioni);
917
+ fetchMultiProvider.eseguiConFallback = async (chiama, opzioni = {}) => {
918
+ if (modelloSessione && JSON.stringify(separaFonteModello(opzioni.modello)) !== JSON.stringify(separaFonteModello(modelloSessione))) {
919
+ return chiama({ fetchDiRete: fetchMultiProvider });
920
+ }
921
+ if (occupato) throw new OwnerRuntimeUnavailableError('Una chiamata di questa sessione è già in corso.', 'PROVIDER_FALLBACK_BUSY');
922
+ occupato = true;
923
+ try {
924
+ const iniziale = separaFonteModello(opzioni.modello);
925
+ let destinazione = effettivo ?? { provider: iniziale.fonte, model: iniziale.modelloRemoto };
926
+ const usaAttrezzi = Boolean(opzioni.attrezzi?.length || opzioni.messaggi?.some(m => m.role === 'tool' || m.tool_calls?.length));
927
+ while (true) {
928
+ opzioni.segnaleStop?.throwIfAborted();
929
+ const contesto = {};
930
+ let rispostaInterrotta = false;
931
+ const fetchTentativo = (url, init) => invia(url, init, contesto);
932
+ let risultato;
933
+ try {
934
+ risultato = await chiama({
935
+ modello: `${destinazione.provider}:${destinazione.model}`, fetchDiRete: fetchTentativo,
936
+ ...(opzioni.onDelta ? { onDelta: (...args) => { rispostaInterrotta = true; return opzioni.onDelta(...args); } } : {}),
937
+ });
938
+ } catch (error) {
939
+ if (opzioni.segnaleStop?.aborted || error?.fermatoSuRichiesta || error?.name === 'AbortError') {
940
+ /*
941
+ * ⛔ 14/09, giro vero della coda (banco 5475): 7 invii, 6 fermati, e la sessione diceva «1 giro». Una chiamata già
942
+ * PARTITA verso il fornitore e poi fermata non lasciava nessuna traccia, perché qui si rilanciava prima del deposito:
943
+ * i token di uno stream interrotto non arrivano (li porta l'ultimo pezzo), ma la chiamata c'è stata. Si deposita con
944
+ * `usage: null` e `esito: 'fermato'` — nessun numero inventato. Stessa forma di Codex, dove `TurnAbortedEvent` porta
945
+ * motivo e orari e i token restano `Option` (codex-rs/protocol/src/protocol.rs:4154 e :2318, clone 728cb12).
946
+ * ⛔ Solo se la richiesta è partita (`contesto.scelta`): uno stop prima della rete non è un giro. E un deposito che
947
+ * fallisce non deve coprire lo stop.
948
+ */
949
+ if (contesto.scelta && typeof onConsumoFornitore === 'function') {
950
+ try { await onConsumoFornitore({ tipo: 'consumo-fornitore', ...destinazione, usage: null, costoDichiarato: null, esito: 'fermato' }); } catch { /* lo stop resta lo stop */ }
951
+ }
952
+ throw error;
953
+ }
954
+ const classificazione = contesto.errore ?? classificaGuasto(error, error?.stato ?? error?.statusCode);
955
+ const pulito = erroreFornitorePubblico(classificazione, error?.stato ?? contesto.errore?.stato);
956
+ if (!contesto.errore && contesto.scelta) providerStore.mettiInPanchina(destinazione.provider, contesto.scelta.impronta, { classe: classificazione.classe });
957
+ if (typeof onConsumoFornitore === 'function') await onConsumoFornitore({ tipo: 'consumo-fornitore', ...destinazione, usage: null, costoDichiarato: null, esito: classificazione.classe === 'traffico' ? 'traffico' : 'interrotto' });
958
+ if (!classificazione.transitorio) throw pulito;
959
+ let prossima = null;
960
+ while (++indice < catena.length) {
961
+ const candidata = catena[indice];
962
+ if (candidata.provider === destinazione.provider || !providerStore.scegliChiave(candidata.provider)) continue;
963
+ try { validaFallbackProviders([candidata], { usaAttrezzi }); } catch { continue; }
964
+ prossima = candidata; break;
965
+ }
966
+ if (!prossima) throw pulito;
967
+ const messaggio = classificazione.classe === 'traffico'
968
+ ? `Il fornitore ${REGISTRO_FORNITORI[destinazione.provider].etichetta} limita il traffico: continuo con ${REGISTRO_FORNITORI[prossima.provider].etichetta} · modello ${prossima.model}`
969
+ : `Il fornitore ${REGISTRO_FORNITORI[destinazione.provider].etichetta} non risponde: continuo con ${REGISTRO_FORNITORI[prossima.provider].etichetta} · modello ${prossima.model}`;
970
+ await onCambioFornitore({ tipo: 'cambio-fornitore', precedente: destinazione, effettivo: prossima, classe: classificazione.classe, rispostaInterrotta, messaggio });
971
+ opzioni.segnaleStop?.throwIfAborted();
972
+ destinazione = prossima; effettivo = prossima;
973
+ continue;
974
+ }
975
+ // Un errore nel deposito della prova non deve provocare un'altra chiamata pagabile.
976
+ const usage = consumoPubblico(risultato?.usage);
977
+ const costoDichiarato = typeof usage?.cost === 'number' && Number.isFinite(usage.cost) && usage.cost >= 0 ? usage.cost : null;
978
+ if (typeof onConsumoFornitore === 'function') await onConsumoFornitore({ tipo: 'consumo-fornitore', ...destinazione, usage, costoDichiarato, esito: 'completato' });
979
+ return { ...risultato, usage, fornitoreEffettivo: destinazione.provider, modelloEffettivo: destinazione.model };
980
+ }
981
+ } finally { occupato = false; }
982
+ };
983
+ return fetchMultiProvider;
984
+ }
985
+
986
+ /**
987
+ * ⭐⭐⭐ 12/09 — P-B: LA CACHE CHE IL FORNITORE DICHIARA CON UN ALTRO NOME.
988
+ *
989
+ * DeepSeek chiama i token letti dalla cache `prompt_cache_hit_tokens`, Kimi li mette in
990
+ * `usage.cached_tokens` al primo livello, OpenRouter aggiunge un `cache_discount` che è **denaro**.
991
+ * Chi legge a valle conosce il solo nome canonico `prompt_tokens_details.cached_tokens`: su quei
992
+ * fornitori riporterebbe zero — e «zero da cache» su un agente che rilegge lo stesso prefisso 24
993
+ * volte non è un dettaglio del pannello costi, è **non sapere quanto stiamo spendendo**
994
+ * (misurato il 22/8: 87 token dentro per ogni 1 fuori, il 93% del costo).
995
+ *
996
+ * ⛔ Si aggiunge il nome canonico, NON si toglie niente: i campi nativi restano dove sono, e chi
997
+ * già li conosce (il kernel ne legge tre) continua a leggerli. Se non c'è niente da dichiarare
998
+ * la risposta torna **identica**, senza essere nemmeno letta.
999
+ * ⛔ SOLO le risposte JSON non in streaming. Una risposta SSE si lascia passare intatta: riscrivere
1000
+ * un flusso che non abbiamo prodotto, per un campo che il kernel sa già leggere in tre forme,
1001
+ * costerebbe più del difetto che cura. ⇒ Sul giro in streaming il valore resta quello che il
1002
+ * kernel estrae; qui si coprono compattazione, banco e ogni chiamata `stream:false`.
1003
+ * Dichiarato come NON coperto nel rapporto, non risolto in silenzio.
1004
+ */
1005
+ async function conCacheDichiarata(risposta, fonte) {
1006
+ try {
1007
+ if (!risposta?.ok) return risposta;
1008
+ const tipo = risposta.headers?.get?.('content-type') ?? '';
1009
+ if (!tipo.includes('json')) return risposta; // un `text/event-stream` esce di qui senza essere toccato
1010
+ const testo = await risposta.clone().text();
1011
+ const corpo = JSON.parse(testo);
1012
+ const normalizzato = normalizzaUsage(corpo?.usage, fonte);
1013
+ const sconto = scontoDaCache(corpo, fonte);
1014
+ if (normalizzato === corpo?.usage && sconto === null) return risposta;
1015
+ const nuovo = { ...corpo, usage: { ...normalizzato, ...(sconto !== null ? { cache_discount: sconto } : {}) } };
1016
+ return new Response(JSON.stringify(nuovo), { status: risposta.status, statusText: risposta.statusText, headers: risposta.headers });
1017
+ } catch (errore) {
1018
+ /*
1019
+ * ⛔⛔ 16/09/2026 — IL CATCH DICE QUALE GUASTO COPRE, E RILANCIA GLI ALTRI.
1020
+ *
1021
+ * Quello che copre: «il corpo non è quello che credevamo» (JSON malformato, campi assenti).
1022
+ * Lì una misura che non si scrive non deve rompere un giro — è la disciplina di
1023
+ * `persistiTempiDelGiro`.
1024
+ *
1025
+ * ⛔ Quello che NON deve coprire: un corpo che **non finisce mai**. Qui si sta leggendo
1026
+ * `risposta.clone().text()`: se il fornitore tace a metà JSON, a interrompere quella lettura
1027
+ * è il failsafe di inattività (o, dietro di lui, il `bodyTimeout` del trasporto). Ingoiare
1028
+ * quell'errore e restituire la risposta com'era vorrebbe dire consegnare al kernel un corpo
1029
+ * già rotto e far scoprire il guasto a qualcun altro, più tardi e senza il suo nome.
1030
+ * ⇒ Gli errori di CONTRATTO si rilanciano: [[il-catch-giusto-nasconde-il-bug-sbagliato]].
1031
+ */
1032
+ if (CODICI_DI_CORPO_MAI_FINITO.has(String(errore?.code ?? errore?.cause?.code ?? ''))) throw errore;
1033
+ return risposta;
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * ⛔⛔⛔ 10/09 — IL SERVER DELL'OWNER E' RIMASTO SENZA KERNEL DOPO UN RIAVVIO.
1039
+ *
1040
+ * Sintomo: ogni giro moriva con «Il runtime agente non è configurato per questa installazione.»
1041
+ * (`RunError`, `code: internal-error`), e in chat compariva «Il giro si è interrotto per un
1042
+ * errore». Misurato leggendo il registro della sessione: `RunStarted → ToolCallStart →
1043
+ * ToolCallArgs → RunError`, con zero eventi in mezzo.
1044
+ *
1045
+ * CAUSA: qui il modulo del kernel si leggeva SOLO da `process.env.TALOS_OWNER_RUNTIME_MODULE`,
1046
+ * e `scripts/aggiorna-4174.ps1` ha smesso di forzarla. Senza variabile: `null` ⇒ nessun runtime.
1047
+ * ⛔ Il ripiego esisteva già nel repo, in un altro file: `config.mjs` ha `kernelNelRepo()`, che
1048
+ * quando la variabile manca torna il kernel versionato — cioè il progetto aveva già DECISO che
1049
+ * il kernel nel repo è il default. Questo file semplicemente non lo sapeva. (È la regola «prima
1050
+ * di cercare fuori, cerca nel tuo codebase»: la risposta era a due file di distanza.)
1051
+ * ⇒ Stessa decisione, un posto solo in più. Chi imposta la variabile continua a vincere, byte per
1052
+ * byte: il ripiego vale SOLO quando la variabile non c'è.
1053
+ */
1054
+ function kernelNelRepo() {
1055
+ try {
1056
+ const percorso = fileURLToPath(new URL('kernel/talosHarness.mjs', import.meta.url));
1057
+ return statSync(percorso).isFile() ? percorso : null;
1058
+ } catch {
1059
+ return null;
1060
+ }
1061
+ }
1062
+
1063
+ export function createOwnerRuntimeAdapter({
1064
+ modulePath = process.env.TALOS_OWNER_RUNTIME_MODULE ?? kernelNelRepo(),
1065
+ importFn = (specifier) => import(specifier),
1066
+ openRouterRuntimeFn = () => ({ timeoutSeconds: OPENROUTER_IDLE_MS_PREDEFINITO / 1_000 }),
1067
+ modelCapabilityFn = async () => null,
1068
+ /**
1069
+ * ⭐ 03/9 — da dove si leggono chiave, indirizzo e motore locale per
1070
+ * instradare un modello non-OpenRouter. ⛔ Assente = comportamento di
1071
+ * sempre, byte per byte: chi non le passa non cambia di una virgola.
1072
+ */
1073
+ destinazioneModelloDeps = null,
1074
+ providerStore = null,
1075
+ resolveImagesFn = null,
1076
+ } = {}) {
1077
+ const specifier = normalizzaModuloPath(modulePath);
1078
+ let moduloPromise = null;
1079
+ const carica = async () => {
1080
+ if (!specifier) throw new OwnerRuntimeUnavailableError('Il runtime agente non è configurato per questa installazione.');
1081
+ if (!moduloPromise) {
1082
+ moduloPromise = Promise.resolve(importFn(specifier)).catch((error) => {
1083
+ moduloPromise = null;
1084
+ throw new OwnerRuntimeUnavailableError('Il runtime agente non è disponibile. Controlla la configurazione del server.', 'OWNER_RUNTIME_LOAD_FAILED', { cause: error });
1085
+ });
1086
+ }
1087
+ return moduloPromise;
1088
+ };
1089
+ const richiama = async (nome, ...argomenti) => {
1090
+ const runtime = await carica();
1091
+ if (typeof runtime[nome] !== 'function') {
1092
+ throw new OwnerRuntimeUnavailableError(`Il runtime agente non espone l’operazione richiesta (${nome}).`, 'OWNER_RUNTIME_CONTRACT_INVALID');
1093
+ }
1094
+ return runtime[nome](...argomenti);
1095
+ };
1096
+ return Object.freeze({
1097
+ /*
1098
+ * ⛔⛔⛔ 06/9, owner, due volte e in maiuscolo: «IL MODELLO DEVE LEGGERE LA PAGINA DOVE VADO IO,
1099
+ * DEVE AVERE GLI OCCHI SULLA SEZIONE BROWSER ANCHE SE SONO IO A NAVIGARCI DENTRO».
1100
+ * Una pagina di un'altra origine dentro una cornice NON si legge dal JavaScript della pagina che
1101
+ * la ospita — è il confine di origine del browser, e non c'è trucco che lo aggiri (ricerca
1102
+ * 06/09/2026: browser-use «Leaving Playwright for CDP», microsoft/playwright #21780). Chi ci
1103
+ * riesce lo fa fuori dalla pagina: qui la legge il SERVER, con la stessa funzione dell'attrezzo
1104
+ * `naviga` — cioè con la stessa validazione già scritta e già provata contro gli indirizzi
1105
+ * interni (SSRF: allowlist di schema, niente indirizzi privati, catena di redirect limitata),
1106
+ * invece di scrivere una seconda validazione che diverge dalla prima.
1107
+ * ⛔ Il server non ha i cookie della persona: di un sito dietro login vede la versione pubblica.
1108
+ * Va detto a schermo, non nascosto.
1109
+ */
1110
+ async leggiPagina(url) {
1111
+ const runtime = await carica();
1112
+ if (typeof runtime.leggiPaginaSicura !== 'function') {
1113
+ throw new OwnerRuntimeUnavailableError('Il runtime agente non espone la lettura di una pagina.', 'OWNER_RUNTIME_CONTRACT_INVALID');
1114
+ }
1115
+ const pagina = await runtime.leggiPaginaSicura(String(url ?? ''));
1116
+ return { url: String(pagina?.url ?? url ?? ''), stato: Number(pagina?.stato ?? 0) || 0, corpo: String(pagina?.corpo ?? '') };
1117
+ },
1118
+ async runtimeSnapshot() {
1119
+ if (!specifier) return parseRuntimeOwnerSnapshot(null);
1120
+ const runtime = await carica();
1121
+ if (typeof runtime.runtimeSnapshot !== 'function') {
1122
+ return parseRuntimeOwnerSnapshot({ status: 'unavailable', items: null, reason: 'runtime_snapshot_not_exposed', observedAt: null });
1123
+ }
1124
+ return parseRuntimeOwnerSnapshot(await runtime.runtimeSnapshot());
1125
+ },
1126
+ async taskCatalogProvider() {
1127
+ if (!specifier) return null;
1128
+ const runtime = await carica();
1129
+ if (typeof runtime.listaTaskDisponibili !== 'function' || typeof runtime.preparaEsecuzione !== 'function') {
1130
+ throw new OwnerRuntimeUnavailableError('Il runtime agente non espone il catalogo task richiesto.', 'OWNER_RUNTIME_CONTRACT_INVALID');
1131
+ }
1132
+ return Object.freeze({
1133
+ list: () => runtime.listaTaskDisponibili(),
1134
+ prepare: (taskId) => runtime.preparaEsecuzione(taskId),
1135
+ });
1136
+ },
1137
+ /**
1138
+ * ⭐⭐⭐ O-01 (04/9) — GLI ATTREZZI VERI, CHIESTI AL KERNEL.
1139
+ *
1140
+ * Il Capability hub («+» del composer) elencava SETTE nomi scritti a mano
1141
+ * dentro una stringa di template, sotto l'etichetta «Attrezzi
1142
+ * dell'harness · sempre offerti al modello». Il kernel ne offre 43 (7
1143
+ * base + i 36 di `strumentiEstesi`, session-registry.mjs): 36 attrezzi
1144
+ * VERI — `web_search`, `document_create`, `generate_image`,
1145
+ * `delega_sottotask`, tutta Libreria/Notes/Tasks/Memory/Research/Forge —
1146
+ * non comparivano da nessuna parte. Un inventario incompleto presentato
1147
+ * come completo è uno stato inventato, esattamente come un contatore
1148
+ * inventato.
1149
+ *
1150
+ * ⛔ La cura non è allungare la lista a mano (invecchierebbe di nuovo, e
1151
+ * in silenzio): si LEGGE dal kernel, che è l'unico posto dove quei nomi
1152
+ * e quelle descrizioni esistono davvero. Nessuna copia, nessun secondo
1153
+ * elenco da tenere allineato.
1154
+ *
1155
+ * `tokenSchemaStimati` è una STIMA dichiarata (caratteri del JSON / 4,
1156
+ * l'euristica affermata) sul JSON che va davvero sul filo, non un numero
1157
+ * inventato: serve a rispondere «quanto mi costa avere questi attrezzi
1158
+ * offerti a ogni giro» — la stessa domanda a cui lo stato dell'arte
1159
+ * risponde col suo «schema token estimate» per server MCP, qui estesa a
1160
+ * OGNI attrezzo, MCP compresi quando ci saranno.
1161
+ *
1162
+ * @returns {Promise<{base: Array<{nome:string,descrizione:string,tokenSchemaStimati:number}>, estesi: Array}>}
1163
+ */
1164
+ async attrezziKernel() {
1165
+ const runtime = await carica();
1166
+ const leggi = (elenco, dove) => {
1167
+ if (!Array.isArray(elenco)) {
1168
+ throw new OwnerRuntimeUnavailableError(`Il runtime agente non espone l’elenco degli attrezzi (${dove}).`, 'OWNER_RUNTIME_CONTRACT_INVALID');
1169
+ }
1170
+ return elenco.map((voce) => {
1171
+ const f = voce?.function ?? voce ?? {};
1172
+ return {
1173
+ nome: String(f.name ?? ''),
1174
+ descrizione: String(f.description ?? ''),
1175
+ // ⛔ Misurato sul JSON reale della dichiarazione, non su un valore per attrezzo scritto altrove.
1176
+ tokenSchemaStimati: Math.ceil(JSON.stringify(voce ?? {}).length / 4),
1177
+ };
1178
+ }).filter((a) => a.nome);
1179
+ };
1180
+ return {
1181
+ base: leggi(runtime.ATTREZZI_OPENAI, 'ATTREZZI_OPENAI'),
1182
+ estesi: leggi(runtime.ATTREZZI_ESTESI_OPENAI, 'ATTREZZI_ESTESI_OPENAI'),
1183
+ };
1184
+ },
1185
+ async talosLavora(input) {
1186
+ const fallbackProviders = validaFallbackProviders(input?.fallbackProviders ?? []);
1187
+ if (fallbackProviders.length) {
1188
+ const runtime = await carica();
1189
+ if (runtime.SUPPORTA_FALLBACK_FORNITORI !== 1) {
1190
+ throw new OwnerRuntimeUnavailableError('Il motore di questa installazione non collega ancora il cambio di fornitore alla conversazione.', 'PROVIDER_FALLBACK_CONTRACT_REQUIRED');
1191
+ }
1192
+ }
1193
+ const fetchOriginale = typeof input?.fetchDiRete === 'function' ? input.fetchDiRete : fetch;
1194
+ const fetchConDescrizione = creaFetchConDescrizioneComando(fetchOriginale);
1195
+ const fetchResiliente = creaFetchOpenRouterResiliente(fetchConDescrizione, {
1196
+ timeoutMsFn: async () => {
1197
+ const runtime = await Promise.resolve(openRouterRuntimeFn()).catch(() => null);
1198
+ return Number(runtime?.timeoutSeconds) * 1_000;
1199
+ },
1200
+ modelCapabilityFn,
1201
+ userSignal: input?.segnaleStop ?? null,
1202
+ });
1203
+ /*
1204
+ * ⛔ L'ORDINE conta: il multi-provider sta PIÙ ESTERNO della resilienza
1205
+ * OpenRouter, così le ritentate e i timeout di quella restano applicati
1206
+ * alla richiesta finale, qualunque sia la sua destinazione. Metterlo
1207
+ * dentro avrebbe fatto ritentare su OpenRouter una chiamata già
1208
+ * dirottata altrove.
1209
+ */
1210
+ const fetchInstradata = creaFetchMultiProvider(fetchResiliente, {
1211
+ /* P0 · punto 7 (16/09): il failsafe della sessione, letto una volta e passato a valle. */
1212
+ inattivitaGenerazioneMs: leggiInattivitaGenerazioneMs(),
1213
+ dipendenze: destinazioneModelloDeps, providerStore, fallbackProviders, modelloSessione: input?.modello,
1214
+ onAvviso: input?.onAvviso, onCambioFornitore: input?.onCambioFornitore, onConsumoFornitore: input?.onConsumoFornitore,
1215
+ });
1216
+ const fetchConImmagini = async (url, init = {}, successiva = fetchInstradata) => {
1217
+ // BC-48 C-bis: GLM via OpenRouter usa cache implicita (fonti nel rapporto
1218
+ // del 12/09/2026). Il kernel sposta il marcatore all'ultimo sistema:
1219
+ // alla ripresa il preambolo diventava stringa dopo essere stato array.
1220
+ // Normalizziamo solo la forma esatta prodotta dal kernel; testo, immagini
1221
+ // e forme estese restano integri. Nessuna mutazione della storia salvata.
1222
+ if (String(url).includes('/chat/completions') && typeof init.body === 'string') {
1223
+ let corpo;
1224
+ try { corpo = JSON.parse(init.body); } catch { /* Il trasporto gestisce il JSON malformato. */ }
1225
+ if (typeof corpo?.model === 'string' && separaFonteModello(corpo.model).fonte === 'openrouter'
1226
+ && /^z-ai\/glm-/.test(separaFonteModello(corpo.model).modelloRemoto) && Array.isArray(corpo.messages)) {
1227
+ let cambiato = false;
1228
+ const messages = corpo.messages.map(m => {
1229
+ const p = Array.isArray(m?.content) && m.content.length === 1 ? m.content[0] : null;
1230
+ if (m?.role !== 'system' || p?.type !== 'text' || typeof p.text !== 'string'
1231
+ || Object.keys(p).length !== 3 || p.cache_control?.type !== 'ephemeral'
1232
+ || p.cache_control.ttl !== '1h' || Object.keys(p.cache_control).length !== 2) return m;
1233
+ cambiato = true;
1234
+ return { ...m, content: p.text };
1235
+ });
1236
+ if (cambiato) init = { ...init, body: JSON.stringify({ ...corpo, messages }) };
1237
+ }
1238
+ }
1239
+ // BC-48 C-bis: fine della normalizzazione per la cache implicita GLM.
1240
+ if (input?.contextHooks && String(url).includes('/chat/completions') && typeof init.body === 'string') {
1241
+ let body;
1242
+ try { body = JSON.parse(init.body); } catch { /* Preserve the existing malformed-body path. */ }
1243
+ if (typeof body?.model === 'string' && separaFonteModello(body.model).fonte === 'openrouter') {
1244
+ const plugins = (Array.isArray(body.plugins) ? body.plugins : []).filter(plugin => plugin?.id !== 'context-compression');
1245
+ init = { ...init, body: JSON.stringify({ ...body, plugins: [...plugins, { id: 'context-compression', enabled: false }] }) };
1246
+ }
1247
+ }
1248
+ if (!resolveImagesFn || !String(url).includes('/chat/completions') || typeof init.body !== 'string') return successiva(url, init);
1249
+ let body;
1250
+ try { body = JSON.parse(init.body); } catch { return successiva(url, init); }
1251
+ if (!Array.isArray(body.messages)) return successiva(url, init);
1252
+ const hasImages = body.messages.some(m => Array.isArray(m.content) && m.content.some(p => p?.type === 'image_url'));
1253
+ if (hasImages) {
1254
+ const capability = await Promise.resolve(modelCapabilityFn(body.model)).catch(() => null);
1255
+ if (capability?.inputModalities?.length && !capability.inputModalities.includes('image')) {
1256
+ throw new OwnerRuntimeUnavailableError('Il modello selezionato non accetta immagini. Scegli un modello con visione.', 'MODEL_IMAGE_NOT_SUPPORTED');
1257
+ }
1258
+ }
1259
+ const messages = await resolveImagesFn(body.messages);
1260
+ return successiva(url, { ...init, body: JSON.stringify({ ...body, messages }) });
1261
+ };
1262
+ if (fetchInstradata.eseguiConFallback) {
1263
+ // Anche il tentativo passato al kernel deve conservare il risolutore delle immagini.
1264
+ fetchConImmagini.eseguiConFallback = (chiama, opzioni) => fetchInstradata.eseguiConFallback(aggiunte =>
1265
+ chiama({ ...aggiunte, fetchDiRete: (url, init) => fetchConImmagini(url, init, aggiunte.fetchDiRete) }), opzioni);
1266
+ }
1267
+ // BC-48 A: lo stesso confine di fetchConImmagini, ma PRIMA della serializzazione:
1268
+ // una modifica soltanto del body non resterebbe nello storico della sessione.
1269
+ // Gli hook legacy non vengono inventati: ciò disabiliterebbe la loro compattazione.
1270
+ let contextHooks = input?.contextHooks;
1271
+ if (contextHooks && input?.cartella && !input?.mobile) {
1272
+ const [file, radice] = await Promise.all([
1273
+ trovaIstruzioniDiProgetto(input.cartella), trovaRadiceProgetto(input.cartella),
1274
+ ]);
1275
+ contextHooks = collegaSezioniAiContextHooks({ contextHooks, file, cartella: input.cartella, radice: radice ?? input.cartella });
1276
+ }
1277
+ return richiama('talosLavora', { ...input, contextHooks, fetchDiRete: fetchConImmagini });
1278
+ // BC-48 A · fine collegamento.
1279
+ },
1280
+ /** One bounded summary request through the same provider adapters as chat. */
1281
+ async callContextModel({ provider, model, messages, maxOutputTokens, signal, fetchDiRete = fetch }) {
1282
+ const fail = (code, message, usage) => { throw Object.assign(new Error(message), { code, ...(usage !== undefined ? { usage } : {}) }); };
1283
+ if (!FONTI_MODELLO.includes(provider) || typeof model !== 'string' || !model.trim() || !Array.isArray(messages) || !Number.isSafeInteger(maxOutputTokens) || maxOutputTokens < 1) fail('CTX_MODEL_INVALID', 'Richiesta di sintesi non valida.');
1284
+ if (!destinazioneModelloDeps) fail('CTX_TRANSPORT_UNAVAILABLE', 'Il trasporto del modello non è collegato al compattatore.');
1285
+ signal?.throwIfAborted();
1286
+ const routed = creaFetchMultiProvider(fetchDiRete, { dipendenze: destinazioneModelloDeps });
1287
+ const key = provider === 'openrouter' ? destinazioneModelloDeps.leggiChiave?.('openrouter') : null;
1288
+ if (provider === 'openrouter' && !key) fail('CTX_TOKEN_AUTH', 'La chiave del provider selezionato non è disponibile.');
1289
+ /*
1290
+ * ⛔ 09/09/2026 — trovato dal giro vero D1 (z-ai/glm-5.3-flash via OpenRouter): la sintesi tornava
1291
+ * SENZA testo, perché il modello ragiona per difetto e il ragionamento si mangiava il budget della
1292
+ * risposta. Spegnerlo non si può («Reasoning is mandatory for this endpoint and cannot be
1293
+ * disabled», HTTP 400, misurato). Misurato con quattro chiamate: senza campo reasoning 133 token
1294
+ * di ragionamento e a volte `finish_reason: length`; con `reasoning.effort: 'low'` ZERO token di
1295
+ * ragionamento, `finish_reason: stop`, costo più basso. Una sintesi non ha bisogno di pensare a
1296
+ * lungo: chiede poco, nel rispetto delle capacità del catalogo (`normalizzaReasoningPerModello`
1297
+ * toglie un effort che il modello non supporta, mai `none` a chi lo vieta).
1298
+ * Fonte 09/09/2026: openrouter.ai/docs/use-cases/reasoning-tokens — «low: approximately 20% of
1299
+ * max_tokens», `effort: 'none'` disabilita e va evitato sui modelli «mandatory».
1300
+ */
1301
+ const capability = provider === 'openrouter' ? await Promise.resolve(modelCapabilityFn(model)).catch(() => null) : null;
1302
+ const reasoning = provider === 'openrouter' ? normalizzaReasoningPerModello({ effort: 'low' }, capability) : undefined;
1303
+ const response = await routed(ENDPOINT_OPENROUTER, {
1304
+ method: 'POST', headers: { 'Content-Type': 'application/json', ...(key ? { Authorization: `Bearer ${key}` } : {}) },
1305
+ body: JSON.stringify({ model: provider === 'openrouter' ? model : `${provider}:${model}`, messages: structuredClone(messages), tools: [], max_tokens: maxOutputTokens, stream: false, ...(reasoning ? { reasoning } : {}), ...(provider === 'openrouter' ? { transforms: [], plugins: [{ id: 'context-compression', enabled: false }] } : {}) }),
1306
+ /*
1307
+ * ⛔ P0 · punto 7 (16/09/2026) — la compattazione del contesto aveva anch'essa 180 s fissi.
1308
+ * È la chiamata che si fa proprio quando la conversazione è DIVENTATA GRANDE: il caso in cui
1309
+ * il modello ci mette di più è esattamente quello per cui serve. Stesso failsafe del resto,
1310
+ * e lo `signal` del chiamante (che porta lo Stop) resta il primo a poter chiudere.
1311
+ */
1312
+ signal: AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(leggiInattivitaGenerazioneMs() || 1_800_000)]),
1313
+ });
1314
+ if (!response.ok) {
1315
+ await response.body?.cancel();
1316
+ fail('CTX_SUMMARY_HTTP', `Il modello di sintesi ha risposto con HTTP ${response.status}.`);
1317
+ }
1318
+ let result;
1319
+ try { result = await response.json(); } catch { fail('CTX_SUMMARY_RESPONSE_INVALID', 'Risposta di sintesi non leggibile.'); }
1320
+ const choice = result?.choices?.[0];
1321
+ const usage = result?.usage;
1322
+ if (choice?.message?.tool_calls?.length) fail('CTX_SUMMARY_TOOLS', 'La sintesi non può eseguire strumenti.', usage);
1323
+ // 09/09 — il caso visto dal vivo: niente testo ma token di ragionamento spesi. Non è una risposta
1324
+ // «invalida» da guardare nel codice: è un budget finito nel pensiero, e va detto in quelle parole.
1325
+ const reasoningTokens = usage?.completion_tokens_details?.reasoning_tokens;
1326
+ if (typeof choice?.message?.content !== 'string' && Number.isSafeInteger(reasoningTokens) && reasoningTokens > 0) {
1327
+ fail('CTX_TRUNCATED_SUMMARY', `Il modello ha speso ${reasoningTokens} token nel ragionamento e non ha lasciato spazio alla sintesi.`, usage);
1328
+ }
1329
+ if (typeof choice?.message?.content !== 'string' || typeof choice?.finish_reason !== 'string') fail('CTX_SUMMARY_RESPONSE_INVALID', 'La sintesi non dichiara testo e stato finale.', usage);
1330
+ return { text: choice.message.content, finishReason: choice.finish_reason, usage };
1331
+ },
1332
+ async eseguiComandoSandboxato(...args) { return richiama('eseguiComandoSandboxato', ...args); },
1333
+ async eseguiFlowForge(...args) {
1334
+ if (specifier) return richiama('eseguiFlowForge', ...args);
1335
+ return eseguiFlowForgeLocale(...args);
1336
+ },
1337
+ validaManifestForge(manifest) { return validaManifestForgeLocale(manifest); },
1338
+ async chiamaConRitenta(options) {
1339
+ if (specifier) return richiama('chiamaConRitenta', options);
1340
+ return chiamaConRitentaLocale(options);
1341
+ },
1342
+ async compattaConversazione(messaggi, chiamaModello) {
1343
+ if (specifier) return richiama('compattaConversazione', messaggi, chiamaModello);
1344
+ return compattaConversazioneLocale(messaggi, chiamaModello);
1345
+ },
1346
+ forgeToolPrefix: FORGE_PREFISSO_NOME_TOOL,
1347
+ });
1348
+ }