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.
- package/CHANGELOG.md +87 -0
- package/LICENSE +661 -0
- package/README.md +116 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +100 -0
- package/dist/automations/history-store.js +34 -0
- package/dist/automations/policy-store.js +43 -0
- package/dist/automations/runner.js +107 -0
- package/dist/automations/schedule.js +85 -0
- package/dist/automations/windows-task.js +53 -0
- package/dist/commands/advanced-cli.js +233 -0
- package/dist/commands/checkpoint-cli.js +45 -0
- package/dist/commands/config-cli.js +107 -0
- package/dist/commands/context.js +17 -0
- package/dist/commands/extensions-cli.js +207 -0
- package/dist/commands/project-cli.js +74 -0
- package/dist/commands/project-commands.js +143 -0
- package/dist/commands/provider-cli.js +244 -0
- package/dist/commands/services-cli.js +248 -0
- package/dist/commands/session-cli.js +171 -0
- package/dist/commands/system-cli.js +405 -0
- package/dist/config/commands.js +60 -0
- package/dist/config/load.js +382 -0
- package/dist/config/migrations.js +48 -0
- package/dist/config/types.js +11 -0
- package/dist/diagnostics/development-log.js +154 -0
- package/dist/diagnostics/doctor.js +104 -0
- package/dist/diagnostics/redact.js +80 -0
- package/dist/diagnostics/zip.js +52 -0
- package/dist/errors.js +156 -0
- package/dist/events/bridge.js +130 -0
- package/dist/extensions/installer.js +178 -0
- package/dist/extensions/package-schema.js +24 -0
- package/dist/headless/result.js +84 -0
- package/dist/headless/run.js +230 -0
- package/dist/i18n/en/approval.js +46 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +92 -0
- package/dist/i18n/en/errors.js +267 -0
- package/dist/i18n/en/firstrun.js +101 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +774 -0
- package/dist/i18n/en/tools.js +127 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/error-view.js +307 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/kernel-map.js +139 -0
- package/dist/io.js +45 -0
- package/dist/main.js +331 -0
- package/dist/paths.js +41 -0
- package/dist/protocol/v2/codec.js +9 -0
- package/dist/protocol/v2/events.js +555 -0
- package/dist/protocol/v2/index.js +4 -0
- package/dist/protocol/v2/replay.js +28 -0
- package/dist/protocol/v2/types.js +1 -0
- package/dist/provider/control-plane.js +135 -0
- package/dist/provider/environment-keys.js +275 -0
- package/dist/provider/health.js +110 -0
- package/dist/provider/missing-key.js +48 -0
- package/dist/provider/model-catalog.js +168 -0
- package/dist/provider/openrouter-login.js +172 -0
- package/dist/provider/provider-text.js +13 -0
- package/dist/provider/store.js +95 -0
- package/dist/provider/system-keyring.js +214 -0
- package/dist/runtime/active-run.js +63 -0
- package/dist/runtime/agent-tree.js +74 -0
- package/dist/runtime/attachments.js +84 -0
- package/dist/runtime/brokered-executor.js +395 -0
- package/dist/runtime/context-archive.js +69 -0
- package/dist/runtime/context-status.js +76 -0
- package/dist/runtime/create-runtime.js +111 -0
- package/dist/runtime/model-profile.js +361 -0
- package/dist/runtime/output-store.js +137 -0
- package/dist/runtime/provider-attempts.js +185 -0
- package/dist/runtime/replay-buffer.js +153 -0
- package/dist/runtime/repo.js +95 -0
- package/dist/runtime/session-facade.js +135 -0
- package/dist/runtime/session-summary.js +125 -0
- package/dist/runtime/supervisor.js +265 -0
- package/dist/runtime/talos-composition.js +1011 -0
- package/dist/runtime/types.js +5 -0
- package/dist/runtime/usage-snapshot.js +82 -0
- package/dist/security/auto-classifier.js +38 -0
- package/dist/security/credential-free-environment.js +54 -0
- package/dist/security/evaluate.js +243 -0
- package/dist/security/execution-backends.js +236 -0
- package/dist/security/execution-broker.js +102 -0
- package/dist/security/forge-scan.js +74 -0
- package/dist/security/from-approval.js +39 -0
- package/dist/security/mxc-execution-backend.js +281 -0
- package/dist/security/permission-engine.js +138 -0
- package/dist/security/permission-explanation.js +54 -0
- package/dist/security/persist.js +179 -0
- package/dist/security/plugin-guard.js +230 -0
- package/dist/security/process-tree-evidence-store.js +74 -0
- package/dist/security/process-tree-probe.js +554 -0
- package/dist/security/project-resource-inventory.js +186 -0
- package/dist/security/project-trust-gate.js +38 -0
- package/dist/security/project-trust.js +367 -0
- package/dist/security/rule-parser.js +201 -0
- package/dist/security/shell-segmentation.js +68 -0
- package/dist/security/trust-authority.js +324 -0
- package/dist/security/types.js +1 -0
- package/dist/security/workspace-identity.js +102 -0
- package/dist/services/automation-facade.js +59 -0
- package/dist/services/forge-facade.js +77 -0
- package/dist/services/hook-facade.js +240 -0
- package/dist/services/index.js +16 -0
- package/dist/services/library-facade.js +174 -0
- package/dist/services/mcp-facade.js +348 -0
- package/dist/services/memory-facade.js +126 -0
- package/dist/services/notes-facade.js +157 -0
- package/dist/services/plugin-facade.js +308 -0
- package/dist/services/research-facade.js +110 -0
- package/dist/services/task-board-facade.js +236 -0
- package/dist/services/workflow-catalog.js +246 -0
- package/dist/sessions/export-format.js +98 -0
- package/dist/sessions/metadata-store.js +160 -0
- package/dist/sessions/share.js +122 -0
- package/dist/sessions/transfer.js +16 -0
- package/dist/subcommands.js +62 -0
- package/dist/tui/agent-roster.js +36 -0
- package/dist/tui/agent-view.js +51 -0
- package/dist/tui/app.js +4098 -0
- package/dist/tui/approval.js +36 -0
- package/dist/tui/boot/boot-sequence.js +76 -0
- package/dist/tui/boot/cinematic.js +243 -0
- package/dist/tui/boot/desktop-logo.js +82 -0
- package/dist/tui/boot.js +3 -0
- package/dist/tui/busy-input.js +58 -0
- package/dist/tui/catalog-service.js +560 -0
- package/dist/tui/components/assistant-stream.js +1 -0
- package/dist/tui/components/command-menu.js +68 -0
- package/dist/tui/components/composer.js +219 -0
- package/dist/tui/components/diff.js +35 -0
- package/dist/tui/components/footer.js +48 -0
- package/dist/tui/components/header.js +6 -0
- package/dist/tui/components/markdown.js +305 -0
- package/dist/tui/components/status-indicator.js +38 -0
- package/dist/tui/components/terminal-shell.js +214 -0
- package/dist/tui/components/thinking-row.js +19 -0
- package/dist/tui/components/tool-row.js +97 -0
- package/dist/tui/components/transcript-virtualizer.js +165 -0
- package/dist/tui/components/transcript.js +43 -0
- package/dist/tui/descendant-approvals.js +104 -0
- package/dist/tui/diff-model.js +77 -0
- package/dist/tui/editor-history.js +21 -0
- package/dist/tui/editor.js +210 -0
- package/dist/tui/event-adapter.js +436 -0
- package/dist/tui/exit-output.js +58 -0
- package/dist/tui/external-editor.js +53 -0
- package/dist/tui/file-completion.js +89 -0
- package/dist/tui/focus-manager.js +5 -0
- package/dist/tui/highlight.js +30 -0
- package/dist/tui/input-router.js +18 -0
- package/dist/tui/interrupt.js +99 -0
- package/dist/tui/keybindings.js +194 -0
- package/dist/tui/keymap-resolver.js +91 -0
- package/dist/tui/launch-state.js +148 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +56 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +43 -0
- package/dist/tui/overlays/approval-dialog.js +642 -0
- package/dist/tui/overlays/automation-center.js +51 -0
- package/dist/tui/overlays/checkpoint-picker.js +47 -0
- package/dist/tui/overlays/context-inspector.js +36 -0
- package/dist/tui/overlays/effort-line.js +110 -0
- package/dist/tui/overlays/forge-center.js +46 -0
- package/dist/tui/overlays/help-dialog.js +16 -0
- package/dist/tui/overlays/history-picker.js +37 -0
- package/dist/tui/overlays/hook-center.js +70 -0
- package/dist/tui/overlays/library-center.js +55 -0
- package/dist/tui/overlays/mcp-center.js +60 -0
- package/dist/tui/overlays/memory-center.js +63 -0
- package/dist/tui/overlays/model-picker.js +77 -0
- package/dist/tui/overlays/notes-center.js +53 -0
- package/dist/tui/overlays/overlay-host.js +8 -0
- package/dist/tui/overlays/plugin-center.js +76 -0
- package/dist/tui/overlays/provider-picker.js +171 -0
- package/dist/tui/overlays/queue-editor.js +32 -0
- package/dist/tui/overlays/research-center.js +59 -0
- package/dist/tui/overlays/scroll-window.js +234 -0
- package/dist/tui/overlays/session-picker.js +115 -0
- package/dist/tui/overlays/tasks-center.js +83 -0
- package/dist/tui/overlays/theme-picker.js +21 -0
- package/dist/tui/overlays/transcript-search.js +58 -0
- package/dist/tui/overlays/trust-center.js +29 -0
- package/dist/tui/overlays/workflow-center.js +46 -0
- package/dist/tui/project-file-index.js +214 -0
- package/dist/tui/project-references.js +85 -0
- package/dist/tui/project-trust-prompt.js +289 -0
- package/dist/tui/prompt-history-store.js +116 -0
- package/dist/tui/queue-store.js +110 -0
- package/dist/tui/regions/budget.js +59 -0
- package/dist/tui/regions/views.js +96 -0
- package/dist/tui/render-coordinator.js +148 -0
- package/dist/tui/render-scheduler.js +4 -0
- package/dist/tui/run.js +69 -0
- package/dist/tui/selection-list.js +29 -0
- package/dist/tui/session-controller.js +791 -0
- package/dist/tui/session-export.js +54 -0
- package/dist/tui/setup-wizard.js +46 -0
- package/dist/tui/shell-input.js +35 -0
- package/dist/tui/shell-layout.js +48 -0
- package/dist/tui/shell-model.js +98 -0
- package/dist/tui/slash-commands.js +82 -0
- package/dist/tui/state.js +151 -0
- package/dist/tui/status-bar.js +125 -0
- package/dist/tui/status-view.js +40 -0
- package/dist/tui/terminal-capabilities.js +9 -0
- package/dist/tui/terminal-session.js +11 -0
- package/dist/tui/text-width.js +66 -0
- package/dist/tui/theme-catalog.js +27 -0
- package/dist/tui/theme-store.js +23 -0
- package/dist/tui/theme.js +23 -0
- package/dist/tui/tool-display.js +135 -0
- package/dist/tui/tool-facts.js +1 -0
- package/dist/tui/tools/bash-renderer.js +32 -0
- package/dist/tui/tools/change.js +61 -0
- package/dist/tui/tools/edit-renderer.js +17 -0
- package/dist/tui/tools/generic-renderer.js +13 -0
- package/dist/tui/tools/list-renderer.js +14 -0
- package/dist/tui/tools/read-renderer.js +13 -0
- package/dist/tui/tools/registry.js +20 -0
- package/dist/tui/tools/row-format.js +187 -0
- package/dist/tui/tools/search-renderer.js +38 -0
- package/dist/tui/tools/shared.js +97 -0
- package/dist/tui/tools/write-renderer.js +15 -0
- package/dist/tui/transcript-model.js +441 -0
- package/dist/tui/ui-preferences.js +79 -0
- package/dist/tui/usage-view.js +77 -0
- package/dist/tui/vim-mode.js +99 -0
- package/dist/update/check.js +15 -0
- package/dist/update/npm.js +51 -0
- package/dist/update/run.js +129 -0
- package/dist/version.js +2 -0
- package/dist/workspace/checkpoint-store.js +210 -0
- package/dist/workspace/checkpoint.js +413 -0
- package/dist/workspace/restore.js +232 -0
- package/package.json +64 -5
- package/vendor/context-engine/package.json +11 -0
- package/vendor/context-engine/src/compaction-planner.mjs +57 -0
- package/vendor/context-engine/src/contracts.mjs +48 -0
- package/vendor/context-engine/src/engine.mjs +445 -0
- package/vendor/context-engine/src/node/context-export.mjs +164 -0
- package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
- package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
- package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
- package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
- package/vendor/context-engine/src/profiles.mjs +10 -0
- package/vendor/context-engine/src/retrieval.mjs +104 -0
- package/vendor/context-engine/src/summary.mjs +97 -0
- package/vendor/context-engine/src/usage.mjs +34 -0
- package/vendor/harness-ui/package.json +38 -0
- package/vendor/harness-ui/src/acp-agent.mjs +350 -0
- package/vendor/harness-ui/src/agent-service.mjs +2188 -0
- package/vendor/harness-ui/src/agui-events.mjs +452 -0
- package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
- package/vendor/harness-ui/src/artifact-store.mjs +39 -0
- package/vendor/harness-ui/src/assistenza.mjs +123 -0
- package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
- package/vendor/harness-ui/src/automation-store.mjs +145 -0
- package/vendor/harness-ui/src/browser-annota.mjs +639 -0
- package/vendor/harness-ui/src/browser-frame.mjs +211 -0
- package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
- package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
- package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
- package/vendor/harness-ui/src/browser-stream.mjs +445 -0
- package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
- package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
- package/vendor/harness-ui/src/config.mjs +697 -0
- package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
- package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
- package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
- package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
- package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
- package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
- package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
- package/vendor/harness-ui/src/context-runtime.mjs +118 -0
- package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
- package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
- package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
- package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
- package/vendor/harness-ui/src/custom-task.mjs +171 -0
- package/vendor/harness-ui/src/doctor.mjs +142 -0
- package/vendor/harness-ui/src/document-filename.mjs +97 -0
- package/vendor/harness-ui/src/document-generator.mjs +493 -0
- package/vendor/harness-ui/src/document-report.mjs +331 -0
- package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
- package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
- package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
- package/vendor/harness-ui/src/forge-contract.mjs +221 -0
- package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
- package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
- package/vendor/harness-ui/src/generation-idle.mjs +332 -0
- package/vendor/harness-ui/src/gguf-header.mjs +207 -0
- package/vendor/harness-ui/src/git-service.mjs +626 -0
- package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
- package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
- package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
- package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
- package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
- package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
- package/vendor/harness-ui/src/hook-registry.mjs +186 -0
- package/vendor/harness-ui/src/http-app.mjs +6259 -0
- package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
- package/vendor/harness-ui/src/id-archivio.mjs +27 -0
- package/vendor/harness-ui/src/image-generator.mjs +143 -0
- package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
- package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
- package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
- package/vendor/harness-ui/src/library-store.mjs +652 -0
- package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
- package/vendor/harness-ui/src/local-model-store.mjs +339 -0
- package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
- package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
- package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
- package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
- package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
- package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
- package/vendor/harness-ui/src/mcp-client.mjs +98 -0
- package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
- package/vendor/harness-ui/src/mcp-session.mjs +177 -0
- package/vendor/harness-ui/src/memory-store.mjs +227 -0
- package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
- package/vendor/harness-ui/src/model-catalog.mjs +129 -0
- package/vendor/harness-ui/src/model-destination.mjs +189 -0
- package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
- package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
- package/vendor/harness-ui/src/notes-store.mjs +250 -0
- package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
- package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
- package/vendor/harness-ui/src/path-policy.mjs +442 -0
- package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
- package/vendor/harness-ui/src/plugin-session.mjs +180 -0
- package/vendor/harness-ui/src/process-policy.mjs +345 -0
- package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
- package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
- package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
- package/vendor/harness-ui/src/provider-probe.mjs +582 -0
- package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
- package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
- package/vendor/harness-ui/src/public-problem.mjs +109 -0
- package/vendor/harness-ui/src/research/card.mjs +235 -0
- package/vendor/harness-ui/src/research/citations.mjs +142 -0
- package/vendor/harness-ui/src/research/collector.mjs +275 -0
- package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
- package/vendor/harness-ui/src/research/dossier.mjs +114 -0
- package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
- package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
- package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
- package/vendor/harness-ui/src/research/independence.mjs +159 -0
- package/vendor/harness-ui/src/research/ledger.mjs +166 -0
- package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
- package/vendor/harness-ui/src/research/narration.mjs +181 -0
- package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
- package/vendor/harness-ui/src/research/opposing.mjs +305 -0
- package/vendor/harness-ui/src/research/outline.mjs +111 -0
- package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
- package/vendor/harness-ui/src/research/pdf.mjs +291 -0
- package/vendor/harness-ui/src/research/plan.mjs +301 -0
- package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
- package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
- package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
- package/vendor/harness-ui/src/research/recheck.mjs +194 -0
- package/vendor/harness-ui/src/research/report.mjs +203 -0
- package/vendor/harness-ui/src/research/run.mjs +527 -0
- package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
- package/vendor/harness-ui/src/research/verification.mjs +572 -0
- package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
- package/vendor/harness-ui/src/research-store.mjs +1133 -0
- package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
- package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
- package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
- package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
- package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
- package/vendor/harness-ui/src/search-source-store.mjs +172 -0
- package/vendor/harness-ui/src/session-registry.mjs +6095 -0
- package/vendor/harness-ui/src/session-store.mjs +220 -0
- package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
- package/vendor/harness-ui/src/setup-stato.mjs +31 -0
- package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
- package/vendor/harness-ui/src/skill-registry.mjs +120 -0
- package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
- package/vendor/harness-ui/src/static-files.mjs +96 -0
- package/vendor/harness-ui/src/stream-partition.mjs +123 -0
- package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
- package/vendor/harness-ui/src/task-catalog.mjs +65 -0
- package/vendor/harness-ui/src/tasks-store.mjs +220 -0
- package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
- package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
- package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
- package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
- package/vendor/harness-ui/src/usage-cache.mjs +315 -0
- package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
- package/vendor/harness-ui/src/workspace-context.mjs +124 -0
- package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
- package/vendor/harness-ui/src/workspace-files.mjs +589 -0
- package/vendor/harness-ui/src/workspace-info.mjs +189 -0
- package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
- package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
- package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
- package/vendor/manifest.json +170 -0
|
@@ -0,0 +1,2679 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* research-orchestrator.mjs — FASE N, ottavo sistema (30/8): Deep
|
|
3
|
+
* Research, "fetta onesta" (owner, AskUserQuestion: "Fetta onesta
|
|
4
|
+
* (consigliato)"). Mirror ARCHITETTURALE di `subagent-orchestrator.mjs`
|
|
5
|
+
* (FASE C): istanziato UNA volta dentro `session-registry.mjs`, dove
|
|
6
|
+
* `avviaESegui`/`sessioni` sono in scope — gli 8 callback `onRicerca*`
|
|
7
|
+
* che `avviaESegui` costruisce sono thin delegate verso i metodi qui,
|
|
8
|
+
* stesso principio di `onDelega` verso `delegaSottoTask`.
|
|
9
|
+
*
|
|
10
|
+
* ⭐⭐⭐ La FORMA che serve è DIVERSA da `delegaSottoTask`: quella BLOCCA
|
|
11
|
+
* (una Promise risolta solo alla conclusione del figlio, per il
|
|
12
|
+
* dispatcher del kernel del PADRE che aspetta un riassunto SINCRONO
|
|
13
|
+
* dentro lo stesso giro). `research_start` deve tornare SUBITO con un
|
|
14
|
+
* id (mobile, `researchTools.ts`: "avvia e torna SUBITO... una
|
|
15
|
+
* ricerca dura minuti, e un tool che aspetta terrebbe occupato il giro
|
|
16
|
+
* di conversazione per tutto quel tempo") — qui `onConclusioneFn` si
|
|
17
|
+
* PASSA e basta, mai atteso: esattamente come `avviaESegui` stesso fa
|
|
18
|
+
* per OGNI sessione (avvia, ritorna `{sessionId}`, il `.then()` gira
|
|
19
|
+
* in background).
|
|
20
|
+
*
|
|
21
|
+
* L'id di una ricerca È il sessionId della sessione che la esegue — un
|
|
22
|
+
* solo spazio di identità, mai una mappatura a parte, mai
|
|
23
|
+
* disallineabile per costruzione.
|
|
24
|
+
*
|
|
25
|
+
* ⭐⭐⭐ Deep Research è PER-PROGETTO (vedi la doc di testa di
|
|
26
|
+
* `research-store.mjs`): la ricerca gira nella STESSA cartella della
|
|
27
|
+
* sessione che l'ha avviata (mai un workspace dedicato) — il suo
|
|
28
|
+
* rapporto finale, salvato in Libreria via `salvaVoceLibreriaFn`,
|
|
29
|
+
* finisce quindi nella Libreria di QUEL progetto, raggiungibile dalle
|
|
30
|
+
* sessioni future sullo stesso progetto (mobile: "i rapporti di
|
|
31
|
+
* ricerca SONO file di Libreria"). `permessi:'Read only'` — la ricerca
|
|
32
|
+
* non deve MAI scrivere/eseguire nel progetto ospite, solo cercare/
|
|
33
|
+
* leggere/sintetizzare: difesa in profondità, il prompt lo dice E il
|
|
34
|
+
* permesso lo garantisce, mai uno solo dei due.
|
|
35
|
+
*
|
|
36
|
+
* ⛔⛔⛔ Pausa/ripresa RIUSANO la macchina già costruita per il bottone
|
|
37
|
+
* Stop (`voce.controller.abort()`) e per `resume()` — non un secondo
|
|
38
|
+
* meccanismo. L'AMBIGUITÀ che questo risolve, trovata leggendo
|
|
39
|
+
* `talosHarness.mjs` (comeSonoFinitiIGiri) prima di scrivere una riga:
|
|
40
|
+
* `comeFinita.esito==='fermato'` copre SIA "ho chiamato io l'abort per
|
|
41
|
+
* una pausa" SIA "il modello ha semplicemente smesso di generare senza
|
|
42
|
+
* rispondere senza che nessuno l'abbia fermato" — indistinguibili dal
|
|
43
|
+
* SOLO esito del kernel. Cura: un flag sulla VOCE stessa (proprietà
|
|
44
|
+
* dinamica, mai nello schema condiviso di `avviaESegui`), scritto
|
|
45
|
+
* PRIMA di abortire, letto e SEMPRE azzerato dentro
|
|
46
|
+
* `onConclusioneRicerca` — mai lasciato sporco per il giro successivo
|
|
47
|
+
* (altrimenti una ripresa che poi conclude DAVVERO verrebbe scambiata
|
|
48
|
+
* per un'altra pausa, silenziosamente: il bug esatto che un test
|
|
49
|
+
* dedicato sotto verifica AL CONTRARIO).
|
|
50
|
+
*
|
|
51
|
+
* ⛔ Nessuno stato "sta girando ORA" è mai scritto su disco (vedi
|
|
52
|
+
* `research-store.mjs`): `statoVivo()` lo deriva SEMPRE dal vivo,
|
|
53
|
+
* confrontando `terminata` (research-store) con lo stato REALE della
|
|
54
|
+
* sessione in `sessioni` — per costruzione non può disallinearsi.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* ⛔ L2 (11/09) — i DUE default del lettore di rapporti. Importati e non ri-scritti: il posto
|
|
59
|
+
* del rapporto e la sua forma minima vivono in `research-store.mjs`, insieme a chi lo scrive.
|
|
60
|
+
* Restano entrambi iniettabili (vedi `creaResearchOrchestrator`), quindi i test non toccano
|
|
61
|
+
* mai un filesystem — stessa disciplina di tutte le altre `*Fn` di questo modulo.
|
|
62
|
+
*/
|
|
63
|
+
import {
|
|
64
|
+
accodaEvento, elencaFonti, leggiFonte, leggiGiornale, leggiIndiceFonti, leggiIstantaneaCache,
|
|
65
|
+
leggiPiano, leggiRapporto, rileggiRapportoMinimo, scriviFonte, scriviIndiceFonti,
|
|
66
|
+
scriviIstantaneaCache, scriviPiano, statRapporto,
|
|
67
|
+
} from './research-store.mjs';
|
|
68
|
+
import { talosResearchFetchCache } from './research/fetch-cache.mjs';
|
|
69
|
+
import { consegnaPartiRapporto, depositaParteRapporto } from './research/deposito-a-pezzi.mjs';
|
|
70
|
+
/*
|
|
71
|
+
* ⭐⭐⭐⭐ L9 (12/09/2026) — IL MOTORE PORTATO COMINCIA A LAVORARE DENTRO LA CORSA.
|
|
72
|
+
*
|
|
73
|
+
* L4 aveva agganciato tre file su venti (`report`, `run`, `verification` per il solo bilancio),
|
|
74
|
+
* L5 un quarto (`recheck`). Il resto — il piano, il collettore, il giudizio vero, la contraria,
|
|
75
|
+
* l'indipendenza, la fedeltà — restava «portato e provato, mai chiamato»: giornale con tre
|
|
76
|
+
* eventi, `Piano` e `Speso` vuoti, e 38 affermazioni su 38 marcate «non verificate» sul giro
|
|
77
|
+
* vero del 12/09 (`CODA-UNICA-DEBITI`, voce «L8 #2 ✅ CHIUDE»).
|
|
78
|
+
*
|
|
79
|
+
* `plan.mjs` i rami per profondità (2/4/6) e il COSTO ATTESO, detto PRIMA di partire;
|
|
80
|
+
* `raccolta-viva.mjs` il ponte per cui `web_search`/`naviga` della figlia diventano passi del
|
|
81
|
+
* giornale, spesa contata, cache che prende e fonti tenute su disco;
|
|
82
|
+
* `verification.mjs` i tre livelli veri, col giudice che NON è l'autore;
|
|
83
|
+
* `opposing.mjs` la contraria cercata apposta;
|
|
84
|
+
* `independence.mjs` le prove distinte, contate a gruppi e non a URL;
|
|
85
|
+
* `fidelity.mjs` il punteggio, con la data — «un punteggio senza data è una promessa che
|
|
86
|
+
* scade in silenzio».
|
|
87
|
+
*
|
|
88
|
+
* ⛔ Nessuno di questi entra nel kernel, oggi come ieri: il kernel riceve UNA porta
|
|
89
|
+
* (`cacheWeb`, la stessa firma di `fetch-cache.around`) e una funzione (`onPaginaLetta`), e
|
|
90
|
+
* non importa una sola riga di `src/research/`.
|
|
91
|
+
*/
|
|
92
|
+
import { talosResearchFidelity } from './research/fidelity.mjs';
|
|
93
|
+
import { talosResearchIndependentSources } from './research/independence.mjs';
|
|
94
|
+
import { talosResearchOpposingPrompt } from './research/opposing.mjs';
|
|
95
|
+
import {
|
|
96
|
+
TALOS_RESEARCH_DEPTHS, talosResearchPlanCost, talosResearchPlanFor, talosResearchPlanTotals,
|
|
97
|
+
} from './research/plan.mjs';
|
|
98
|
+
import { creaRaccoltaViva } from './research/raccolta-viva.mjs';
|
|
99
|
+
/*
|
|
100
|
+
* ⭐⭐⭐ L4 (11/09/2026) — IL MOTORE PORTATO DAL MOBILE ENTRA IN SCENA.
|
|
101
|
+
*
|
|
102
|
+
* Fino a ieri `src/research/` era un albero nuovo che **nessuno chiamava** (L3a/L3b lo dicono
|
|
103
|
+
* entrambi, nel loro «cosa NON ho verificato»): venti file, 275 test verdi, zero chiamanti.
|
|
104
|
+
* Queste tre righe sono il primo aggancio, e agganciano esattamente due cose:
|
|
105
|
+
*
|
|
106
|
+
* `report.mjs` il record recintato ```talos-research-report — il cancello di consegna
|
|
107
|
+
* smette di giudicare la PROSA e comincia a giudicare il RECORD;
|
|
108
|
+
* `run.mjs` il replay del giornale — la ripresa smette di dipendere da
|
|
109
|
+
* `voce.messaggiFinali` (che un riavvio cancella) e riparte dal disco;
|
|
110
|
+
* `verification.mjs` il bilancio delle affermazioni, che è ciò con cui la riga in elenco
|
|
111
|
+
* guida (§6.7: mai col conteggio delle fonti).
|
|
112
|
+
*
|
|
113
|
+
* ⛔ Nessuno di questi import entra nel kernel: `talosHarness.mjs` importa TRE costanti da
|
|
114
|
+
* `research-store.mjs` e niente altro, e `research-store.mjs` continua a non importare nulla
|
|
115
|
+
* da `src/research/`. Il motore vive qui, nel direttore — non nel cancello di sicurezza.
|
|
116
|
+
*/
|
|
117
|
+
import { talosResearchParseReport, talosResearchReportDocument, talosResearchSupportLabel } from './research/report.mjs';
|
|
118
|
+
/*
|
|
119
|
+
* ⭐⭐⭐ L5 (12/09/2026) — IL MOTORE DELLA RI-VERIFICA NEL TEMPO, agganciato per la prima volta.
|
|
120
|
+
*
|
|
121
|
+
* `recheck.mjs` era, come tutto `src/research/`, un albero senza chiamanti. Lo chiama la rotta
|
|
122
|
+
* `POST …/research/:id/riverifica`, ed è il «+1.1» del disegno (§6.8): la riga della tabella
|
|
123
|
+
* dove OGNI concorrente ispezionato ha ❌, perché per farla serve aver tenuto il TESTO, non
|
|
124
|
+
* l'URL.
|
|
125
|
+
*
|
|
126
|
+
* ⛔⛔ E QUI VA DETTO SUBITO COSA OGGI NON SI PUÒ MISURARE, perché il modulo, se lo si chiama
|
|
127
|
+
* senza saperlo, risponde una bugia educata. `talosResearchRecheckReport` vuole
|
|
128
|
+
* `keptByUrl`: il testo tenuto, **per url**. Sul disco di oggi quel testo non è
|
|
129
|
+
* ricostruibile — `fonti/<sha256>.txt` è indirizzato dal CONTENUTO (nessun url nel nome), e
|
|
130
|
+
* il giornale porta `resultRef` ma non l'indirizzo da cui quel testo viene. ⇒ la mappa è
|
|
131
|
+
* vuota, e con una mappa vuota `talosResearchSurvival` torna **1** («niente di ciò su cui ci
|
|
132
|
+
* appoggiavamo è sparito») e la fonte esce **`intact`**. Sarebbe un timbro «intatta» su una
|
|
133
|
+
* pagina che nessuno ha mai confrontato: esattamente il segno di verifica falso che tutto
|
|
134
|
+
* questo disegno esiste per togliere.
|
|
135
|
+
* ⇒ Il lettore qui sotto NON pubblica `intact` quando il testo tenuto manca: pubblica
|
|
136
|
+
* `non-misurabile`, e dice perché. La metà che invece è vera **senza** testo tenuto — «il
|
|
137
|
+
* passaggio citato è ancora ritrovabile in quella pagina?» — si misura eccome, perché il
|
|
138
|
+
* passaggio sta nel record del rapporto, non nel testo tenuto: ed è la metà che `recheck.mjs`
|
|
139
|
+
* stesso dichiara essere «nessuna euristica, nessuna soglia, nessuna opinione».
|
|
140
|
+
*/
|
|
141
|
+
import { talosResearchRecheckReport } from './research/recheck.mjs';
|
|
142
|
+
import {
|
|
143
|
+
talosResearchIsTerminal, talosResearchNextStep, talosResearchRecover,
|
|
144
|
+
talosResearchReplay, talosResearchSpent, talosResearchWorkLeft,
|
|
145
|
+
} from './research/run.mjs';
|
|
146
|
+
import {
|
|
147
|
+
talosResearchJudgePrompt, talosResearchPickJudge, talosResearchVerifiedStanding,
|
|
148
|
+
talosResearchVerify,
|
|
149
|
+
} from './research/verification.mjs';
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* ⭐⭐⭐ L4 §6.5 — IL CANCELLO DI CONSEGNA, SUL RECORD VERO.
|
|
153
|
+
*
|
|
154
|
+
* L2 controllava la PROSA: un titolo, una riga di testo, un URL sotto «## Fonti». Bastava a
|
|
155
|
+
* respingere la scusa da 290 byte dell'11/09 — ed era dichiaratamente un ripiego, perché il
|
|
156
|
+
* record recintato non esisteva ancora nel repo. Adesso esiste (`src/research/report.mjs`,
|
|
157
|
+
* portato dal mobile), e il cancello guarda quello.
|
|
158
|
+
*
|
|
159
|
+
* ⛔ La differenza non è di severità, è di NATURA. La prosa dice «qui c'è un URL»; il record
|
|
160
|
+
* dice **quale affermazione** poggia su **quale fonte**, con quale passaggio e — quando un
|
|
161
|
+
* giudice c'è stato — con quale verdetto. Solo la seconda si può rileggere un mese dopo e
|
|
162
|
+
* ricontrollare. `report.mjs` lo scrive nella sua testa: «un prodotto che conserva gli URL
|
|
163
|
+
* non può farlo a nessun prezzo, perché la prova che ha controllato non c'è più».
|
|
164
|
+
*
|
|
165
|
+
* ⛔ `talosResearchParseReport` torna `null` e non un recupero parziale — è il contratto del
|
|
166
|
+
* mobile, conservato byte per byte dal porto: «un rapporto letto a metà mostrerebbe verdetti
|
|
167
|
+
* accanto ad affermazioni a cui non appartengono, e un segno di verifica sbagliato è peggio
|
|
168
|
+
* di nessuno». Qui `null` diventa «non rilegge», mai «va bene lo stesso».
|
|
169
|
+
*
|
|
170
|
+
* ⛔⛔ IL RIPIEGO, e perché è STRETTO. Le ricerche nate prima dell'11/09 non possono avere il
|
|
171
|
+
* record: quando sono state fatte non esisteva. Respingerle adesso vorrebbe dire timbrare
|
|
172
|
+
* «senza rapporto» su lavoro vero già pagato, cioè l'errore opposto e speculare a quello che
|
|
173
|
+
* L2 ha tolto. ⇒ per quelle, e SOLO per quelle (`ripiegoConsentito`, che il chiamante ricava
|
|
174
|
+
* dal campo `formato` della voce), vale ancora la forma minima — e l'esito lo **dichiara**
|
|
175
|
+
* con `ripiego:true`, invece di far passare due controlli diversi sotto lo stesso `ok`.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} testo
|
|
178
|
+
* @param {{ripiegoConsentito?: boolean}} [opzioni]
|
|
179
|
+
*/
|
|
180
|
+
export function rileggiRapportoRecintato(testo, { ripiegoConsentito = false } = {}) {
|
|
181
|
+
const vuoto = { ok: false, intestazione: null, affermazioni: 0, fonti: [], motivo: null, bilancio: null, proveDistinte: 0, ripiego: false, record: null };
|
|
182
|
+
if (typeof testo !== 'string' || testo.trim().length === 0) {
|
|
183
|
+
return { ...vuoto, motivo: 'il rapporto è vuoto' };
|
|
184
|
+
}
|
|
185
|
+
const record = talosResearchParseReport(testo);
|
|
186
|
+
if (record) {
|
|
187
|
+
const fonti = record.sources.map((f) => f?.url).filter((u) => typeof u === 'string' && u.trim().length > 0);
|
|
188
|
+
const comune = {
|
|
189
|
+
intestazione: typeof record.question === 'string' && record.question.trim() ? record.question.trim() : null,
|
|
190
|
+
affermazioni: record.claims.length,
|
|
191
|
+
fonti,
|
|
192
|
+
bilancio: bilancioDelRecord(record),
|
|
193
|
+
proveDistinte: proveDistinteDelRecord(record),
|
|
194
|
+
ripiego: false,
|
|
195
|
+
record,
|
|
196
|
+
};
|
|
197
|
+
if (record.claims.length === 0) return { ...comune, ok: false, motivo: 'il record del rapporto non porta nessuna affermazione' };
|
|
198
|
+
if (fonti.length === 0) return { ...comune, ok: false, motivo: 'il record del rapporto non elenca nessuna fonte' };
|
|
199
|
+
return { ...comune, ok: true, motivo: null };
|
|
200
|
+
}
|
|
201
|
+
if (!ripiegoConsentito) {
|
|
202
|
+
return { ...vuoto, motivo: 'il rapporto non porta il record verificabile (blocco ```talos-research-report)' };
|
|
203
|
+
}
|
|
204
|
+
const minimo = rileggiRapportoMinimo(testo);
|
|
205
|
+
return { ...minimo, bilancio: null, proveDistinte: 0, ripiego: true, record: null };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* ⭐ §6.7 — IL BILANCIO, che è ciò con cui la riga in elenco guida. Mai il conteggio delle fonti.
|
|
210
|
+
*
|
|
211
|
+
* ⛔ Il motivo è misurato, non estetico: «Sci-MMR» (arXiv:2609.11243, 10/09/2026) trova che
|
|
212
|
+
* l'accuratezza della risposta supera di oltre venti punti il recupero delle prove — cioè un
|
|
213
|
+
* rapporto sembra buono anche quando le prove non ci sono. Un numero di fonti conferma quella
|
|
214
|
+
* impressione; un bilancio la smentisce quando è il caso.
|
|
215
|
+
* ⛔ `nonSostenute` e `nonVerificate` restano SEPARATE, e `contese` sta fuori da tutte:
|
|
216
|
+
* `verification.mjs` lo dice alla riga del conteggio — «"non abbiamo potuto controllarlo" e
|
|
217
|
+
* "abbiamo controllato, e la fonte non lo dice" sono due ammissioni diverse, e fonderle
|
|
218
|
+
* lusingherebbe la seconda». I nomi qui sono in italiano perché in italiano è tutto il
|
|
219
|
+
* contratto verso il frontend (`stato`, `motivo`, `domanda`): due lingue nello stesso oggetto
|
|
220
|
+
* sono due contratti che divergono.
|
|
221
|
+
*/
|
|
222
|
+
function bilancioDelRecord(record) {
|
|
223
|
+
const s = talosResearchVerifiedStanding(record.claims);
|
|
224
|
+
return {
|
|
225
|
+
totali: s.total,
|
|
226
|
+
sostenute: s.supported,
|
|
227
|
+
inParte: s.partial,
|
|
228
|
+
nonSostenute: s.unsupported,
|
|
229
|
+
contese: s.contested,
|
|
230
|
+
nonVerificate: s.unchecked,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* ⭐ Quante fonti DIVERSE portano almeno un passaggio davvero ritrovato nel loro testo.
|
|
236
|
+
*
|
|
237
|
+
* ⛔ Non è `sources.length`, ed è tutta la differenza: la bibliografia dice cosa è stato
|
|
238
|
+
* citato, questo dice su quante fonti distinte poggia davvero qualcosa. Un rapporto con dodici
|
|
239
|
+
* fonti in fondo e un solo passaggio trovato ha `proveDistinte: 1`, e quel numero è l'unico dei
|
|
240
|
+
* due che non si può gonfiare allungando l'elenco dei link.
|
|
241
|
+
* ⛔ Un `passage` vuoto è, nel contratto di `report.mjs`, esattamente «non ci è mai stato
|
|
242
|
+
* trovato» — quindi non conta, e il rapporto in prosa lo scrive pure: «(il passaggio citato
|
|
243
|
+
* non è nel testo della fonte…)».
|
|
244
|
+
*/
|
|
245
|
+
function proveDistinteDelRecord(record) {
|
|
246
|
+
const distinte = new Set();
|
|
247
|
+
for (const c of record.claims) {
|
|
248
|
+
if (typeof c?.passage === 'string' && c.passage.trim().length > 0) distinte.add(c.sourceIndex);
|
|
249
|
+
}
|
|
250
|
+
return distinte.size;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* ⭐⭐⭐⭐ L8 (12/09/2026) — IL RECORD LO SCRIVE IL SERVER, E IL MODELLO PORTA I PEZZI.
|
|
255
|
+
*
|
|
256
|
+
* ⛔ Il guasto, dal giro vero del 12/09 (ricerca `3029dea2`, 18 giri, 5 minuti, 265.670 token
|
|
257
|
+
* di ingresso): la consegna chiedeva al modello di chiudere il rapporto con un blocco
|
|
258
|
+
* recintato ```talos-research-report contenente una riga di JSON. `glm-4.7-flash` ha scritto
|
|
259
|
+
* un rapporto in prosa da 8.953 byte — buono, con numeri e sedici URL — e ha ignorato il
|
|
260
|
+
* recinto. Il cancello di consegna, giustamente, ha risposto `senza-rapporto` («il rapporto
|
|
261
|
+
* non porta il record verificabile», `meta.json:motivoDettaglio`): motivo onesto, ma il
|
|
262
|
+
* lavoro pagato è perso lo stesso.
|
|
263
|
+
*
|
|
264
|
+
* ⛔ Perché la cura NON è insistere nella consegna. Ricerca del 12/09/2026, fonti primarie:
|
|
265
|
+
* - «When Lower Privileges Suffice» (arXiv:2606.20023, 18/06/2026, già citata in L1):
|
|
266
|
+
* «prompt-level controls provide only **limited mitigation**». Una forma che vive solo
|
|
267
|
+
* nella consegna è un controllo a livello di prompt, e il 12/09 non ha retto.
|
|
268
|
+
* - «The Constraint Tax: Measuring Validity-Correctness Tradeoffs in Structured Outputs for
|
|
269
|
+
* Small Language Models» (arXiv:2605.26128v1, 20/05/2026): «hard answer-only schema
|
|
270
|
+
* decoding raises schema validity from 61.5% to 100.0%, **but lowers answer accuracy from
|
|
271
|
+
* 19.7% to 11.0%**». ⇒ ⛔ IL VINCOLO CHE NON CONOSCEVO: costringere un modello piccolo a
|
|
272
|
+
* produrre tutto dentro una forma rigida NON è gratis — si paga in qualità della risposta.
|
|
273
|
+
* Per questo `testo` resta PROSA LIBERA e non viene mai riscritto da noi: la forma rigida
|
|
274
|
+
* si applica allo SCHELETRO (chi afferma cosa, su quale fonte, con quale passaggio), mai
|
|
275
|
+
* al ragionamento.
|
|
276
|
+
* - «Constraint Tax in Open-Weight LLMs: An Empirical Study of Tool Calling Suppression Under
|
|
277
|
+
* Structured Output Constraints» (arXiv:2606.25605v1, 24/06/2026): «when Tool Calling and
|
|
278
|
+
* JSON Schema constraints are simultaneously enabled, multiple open-weight models cease
|
|
279
|
+
* invoking tools despite maintaining high schema compliance». ⇒ niente `response_format`
|
|
280
|
+
* imposto sopra agli attrezzi: la struttura vive negli ARGOMENTI dell'attrezzo — il canale
|
|
281
|
+
* che il modello usa già — e non in un secondo vincolo sopra la generazione.
|
|
282
|
+
* - «PHREEQC-MCQ-200» (arXiv:2607.00436v1, 01/07/2026): «the gains are not monotonic:
|
|
283
|
+
* tool-augmented agents also lose items they answered correctly without tools». ⇒ ogni
|
|
284
|
+
* tolleranza qui sotto (un elenco arrivato come stringa JSON, una fonte indicata per numero
|
|
285
|
+
* invece che per URL, un passaggio mancante) esiste perché la strada nuova non deve poter
|
|
286
|
+
* perdere un deposito che la vecchia avrebbe accettato.
|
|
287
|
+
*
|
|
288
|
+
* ⇒ La forma di oggi: il modello passa `testo` (la prosa), `affermazioni` e `fonti`; QUESTA
|
|
289
|
+
* funzione costruisce il documento con `talosResearchReportDocument` — lo stesso scrittore
|
|
290
|
+
* che il cancello rilegge, mai un secondo che possa divergere («scritti entrambi da un
|
|
291
|
+
* oggetto solo così che non possano divergere», testa di `report.mjs`).
|
|
292
|
+
*
|
|
293
|
+
* ⛔ `judge: null` e `claimSupported: 'unchecked'` NON sono argomenti: li mette il server, e il
|
|
294
|
+
* modello non ha modo di toccarli. È la stessa regola di prima («un modello non timbra sé
|
|
295
|
+
* stesso») resa IMPOSSIBILE da violare invece che raccomandata.
|
|
296
|
+
*
|
|
297
|
+
* ⛔ Il `summary` del record è il `testo` del modello VERBATIM. Conseguenza dichiarata: il
|
|
298
|
+
* documento finale può portare due elenchi di fonti, quello scritto dal modello dentro la sua
|
|
299
|
+
* prosa e quello generato sotto. Preferito a riscrivere la prosa: ciò che il modello ha
|
|
300
|
+
* scritto non si tocca, e un elenco in più si legge — una prosa riscritta no.
|
|
301
|
+
*
|
|
302
|
+
* ⭐⭐⭐⭐ L9 (12/09/2026) — DUE AGGIUNTE, ENTRAMBE ADDITIVE.
|
|
303
|
+
*
|
|
304
|
+
* 1. `testiPerUrl` — il testo TENUTO delle pagine, per indirizzo. Serve a riempire
|
|
305
|
+
* `source.text`, che è l'unica cosa contro cui `talosResearchLocate` può dire se un
|
|
306
|
+
* passaggio citato esiste davvero. Senza (ogni chiamante di ieri, il banco, i test del
|
|
307
|
+
* kernel) il comportamento è **identico byte per byte**: `text: ''`, nessuna verifica
|
|
308
|
+
* possibile, `unchecked` dichiarato — che è quello che succedeva prima di oggi.
|
|
309
|
+
* ⛔ Non cambia il DOCUMENTO: `report.mjs` scrive di una fonte solo url, titolo, data e
|
|
310
|
+
* `obtained`. Il testo serve a chi verifica, non a chi legge.
|
|
311
|
+
* 2. Il ritorno porta anche `intestazione`, `claims` e `sources` — i pezzi già costruiti qui.
|
|
312
|
+
* ⛔ Perché: la verifica ha bisogno esattamente di quelli, e ricostruirli fuori vorrebbe dire
|
|
313
|
+
* un SECONDO interprete degli argomenti del modello (la tolleranza sugli elenchi
|
|
314
|
+
* stringhificati, la fonte indicata per URL o per numero, il passaggio assente contato e
|
|
315
|
+
* non rifiutato). Due interpreti dello stesso argomento divergono al primo caso limite;
|
|
316
|
+
* questo repo l'ha già pagato («due lettori sono due verità»).
|
|
317
|
+
*
|
|
318
|
+
* @param {{domanda?: string|null, testo: unknown, affermazioni: unknown, fonti: unknown, testiPerUrl?: Map<string,string>|null}} input
|
|
319
|
+
* @returns {{ok: true, documento: string, affermazioni: number, fonti: number, senzaPassaggio: number,
|
|
320
|
+
* intestazione: string, claims: object[], sources: object[]}
|
|
321
|
+
* |{ok: false, motivo: string}}
|
|
322
|
+
*/
|
|
323
|
+
export function componiRapportoRicerca({ domanda = null, testo, affermazioni, fonti, testiPerUrl = null }) {
|
|
324
|
+
if (typeof testo !== 'string' || testo.trim().length === 0) {
|
|
325
|
+
return { ok: false, motivo: '`testo` is missing: it must carry the full report as Markdown prose.' };
|
|
326
|
+
}
|
|
327
|
+
const elencoAffermazioni = comeElenco(affermazioni);
|
|
328
|
+
const elencoFonti = comeElenco(fonti);
|
|
329
|
+
if (elencoAffermazioni === null) return { ok: false, motivo: '`affermazioni` must be an array of {testo, fonte, passaggio} objects.' };
|
|
330
|
+
if (elencoFonti === null) return { ok: false, motivo: '`fonti` must be an array of {url, titolo} objects.' };
|
|
331
|
+
if (elencoAffermazioni.length === 0) return { ok: false, motivo: '`affermazioni` is empty: a report with no claims cannot be verified, and is rejected.' };
|
|
332
|
+
if (elencoFonti.length === 0) return { ok: false, motivo: '`fonti` is empty: list every http(s) URL you actually used, with its title.' };
|
|
333
|
+
|
|
334
|
+
const sources = [];
|
|
335
|
+
for (let i = 0; i < elencoFonti.length; i += 1) {
|
|
336
|
+
const f = elencoFonti[i];
|
|
337
|
+
const url = typeof f?.url === 'string' ? f.url.trim() : '';
|
|
338
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
339
|
+
return { ok: false, motivo: `\`fonti[${i}].url\` is not a full http(s) URL${url ? ` (got "${url}")` : ' — it is missing'}. Every source needs the address you actually opened.` };
|
|
340
|
+
}
|
|
341
|
+
const titolo = typeof f?.titolo === 'string' && f.titolo.trim()
|
|
342
|
+
? f.titolo.trim()
|
|
343
|
+
: typeof f?.title === 'string' && f.title.trim() ? f.title.trim() : url;
|
|
344
|
+
const data = typeof f?.dataDichiarata === 'string' && f.dataDichiarata.trim() ? f.dataDichiarata.trim() : null;
|
|
345
|
+
/*
|
|
346
|
+
* ⛔ `obtained` NON si indovina. `report.mjs` lo stampa come «pagina letta» / «solo estratto
|
|
347
|
+
* dal motore di ricerca», e `ledger.mjs:154` ci conta sopra le pagine davvero aperte:
|
|
348
|
+
* metterlo a `'page'` per default dichiarerebbe letta ogni pagina che nessuno ha aperto.
|
|
349
|
+
* Lo dichiara il modello con `letta`, che è l'unico che lo sa; assente ⇒ `'snippet'`, cioè
|
|
350
|
+
* l'ipotesi che promette MENO.
|
|
351
|
+
*/
|
|
352
|
+
/*
|
|
353
|
+
* ⛔ L9 — il testo si cerca con l'indirizzo COM'È e senza barra finale, le stesse due forme
|
|
354
|
+
* con cui `indicePerUrl` qui sotto risolve le citazioni: un url che combacia per le
|
|
355
|
+
* affermazioni e non per il testo produrrebbe «il passaggio non è nel testo della fonte»
|
|
356
|
+
* su una pagina che abbiamo in mano — il motivo giusto per il fatto sbagliato.
|
|
357
|
+
*/
|
|
358
|
+
const tenuto = testiPerUrl
|
|
359
|
+
? (testiPerUrl.get(url) ?? testiPerUrl.get(url.replace(/\/+$/, '')) ?? '')
|
|
360
|
+
: '';
|
|
361
|
+
sources.push({ url, title: titolo, publishedAt: data, obtained: f?.letta === true ? 'page' : 'snippet', text: tenuto });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const indicePerUrl = new Map();
|
|
365
|
+
sources.forEach((s, i) => {
|
|
366
|
+
indicePerUrl.set(s.url, i + 1);
|
|
367
|
+
indicePerUrl.set(s.url.replace(/\/+$/, ''), i + 1);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const claims = [];
|
|
371
|
+
let senzaPassaggio = 0;
|
|
372
|
+
for (let i = 0; i < elencoAffermazioni.length; i += 1) {
|
|
373
|
+
const a = elencoAffermazioni[i];
|
|
374
|
+
const testoAffermazione = typeof a?.testo === 'string' ? a.testo.trim()
|
|
375
|
+
: typeof a?.text === 'string' ? a.text.trim() : '';
|
|
376
|
+
if (!testoAffermazione) {
|
|
377
|
+
return { ok: false, motivo: `\`affermazioni[${i}].testo\` is missing: every claim needs the sentence it asserts.` };
|
|
378
|
+
}
|
|
379
|
+
const fonteGrezza = a?.fonte ?? a?.source ?? a?.sourceIndex;
|
|
380
|
+
let indice = null;
|
|
381
|
+
if (typeof fonteGrezza === 'number' && Number.isInteger(fonteGrezza)) indice = fonteGrezza;
|
|
382
|
+
else if (typeof fonteGrezza === 'string' && fonteGrezza.trim()) {
|
|
383
|
+
const pulita = fonteGrezza.trim();
|
|
384
|
+
indice = indicePerUrl.get(pulita) ?? indicePerUrl.get(pulita.replace(/\/+$/, '')) ?? null;
|
|
385
|
+
if (indice === null && /^\d+$/.test(pulita)) indice = Number(pulita);
|
|
386
|
+
}
|
|
387
|
+
if (indice === null) {
|
|
388
|
+
return {
|
|
389
|
+
ok: false,
|
|
390
|
+
motivo: `\`affermazioni[${i}].fonte\` is missing or matches no URL in \`fonti\`${typeof fonteGrezza === 'string' ? ` (got "${fonteGrezza.trim()}")` : ''}. Use the exact URL, spelled the same way as in \`fonti\`.`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (indice < 1 || indice > sources.length) {
|
|
394
|
+
return { ok: false, motivo: `\`affermazioni[${i}].fonte\` points to source ${indice}, but \`fonti\` lists ${sources.length}. Sources are numbered from 1.` };
|
|
395
|
+
}
|
|
396
|
+
/*
|
|
397
|
+
* ⛔ Un passaggio mancante NON fa cadere il deposito, e si conta. `report.mjs` tratta un
|
|
398
|
+
* `passage` vuoto come «non ci è mai stato trovato» e lo scrive in chiaro nella prosa;
|
|
399
|
+
* `proveDistinte` non lo conta. Rifiutare qui butterebbe un rapporto intero per una
|
|
400
|
+
* citazione — l'errore che «PHREEQC-MCQ-200» chiama «perdere item che si sarebbero presi
|
|
401
|
+
* senza l'attrezzo». Il numero torna al modello nella risposta, così lo sa.
|
|
402
|
+
*/
|
|
403
|
+
const passaggio = typeof a?.passaggio === 'string' ? a.passaggio.trim()
|
|
404
|
+
: typeof a?.passage === 'string' ? a.passage.trim() : '';
|
|
405
|
+
if (!passaggio) senzaPassaggio += 1;
|
|
406
|
+
claims.push({
|
|
407
|
+
claim: { text: testoAffermazione, sourceIndex: indice, quote: passaggio, quotePresent: 'unchecked' },
|
|
408
|
+
passage: passaggio,
|
|
409
|
+
checks: {
|
|
410
|
+
resolved: sources[indice - 1].obtained,
|
|
411
|
+
/*
|
|
412
|
+
* ⛔ `false`, non `true`: nessuno ha confrontato questo passaggio col testo della
|
|
413
|
+
* pagina. `fidelity.mjs:96` conta le affermazioni con `resolved === 'page' &&
|
|
414
|
+
* quotePresent` — un `true` qui sarebbe una verifica mai avvenuta.
|
|
415
|
+
*/
|
|
416
|
+
quotePresent: false,
|
|
417
|
+
quoteSpan: null,
|
|
418
|
+
claimSupported: 'unchecked',
|
|
419
|
+
/*
|
|
420
|
+
* ⛔ IN ITALIANO, e non è un dettaglio: `report.mjs` stampa questa frase nella PROSA
|
|
421
|
+
* («Esito: non verificata — …»), cioè la legge una persona, e tutto il resto di quel
|
|
422
|
+
* documento è italiano. Trovato guardando l'artefatto vero prodotto dalla cura, non
|
|
423
|
+
* da un test: nessuna asserzione poteva vederlo.
|
|
424
|
+
*/
|
|
425
|
+
supportReason: 'depositata dal modello che ha scritto il rapporto: nessun giudice indipendente l\'ha ancora controllata.',
|
|
426
|
+
judge: null,
|
|
427
|
+
judgedAt: null,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const intestazione = typeof domanda === 'string' && domanda.trim() ? domanda.trim() : intestazioneDalTesto(testo);
|
|
433
|
+
const documento = talosResearchReportDocument({
|
|
434
|
+
question: intestazione,
|
|
435
|
+
summary: testo.trim(),
|
|
436
|
+
judge: null,
|
|
437
|
+
claims,
|
|
438
|
+
sources,
|
|
439
|
+
});
|
|
440
|
+
return {
|
|
441
|
+
ok: true, documento, affermazioni: claims.length, fonti: sources.length, senzaPassaggio,
|
|
442
|
+
// ⭐ L9 — i pezzi, per chi deve verificarli. Vedi la doc sopra: un solo interprete.
|
|
443
|
+
intestazione, claims, sources,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Un elenco, anche quando arriva come stringa JSON. `null` = non è un elenco e non lo diventa.
|
|
449
|
+
*
|
|
450
|
+
* ⛔ La tolleranza è misurata, non generosa: un modello che stringhifica un array è un caso
|
|
451
|
+
* visto in natura, e rifiutarlo costerebbe un rapporto intero. Tutto il resto (un oggetto
|
|
452
|
+
* solo, un numero, `undefined`) resta un errore che si dice a parole.
|
|
453
|
+
*/
|
|
454
|
+
function comeElenco(valore) {
|
|
455
|
+
if (Array.isArray(valore)) return valore;
|
|
456
|
+
if (typeof valore === 'string' && valore.trim().startsWith('[')) {
|
|
457
|
+
try {
|
|
458
|
+
const letto = JSON.parse(valore);
|
|
459
|
+
return Array.isArray(letto) ? letto : null;
|
|
460
|
+
} catch { return null; }
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Il titolo del rapporto quando il server non conosce la domanda (ricerche vecchie, riprese). */
|
|
466
|
+
function intestazioneDalTesto(testo) {
|
|
467
|
+
const righe = String(testo).split('\n').map((r) => r.trim());
|
|
468
|
+
const titolo = righe.find((r) => r.startsWith('# '));
|
|
469
|
+
if (titolo) return titolo.slice(2).trim();
|
|
470
|
+
return righe.find((r) => r.length > 0) ?? '';
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* ⭐⭐⭐⭐ L9 — LA CONSEGNA PORTA IL PIANO, e questo è ciò che rende il piano una cosa vera.
|
|
475
|
+
*
|
|
476
|
+
* ⛔ Prima di oggi il piano non esisteva affatto nella corsa: `plan.mjs` era portato, provato e
|
|
477
|
+
* mai chiamato, e la figlia riceveva una guida generica («search from a few different
|
|
478
|
+
* angles»). Un piano che nessuno legge non è un piano: è una decorazione nella scheda.
|
|
479
|
+
*
|
|
480
|
+
* ⛔ Le linee si NUMERANO nella consegna, e l'ordine non è estetico: il giornale attribuisce la
|
|
481
|
+
* k-esima ricerca al k-esimo ramo (`raccolta-viva.mjs`, «l'attribuzione al ramo»). Dire alla
|
|
482
|
+
* figlia di seguirle in ordine è ciò che rende quell'attribuzione onesta invece che casuale.
|
|
483
|
+
* ⇒ Se un giorno qualcuno toglie l'elenco da qui, deve togliere anche quella convenzione là.
|
|
484
|
+
*
|
|
485
|
+
* ⛔ Restano DEFAULT, non gabbie: la consegna dice esplicitamente che una linea può essere
|
|
486
|
+
* saltata o allargata se ciò che si trova lo chiede. Il motore del mobile dice la stessa cosa
|
|
487
|
+
* («Default, non gabbie: il piano resta modificabile»), e un piano che il modello non può
|
|
488
|
+
* disobbedire trasformerebbe una ricerca in uno scraper.
|
|
489
|
+
*/
|
|
490
|
+
function promptRicerca(question, depth, piano = []) {
|
|
491
|
+
const guida = depth === 'quick'
|
|
492
|
+
? 'Keep this brief: a couple of searches are enough — do not over-investigate.'
|
|
493
|
+
: depth === 'exhaustive'
|
|
494
|
+
? 'Be exhaustive: search from many different angles, cross-check the claims that matter, and go deep before writing.'
|
|
495
|
+
: 'Do a thorough pass: search from a few different angles before writing the report.';
|
|
496
|
+
/*
|
|
497
|
+
* ⛔ Un blocco con gli a-capo VERI, non righe fuse dal `join(' ')` finale: un elenco numerato
|
|
498
|
+
* schiacciato su una riga sola è esattamente la forma che un modello legge come prosa e non
|
|
499
|
+
* come lista di compiti. È l'unico pezzo multilinea di questa consegna, ed è voluto.
|
|
500
|
+
*/
|
|
501
|
+
const linee = Array.isArray(piano) && piano.length > 0
|
|
502
|
+
? [[
|
|
503
|
+
`Your plan has ${piano.length} lines of inquiry. Work through them IN THIS ORDER, one web_search each, then open the pages that matter:`,
|
|
504
|
+
...piano.map((ramo, i) => ` ${i + 1}. ${ramo.question}`),
|
|
505
|
+
'Skip or widen a line if what you find asks for it — this is a default, not a cage — but do not reorder it: the journal tracks your progress by that order.',
|
|
506
|
+
].join('\n')]
|
|
507
|
+
: [];
|
|
508
|
+
return [
|
|
509
|
+
`Research this question thoroughly using web_search and naviga: "${question}"`,
|
|
510
|
+
guida,
|
|
511
|
+
...linee,
|
|
512
|
+
/*
|
|
513
|
+
* ⛔⛔⛔ L2 (11/09/2026) — QUESTA FRASE È CAMBIATA, ed è il cuore della cura.
|
|
514
|
+
*
|
|
515
|
+
* Prima diceva: «write your findings as a complete final response (not a tool call) — this
|
|
516
|
+
* text becomes the permanent research report». Cioè il rapporto ERA l'ultimo messaggio. Il
|
|
517
|
+
* 11/09, sulla ricerca `d2a453a8`, l'ultimo messaggio con del testo erano 290 byte di scusa
|
|
518
|
+
* («La sessione è in sola lettura, quindi non posso creare documenti direttamente…»), e
|
|
519
|
+
* sono finiti in Libreria come il rapporto permanente, con `terminata:'done'`.
|
|
520
|
+
*
|
|
521
|
+
* ⇒ Adesso il rapporto è un DEPOSITO esplicito, e la consegna lo dice due volte: come si
|
|
522
|
+
* fa (`research_deposit`) e che l'ultimo messaggio NON è il rapporto. La forma richiesta
|
|
523
|
+
* è dichiarata qui e controllata da `rileggiRapportoMinimo` — mai un cancello che chiede
|
|
524
|
+
* una forma che nessuno ha detto.
|
|
525
|
+
*/
|
|
526
|
+
'Deposita il rapporto con research_deposit seguendo le istruzioni sulle parti qui sotto.',
|
|
527
|
+
/*
|
|
528
|
+
* ⭐⭐⭐⭐ L8 (12/09/2026) — LA CONSEGNA NON CHIEDE PIÙ UN RECINTO, CHIEDE TRE ARGOMENTI.
|
|
529
|
+
*
|
|
530
|
+
* ⛔ Cosa c'era prima, e perché è caduto: le tre righe qui sopra dettavano il blocco
|
|
531
|
+
* ```talos-research-report campo per campo, con un esempio copiabile. Il 12/09
|
|
532
|
+
* `glm-4.7-flash` ha letto quella consegna, ha scritto un rapporto in prosa da 8.953 byte
|
|
533
|
+
* e non ha messo il recinto: il cancello ha risposto `senza-rapporto` e cinque minuti di
|
|
534
|
+
* ricerca sono rimasti senza consegna. Non era una consegna poco chiara — era una forma
|
|
535
|
+
* affidata alla prosa, cioè un controllo a livello di prompt («limited mitigation»,
|
|
536
|
+
* arXiv:2606.20023).
|
|
537
|
+
*
|
|
538
|
+
* ⇒ Adesso la forma sta negli ARGOMENTI dell'attrezzo, dove uno schema la dichiara e il
|
|
539
|
+
* server la costruisce. Qui resta il PERCHÉ, che uno schema non può dire: ogni
|
|
540
|
+
* affermazione porta la sua fonte e il passaggio verbatim, e i verdetti non li dà chi
|
|
541
|
+
* scrive. ⛔ `judge` e `claimSupported` non sono nemmeno più argomenti: non c'è più nulla
|
|
542
|
+
* da raccomandare, perché non c'è più nulla che il modello possa timbrare.
|
|
543
|
+
*/
|
|
544
|
+
'`testo`: prosa Markdown della parte corrente; il rapporto assemblato porta titolo, risultati e fonti, senza riscrivere la prosa.',
|
|
545
|
+
'`affermazioni`: one entry per factual claim that matters, each with `testo` (the claim), `fonte` (the exact http(s) URL it rests on, spelled as in `fonti`) and `passaggio` (the sentence you actually read in that source, copied VERBATIM — never reworded, never invented; "" is the honest answer if you cannot find it, and it is counted as such).',
|
|
546
|
+
'`fonti`: one entry per source, with `url` (full http(s)), `titolo`, `dataDichiarata` (the date the source itself declares, or omit it) and `letta`: true only if you opened the page, false if you only saw a search-result snippet.',
|
|
547
|
+
'The server builds the verifiable record from those three and saves it with your text: you do not have to write any JSON, and you cannot mark your own claims as verified — an independent check happens later.',
|
|
548
|
+
'That deposited document IS the permanent report. Your chat message is not the report and is never saved as one — after depositing, just tell the user in one or two lines that the report is ready.',
|
|
549
|
+
'Note any real uncertainty instead of guessing, and never deposit a report without sources.',
|
|
550
|
+
'You cannot write files, run shell commands or create documents in this project: `research_deposit` is the one and only thing you are allowed to write, and it is all you need.',
|
|
551
|
+
consegnaPartiRapporto(),
|
|
552
|
+
].join(' ');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* ⭐ §6.6 (11/09) — il nome della ricerca: la domanda, troncata a 80 caratteri.
|
|
557
|
+
* ⛔ 80 e non un altro numero: è il tetto che `registro.rinomina()` impone a un nome di sessione
|
|
558
|
+
* (`session-registry.mjs`, «Nome non valido: serve 1-80 caratteri»). Un nome più lungo sarebbe
|
|
559
|
+
* rifiutato da quella porta, e due limiti diversi per la stessa cosa divergono al primo caso
|
|
560
|
+
* limite. Il troncamento taglia sull'ultimo spazio quando può: «Come stanno evolvendo gli…» è
|
|
561
|
+
* leggibile, «Come stanno evolvendo gli harness agen» no.
|
|
562
|
+
*/
|
|
563
|
+
function nomeDallaDomanda(domanda) {
|
|
564
|
+
const pulita = String(domanda ?? '').trim().replace(/\s+/g, ' ');
|
|
565
|
+
if (pulita.length <= 80) return pulita;
|
|
566
|
+
const tagliato = pulita.slice(0, 79);
|
|
567
|
+
const spazio = tagliato.lastIndexOf(' ');
|
|
568
|
+
return `${(spazio > 40 ? tagliato.slice(0, spazio) : tagliato).trimEnd()}…`;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* ⭐⭐⭐ BC-44 — quanto si aspetta prima di riprendere da soli, UNA volta.
|
|
573
|
+
*
|
|
574
|
+
* ⛔ Non è «un backoff» con l'articolo indeterminativo: il backoff a più tentativi esiste già, e
|
|
575
|
+
* sta dentro `chiamaConRitenta` (500·2^n + jitter, quattro tentativi). Quello copre la singola
|
|
576
|
+
* chiamata; questo copre la CORSA, e di tentativi ne ha uno solo — quindi non c'è niente da
|
|
577
|
+
* raddoppiare, c'è da scegliere UN'attesa.
|
|
578
|
+
* ⛔ Venti secondi, e il numero ha una ragione misurabile: l'ultimo tentativo del backoff del
|
|
579
|
+
* kernel cade intorno ai 4 s dal primo (0,5+1+2 più jitter), quindi un'attesa più corta
|
|
580
|
+
* ripartirebbe dentro la stessa finestra che ha appena fallito quattro volte. Venti la
|
|
581
|
+
* scavalca con margine e resta sotto la soglia in cui una persona davanti allo schermo
|
|
582
|
+
* comincia a chiedersi se sia morto tutto.
|
|
583
|
+
* ⛔ Non è misurata su un guasto vero del fornitore: è aritmetica sul backoff che abbiamo. Se
|
|
584
|
+
* un giorno la si vorrà tarare, il dato da raccogliere è la durata dei guasti veri, non
|
|
585
|
+
* l'opinione di chi scrive questa riga.
|
|
586
|
+
*/
|
|
587
|
+
export const ATTESA_RIPRESA_AUTOMATICA_MS = 20_000;
|
|
588
|
+
|
|
589
|
+
const PROMPT_RIPRESA = 'Continue the research from where you left off, using web_search and naviga as needed, then write the final report as your last message.';
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* ⭐⭐⭐ L4 §6.6 — LA CONSEGNA DI UNA RIPRESA DAL GIORNALE.
|
|
593
|
+
*
|
|
594
|
+
* Quando il server è stato riavviato la conversazione non c'è più: al modello non si può
|
|
595
|
+
* ridare il suo contesto, e fingere di averlo sarebbe la bugia. Gli si dà invece quello che il
|
|
596
|
+
* giornale sa davvero — la domanda, quanto è già stato speso, quali linee sono ancora aperte,
|
|
597
|
+
* quale passo era in volo, e dove sta il testo già tenuto.
|
|
598
|
+
*
|
|
599
|
+
* ⛔ Nessuna di queste righe è inventata: ognuna esce dal replay o dal disco. Se una lista è
|
|
600
|
+
* vuota non si scrive («0 rami rimasti» su un giro che non ha mai avuto un piano direbbe che
|
|
601
|
+
* non c'è più niente da fare, che è il contrario del vero) — la riga semplicemente non c'è.
|
|
602
|
+
* ⛔ E la consegna ORIGINALE viene riproposta per intera, perché contiene le istruzioni sul
|
|
603
|
+
* deposito e sulla forma del record: senza, una ricerca ripresa consegnerebbe qualcosa che il
|
|
604
|
+
* cancello respinge — un guasto introdotto dalla cura, cioè il peggiore.
|
|
605
|
+
*/
|
|
606
|
+
function consegnaDiRipresa({ giro, prossimo, rimasti, speso, fonti, task, deposito }) {
|
|
607
|
+
const righe = [
|
|
608
|
+
'This research was interrupted (the app or the server restarted). Its conversation is gone, but its journal is not — here is exactly where it stood.',
|
|
609
|
+
`Question: "${giro.question}"`,
|
|
610
|
+
`Already spent before the interruption: ${speso.tokens} tokens, ${speso.searches} searches, ${speso.pages} pages opened. Do not redo work that is listed as done below.`,
|
|
611
|
+
];
|
|
612
|
+
const fatti = giro.steps.filter((p) => p.state === 'done');
|
|
613
|
+
if (fatti.length > 0) righe.push(`Steps already completed: ${fatti.map((p) => p.id).join(', ')}.`);
|
|
614
|
+
if (prossimo) righe.push(`Resume from this step: ${prossimo.id} (${prossimo.kind}), which was ${prossimo.state === 'interrupted' ? 'in flight when the process died' : 'never started'}.`);
|
|
615
|
+
if (rimasti.length > 0) righe.push(`Lines of inquiry still open: ${rimasti.map((r) => `«${r.question}»`).join(' · ')}.`);
|
|
616
|
+
/*
|
|
617
|
+
* ⛔ L9 — «source text(s)» e non «source page(s)», ed è una parola cambiata su una misura:
|
|
618
|
+
* da oggi in `fonti/` finiscono anche gli ESTRATTI dei risultati di ricerca (una fonte vista
|
|
619
|
+
* ma non aperta è comunque una prova, e senza di lei la verifica direbbe «la fonte citata non
|
|
620
|
+
* esiste fra quelle raccolte»). Il conteggio è quindi di testi tenuti, non di pagine aperte —
|
|
621
|
+
* e chiamarli «pagine» sarebbe un numero gonfiato detto a un modello che ci conta sopra.
|
|
622
|
+
*/
|
|
623
|
+
if (fonti.length > 0) righe.push(`${fonti.length} source text(s) were already fetched and kept on disk; they are available without paying for them again.`);
|
|
624
|
+
if (!prossimo && rimasti.length === 0 && fatti.length === 0) {
|
|
625
|
+
/*
|
|
626
|
+
* ⛔ Il caso di oggi, e si dichiara invece di nasconderlo: il giornale registra il ciclo di
|
|
627
|
+
* vita del giro (avvio, pausa, ripresa, fine) ma non ancora i singoli passi di raccolta,
|
|
628
|
+
* perché il collettore non è agganciato (vedi la testa di questo file). Il modello deve
|
|
629
|
+
* sapere che riparte dall'indagine, non da un passo preciso — non che «non c'è niente da
|
|
630
|
+
* fare».
|
|
631
|
+
*/
|
|
632
|
+
righe.push('The journal records the run itself but not individual collection steps, so restart the investigation for this question from the beginning of the evidence you can still see, and do not assume anything was already concluded.');
|
|
633
|
+
}
|
|
634
|
+
righe.push(typeof task?.consegna === 'string' && task.consegna.trim() ? task.consegna.trim() : PROMPT_RIPRESA);
|
|
635
|
+
righe.push(deposito);
|
|
636
|
+
return righe.join('\n');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* ⛔⛔⛔ L2 (11/09/2026) — QUESTA FUNZIONE HA CAMBIATO NOME, E IL NOME È LA CURA.
|
|
641
|
+
*
|
|
642
|
+
* Si chiamava `estraiTestoRapporto` e la sua doc diceva «quello che diventa il rapporto». Non
|
|
643
|
+
* lo è, e non lo è mai stato: è l'ultima cosa che il modello ha detto. Il 11/09 quella cosa
|
|
644
|
+
* erano 290 byte di scusa, e il solo controllo era «esiste del testo?» — una scusa è testo.
|
|
645
|
+
*
|
|
646
|
+
* ⇒ Adesso si chiama `ultimoMessaggioDelModello` e vale quello che vale: un ALLEGATO, mostrato
|
|
647
|
+
* come «ciò che il modello ha detto alla fine», mai come rapporto. Il rapporto è
|
|
648
|
+
* `.harness-ui-research/<id>/rapporto.md`, depositato con `research_deposit`.
|
|
649
|
+
*/
|
|
650
|
+
function ultimoMessaggioDelModello(messaggiFinali) {
|
|
651
|
+
if (!Array.isArray(messaggiFinali)) return null;
|
|
652
|
+
for (let i = messaggiFinali.length - 1; i >= 0; i -= 1) {
|
|
653
|
+
const m = messaggiFinali[i];
|
|
654
|
+
if (m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim().length > 0) return m.content.trim();
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* ⭐⭐⭐ L2 §6.5 — «fra i risultati degli attrezzi c'è un REFUSED di permesso».
|
|
661
|
+
*
|
|
662
|
+
* ⛔ Cerca la firma ESATTA che il kernel emette (`REFUSED. ` in testa al risultato di un
|
|
663
|
+
* attrezzo, `talosHarness.mjs`), non la parola «permission» né una frase italiana: il motivo
|
|
664
|
+
* del rifiuto è prosa e cambia, il prefisso è un contratto. È la lezione «un filtro che
|
|
665
|
+
* riconosce la MENZIONE invece della cosa» (23/8, il guardiano che accusava la sessione
|
|
666
|
+
* dell'owner) applicata qui: un rapporto che PARLA di un rifiuto non è un rifiuto.
|
|
667
|
+
*
|
|
668
|
+
* ⛔ E guarda i messaggi `role:'tool'`, mai quelli dell'assistente: la scusa del modello cita
|
|
669
|
+
* il rifiuto parola per parola, e prenderla per la prova del rifiuto sarebbe credere al
|
|
670
|
+
* racconto invece che al registro.
|
|
671
|
+
*/
|
|
672
|
+
function trovaRifiutoDiPermesso(messaggiFinali) {
|
|
673
|
+
if (!Array.isArray(messaggiFinali)) return null;
|
|
674
|
+
for (const m of messaggiFinali) {
|
|
675
|
+
if (m?.role !== 'tool') continue;
|
|
676
|
+
const testo = typeof m.content === 'string' ? m.content.trim() : '';
|
|
677
|
+
if (!testo.startsWith('REFUSED. ')) continue;
|
|
678
|
+
/*
|
|
679
|
+
* ⛔ Le uniche altre due frasi che cominciano con `REFUSED. ` nel kernel sono gli input
|
|
680
|
+
* vuoti (`REFUSED. Empty html`, `REFUSED. Empty report`): non sono rifiuti di permesso, e
|
|
681
|
+
* chiamarli così manderebbe l'owner a cambiare un permesso che non c'entra.
|
|
682
|
+
* ⛔ Resta fuori anche la premessa negata di `scrivi` (`REFUSED. <perché> Nothing was
|
|
683
|
+
* written.`) — e non serve escluderla a mano: a livello `'ricerca'` `scrivi` non passa
|
|
684
|
+
* mai il cancello, quindi quel ramo non può essere raggiunto qui. Se un giorno una
|
|
685
|
+
* ricerca girasse a un livello più largo, questa riga andrà rivista: è scritto qui
|
|
686
|
+
* perché l'assunzione sia visibile, non nascosta.
|
|
687
|
+
*/
|
|
688
|
+
if (testo.startsWith('REFUSED. Empty ')) continue;
|
|
689
|
+
return testo;
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function nomeRapporto(domanda) {
|
|
695
|
+
const troncato = String(domanda ?? '').trim().slice(0, 100);
|
|
696
|
+
return `Research - ${troncato || 'untitled'}.md`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Il bucket "vivo" — mai duplicato su disco (vedi la doc di testa).
|
|
701
|
+
* `voceSessione` assente O `interrotta` (il processo che la eseguiva
|
|
702
|
+
* non esiste più, e non aveva mai finito da sola — stesso stato che
|
|
703
|
+
* `resume()` rifiuta onestamente) ⇒ 'failed': mai "running" per un
|
|
704
|
+
* processo che non c'è, mai "paused" per un run che non era mai stato
|
|
705
|
+
* fermato apposta.
|
|
706
|
+
*/
|
|
707
|
+
function statoVivo(voceRicerca, voceSessione) {
|
|
708
|
+
if (voceRicerca.terminata) return voceRicerca.terminata;
|
|
709
|
+
if (!voceSessione || voceSessione.interrotta) return 'failed';
|
|
710
|
+
return voceSessione.conclusa ? 'paused' : 'running';
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/* ══════════════ BC-44 (12/09/2026) — TRANSITORIO NON È FALLITO ══════════════
|
|
714
|
+
*
|
|
715
|
+
* Il fatto che l'ha fatta nascere, misurato e non dedotto: la ricerca `dec896c0` del 12/09 ha
|
|
716
|
+
* lavorato **16 giri, 32 attrezzi, 67 testi tenuti, 134.464 token dalla cache**, ha scritto
|
|
717
|
+
* «Poi deposito.» — e a quel punto il fornitore ha chiuso:
|
|
718
|
+
*
|
|
719
|
+
* {"type":"RunError","message":"Upstream idle timeout exceeded","code":"internal-error"}
|
|
720
|
+
*
|
|
721
|
+
* ⇒ `terminata:'failed'`, e `POST …/research/:id/ripresa` rispondeva **409**: venti minuti
|
|
722
|
+
* pagati che il giornale conserva per intero e che nessuno poteva riprendere.
|
|
723
|
+
*
|
|
724
|
+
* ## Le fonti, lette il 12/09/2026 (ricerca PRIMA di scrivere, obbligo owner)
|
|
725
|
+
*
|
|
726
|
+
* ⛔ **OpenRouter, «Errors»** — il vincolo che non conoscevo, e che decide tutto:
|
|
727
|
+
* «If an attempt fails before any tokens reach you, OpenRouter automatically tries a backup
|
|
728
|
+
* provider. The `200 OK` has already been sent by then, so the status stays `200` even when
|
|
729
|
+
* every provider fails.»
|
|
730
|
+
* «a `200 OK` whose JSON body holds only an `error` object and no `choices`; check the body
|
|
731
|
+
* for an `error` field even on a `200`.»
|
|
732
|
+
* ⇒ Il ritentativo del kernel (`chiamaConRitenta`, `siRitenta(stato)`) **non può** coprire
|
|
733
|
+
* questa classe: decide sullo STATO HTTP, e qui lo stato è 200. Non è una dimenticanza del
|
|
734
|
+
* kernel — è una cura che sta a valle del punto in cui l'errore nasce. Per questo la ripresa
|
|
735
|
+
* va messa al livello della RICERCA, dove c'è il giornale, e non al livello della chiamata.
|
|
736
|
+
*
|
|
737
|
+
* ⛔ **Hermes Agent v0.21 (Nous Research)**, letto nel suo codice — `agent/error_classifier.py`
|
|
738
|
+
* e `docs/session-lifecycle.md`. Due cose, entrambe copiate qui nella forma, non nel testo:
|
|
739
|
+
* 1. un classificatore CENTRALE con un campo `retryable` esplicito, che «replaces scattered
|
|
740
|
+
* inline string-matching», e un ordine di precedenza dichiarato (billing prima di
|
|
741
|
+
* rate-limit, SSL prima di disconnect, disconnect prima del catch-all);
|
|
742
|
+
* 2. `resume_pending` (recupero MORBIDO: «preserves the existing session_id — the user
|
|
743
|
+
* continues on the same transcript») tenuto separato da `suspended` («hard force-wipe
|
|
744
|
+
* signal»), con un `resume_reason` che dice PERCHÉ la ripresa è stata segnata.
|
|
745
|
+
* ⇒ Qui: un `failed` transitorio è `resume_pending`, un `failed` non transitorio resta
|
|
746
|
+
* quello che era. E il perché si scrive: `motivoErrore.classe`.
|
|
747
|
+
* ⛔ Hermes annota anche il nostro caso alla lettera: un disconnect su un modello che ragiona è
|
|
748
|
+
* «the upstream proxy idle-killing a long thinking stream», non un contesto pieno — e la cura
|
|
749
|
+
* sbagliata (comprimere) «silently delete[s] conversation history on a phantom
|
|
750
|
+
* context-length error». Per questo `contesto` qui NON è transitorio.
|
|
751
|
+
*
|
|
752
|
+
* ⛔ **Anthropic, «How we built our multi-agent research system»**: «When errors occur, we can't
|
|
753
|
+
* just restart from the beginning: restarts are expensive and frustrating for users. Instead,
|
|
754
|
+
* we built systems that can resume from where the agent was when the errors occurred», e
|
|
755
|
+
* «deterministic safeguards like retry logic and regular checkpoints». Il nostro checkpoint
|
|
756
|
+
* esiste già ed è il giornale: mancava solo il permesso di rientrarci.
|
|
757
|
+
*
|
|
758
|
+
* ## ⛔ DOVE DIVERGO DA HERMES, e perché
|
|
759
|
+
*
|
|
760
|
+
* Hermes manda `unknown` a `retryable=True`. Qui `ignoto` è **NON transitorio**, e non è una
|
|
761
|
+
* svista: là si ritenta LA STESSA CHIAMATA (costo: una chiamata), qui si riapre UNA RICERCA che
|
|
762
|
+
* può spendere venti minuti e centinaia di migliaia di token. Costi diversi ⇒ default diversi.
|
|
763
|
+
* Un guasto deterministico dichiarato «riprendibile» farebbe ripagare un fallimento garantito.
|
|
764
|
+
* ⇒ Si riprende solo ciò che si è RICONOSCIUTO. Quello che non si riconosce si comporta
|
|
765
|
+
* esattamente come ieri: nessuna regressione possibile da questa riga.
|
|
766
|
+
*
|
|
767
|
+
* ⛔ E il `code` da solo NON basta: `agent-service.mjs` marca `internal-error` OGNI guasto del
|
|
768
|
+
* servizio — il nostro caso incluso. Il codice si guarda per primo quando dice qualcosa
|
|
769
|
+
* (`CTX_*`, gli esiti del task), e per il resto si legge il MESSAGGIO, che è l'unica cosa che
|
|
770
|
+
* il fornitore ha davvero detto.
|
|
771
|
+
*/
|
|
772
|
+
|
|
773
|
+
/** Le classi che si riprendono. ⛔ Elenco chiuso: chi non è qui dentro NON è transitorio. */
|
|
774
|
+
const CLASSI_TRANSITORIE = new Set(['rete', 'timeout-fornitore', 'traffico', 'guasto-fornitore', 'flusso-interrotto']);
|
|
775
|
+
|
|
776
|
+
/** La mezza frase italiana di ogni classe — il pezzo variabile di `motivoDelloStato`. */
|
|
777
|
+
const CLAUSOLA_DI_CLASSE = new Map([
|
|
778
|
+
['rete', 'la connessione con il fornitore del modello è caduta'],
|
|
779
|
+
['timeout-fornitore', 'il fornitore del modello ha chiuso la connessione mentre lavorava'],
|
|
780
|
+
['traffico', 'il fornitore del modello ha rifiutato per troppo traffico'],
|
|
781
|
+
['guasto-fornitore', 'il fornitore del modello ha risposto con un guasto suo'],
|
|
782
|
+
['flusso-interrotto', 'la risposta del modello si è interrotta a metà'],
|
|
783
|
+
]);
|
|
784
|
+
|
|
785
|
+
/* Gli esiti del TASK: non sono guasti, e hanno già il loro stato. */
|
|
786
|
+
const CODICI_ESITO_DEL_TASK = new Map([['fermato', 'fermato'], ['giri-esauriti', 'giri-esauriti'], ['premesse-negate', 'premesse-negate']]);
|
|
787
|
+
|
|
788
|
+
/*
|
|
789
|
+
* ⛔ L'ORDINE È LA CURA, non l'elenco. Le prime due famiglie sono NON transitorie e vanno
|
|
790
|
+
* guardate PRIMA delle transitorie, perché le loro frasi contengono le parole delle altre:
|
|
791
|
+
* «you have exceeded your current quota» porta dentro «exceeded», «insufficient credits …
|
|
792
|
+
* rate limit» porta dentro «rate limit». Chi legge per primo vince, quindi legge per primo
|
|
793
|
+
* chi non si deve ritentare. (È la stessa precedenza di Hermes: billing prima di rate_limit.)
|
|
794
|
+
*/
|
|
795
|
+
const SEGNI_CREDITO = ['insufficient credit', 'insufficient_quota', 'insufficient balance', 'credit balance', 'payment required', 'exceeded your current quota', 'out of funds', 'billing'];
|
|
796
|
+
const SEGNI_CREDENZIALE = ['unauthorized', 'invalid api key', 'no auth credentials', 'authentication', 'forbidden', 'http 401', 'http 403'];
|
|
797
|
+
const SEGNI_CONTESTO = ['context length', 'context_length', 'maximum context', 'too many tokens', 'prompt is too long', 'reduce the length'];
|
|
798
|
+
const SEGNI_RICHIESTA = ['invalid request', 'bad request', 'model not found', 'is not a valid model', 'no endpoints found', 'http 400', 'http 404'];
|
|
799
|
+
const SEGNI_TRAFFICO = ['rate limit', 'rate-limit', 'too many requests', 'http 429', 'temporarily rate-limited'];
|
|
800
|
+
const SEGNI_TIMEOUT = ['idle timeout', 'timeout', 'timed out', 'deadline exceeded', 'http 408', 'http 504', 'http 524'];
|
|
801
|
+
const SEGNI_GUASTO = ['bad gateway', 'service unavailable', 'gateway timeout', 'overloaded', 'at capacity', 'over capacity', 'internal server error', 'upstream error', 'provider returned error', 'http 500', 'http 502', 'http 503'];
|
|
802
|
+
const SEGNI_RETE = ['econnreset', 'econnrefused', 'etimedout', 'enotfound', 'eai_again', 'epipe', 'fetch failed', 'socket hang up', 'connection reset', 'connection refused', 'network', 'terminated'];
|
|
803
|
+
const SEGNI_FLUSSO = ['flusso sse', 'unexpected eof', 'premature close', 'stream ended', 'incomplete chunked'];
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* ⭐⭐⭐ BC-44 — LA TABELLA, in una funzione pura che un test può mordere da sola.
|
|
807
|
+
*
|
|
808
|
+
* @param {{codice?: string|null, messaggio?: string|null}} errore
|
|
809
|
+
* @returns {{classe: string, transitorio: boolean}}
|
|
810
|
+
*/
|
|
811
|
+
export function classificaErroreDiCorsa({ codice = null, messaggio = null } = {}) {
|
|
812
|
+
const c = typeof codice === 'string' ? codice.trim() : '';
|
|
813
|
+
const m = typeof messaggio === 'string' ? messaggio.toLowerCase() : '';
|
|
814
|
+
const dentro = (segni) => segni.some((s) => m.includes(s));
|
|
815
|
+
const esito = (classe) => ({ classe, transitorio: CLASSI_TRANSITORIE.has(classe) });
|
|
816
|
+
|
|
817
|
+
// 1. Il codice, quando dice davvero qualcosa. `internal-error` NON dice niente: è il default.
|
|
818
|
+
if (CODICI_ESITO_DEL_TASK.has(c)) return esito(CODICI_ESITO_DEL_TASK.get(c));
|
|
819
|
+
if (c.startsWith('CTX_')) return esito('contesto');
|
|
820
|
+
// P-L (12/09): i guasti dell'agente esterno ACP hanno la loro classe, così una ricerca ricostruita dal codice salvato non torna «ignoto».
|
|
821
|
+
if (c === 'ACP_PROCESS_EXITED') return esito('flusso-interrotto');
|
|
822
|
+
if (c === 'ACP_TIMEOUT') return esito('timeout-fornitore');
|
|
823
|
+
if (c === 'ACP_CANCELLED') return esito('fermato');
|
|
824
|
+
/*
|
|
825
|
+
* ⛔ `fermatoSuRichiesta` arriva come messaggio, non come codice: il kernel lancia «⛔ fermato
|
|
826
|
+
* su richiesta …» e `agent-service` lo marca `internal-error` come tutto il resto. Uno stop
|
|
827
|
+
* voluto non si riprende da solo, mai — sarebbe ripartire contro chi ha premuto Ferma.
|
|
828
|
+
*/
|
|
829
|
+
if (m.includes('fermato su richiesta')) return esito('fermato');
|
|
830
|
+
|
|
831
|
+
// 2. Le NON transitorie che contengono le parole delle transitorie: prima loro (vedi sopra).
|
|
832
|
+
if (dentro(SEGNI_CREDITO)) return esito('credito');
|
|
833
|
+
if (dentro(SEGNI_CREDENZIALE)) return esito('credenziale');
|
|
834
|
+
if (dentro(SEGNI_CONTESTO)) return esito('contesto');
|
|
835
|
+
if (dentro(SEGNI_RICHIESTA)) return esito('richiesta-non-valida');
|
|
836
|
+
|
|
837
|
+
// 3. Le transitorie.
|
|
838
|
+
if (dentro(SEGNI_TRAFFICO)) return esito('traffico');
|
|
839
|
+
if (dentro(SEGNI_TIMEOUT)) return esito('timeout-fornitore');
|
|
840
|
+
if (dentro(SEGNI_GUASTO)) return esito('guasto-fornitore');
|
|
841
|
+
if (dentro(SEGNI_RETE)) return esito('rete');
|
|
842
|
+
if (dentro(SEGNI_FLUSSO)) return esito('flusso-interrotto');
|
|
843
|
+
|
|
844
|
+
// 4. Non riconosciuto ⇒ si comporta come ieri. Vedi «DOVE DIVERGO DA HERMES».
|
|
845
|
+
return esito('ignoto');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* ⭐⭐⭐⭐ BC-44 — LA PROVA C'ERA GIÀ, E NESSUNO LA LEGGEVA.
|
|
850
|
+
*
|
|
851
|
+
* ⛔ Il difetto che questa funzione chiude è più grande di quello che sembra. La cura scritta in
|
|
852
|
+
* `onConclusioneRicerca` vale **da oggi in avanti**: ogni ricerca caduta PRIMA — compresa
|
|
853
|
+
* `dec896c0`, quella che ha fatto nascere BC-44, coi suoi venti minuti pagati — avrebbe avuto
|
|
854
|
+
* `motivoErrore: null` per sempre, e sarebbe rimasta non riprendibile. Una cura che non cura
|
|
855
|
+
* il caso che l'ha fatta scrivere.
|
|
856
|
+
*
|
|
857
|
+
* ⇒ Ma il fatto è registrato lo stesso, e lo era da sempre: nel `.jsonl` della SESSIONE c'è
|
|
858
|
+
* {"type":"RunError","message":"Upstream idle timeout exceeded","code":"internal-error"}
|
|
859
|
+
* e `ripristina()` rimette quegli eventi in `voce.eventi` a ogni avvio del server. Non serve
|
|
860
|
+
* leggere un altro file: basta guardare quello che il registro ha già in mano.
|
|
861
|
+
*
|
|
862
|
+
* ⛔ Si scandisce ALL'INDIETRO e ci si ferma al primo `RunStarted`: gli eventi di una sessione
|
|
863
|
+
* sono la storia di TUTTI i suoi giri, e un `RunError` di tre giri fa non dice niente sul giro
|
|
864
|
+
* che è appena caduto. Senza questa fermata, una ricerca ripresa e poi conclusa bene si
|
|
865
|
+
* porterebbe dietro il motivo della sua prima caduta.
|
|
866
|
+
* ⛔ E non si scrive niente sul disco: la deduzione vive nella LETTURA, come la correzione degli
|
|
867
|
+
* stati di `elenca()`. Ciò che è costato denaro non si riscrive per far quadrare un campo.
|
|
868
|
+
*
|
|
869
|
+
* @returns {{codice: string|null, messaggio: string|null}|null}
|
|
870
|
+
*/
|
|
871
|
+
function ultimaCadutaDegliEventi(eventi) {
|
|
872
|
+
if (!Array.isArray(eventi)) return null;
|
|
873
|
+
for (let i = eventi.length - 1; i >= 0; i -= 1) {
|
|
874
|
+
const e = eventi[i];
|
|
875
|
+
if (e?.type === 'RunStarted') return null;
|
|
876
|
+
if (e?.type === 'RunFinished') return null;
|
|
877
|
+
if (e?.type === 'RunError') {
|
|
878
|
+
return {
|
|
879
|
+
codice: typeof e.code === 'string' ? e.code : null,
|
|
880
|
+
messaggio: typeof e.message === 'string' ? e.message : null,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* La causa della caduta di una ricerca `failed`: quella REGISTRATA se c'è, altrimenti quella
|
|
889
|
+
* DEDOTTA dagli eventi della sessione. `null` quando non c'è né l'una né l'altra.
|
|
890
|
+
*
|
|
891
|
+
* ⛔ `registrata` prima di `dedotta`, sempre: la prima l'ha scritta chi era presente alla
|
|
892
|
+
* conclusione, la seconda è una rilettura. Quando ci sono entrambe non possono che coincidere
|
|
893
|
+
* — ma l'ordine va comunque dichiarato, perché il giorno in cui divergessero vince il testimone.
|
|
894
|
+
*/
|
|
895
|
+
function causaDellaCaduta(record, voceSessione) {
|
|
896
|
+
if (record?.motivoErrore) return record.motivoErrore;
|
|
897
|
+
const grezza = ultimaCadutaDegliEventi(voceSessione?.eventi);
|
|
898
|
+
if (!grezza) return null;
|
|
899
|
+
return { ...classificaErroreDiCorsa(grezza), codice: grezza.codice, messaggio: grezza.messaggio, dedotta: true };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* ⭐⭐⭐ L2 §6.5 — LA FRASE UMANA. Non un codice tradotto dal frontend: il motivo lo dice il
|
|
904
|
+
* server, in una frase sola, e in italiano.
|
|
905
|
+
*
|
|
906
|
+
* ⛔ `null` quando lo stato è `done` o quando la ricerca sta ancora girando: un «motivo» su
|
|
907
|
+
* una cosa riuscita è rumore, e su una in corso è una previsione.
|
|
908
|
+
* ⛔ Le frasi NOMINANO IL COLPEVOLE quando il colpevole siamo noi. «La sessione era in sola
|
|
909
|
+
* lettura e non ha potuto depositare il rapporto» è il prodotto che ammette il proprio
|
|
910
|
+
* errore; «ricerca fallita» sarebbe farlo pagare all'owner.
|
|
911
|
+
*/
|
|
912
|
+
function motivoDelloStato(stato, dettaglio = null, motivoErrore = null) {
|
|
913
|
+
/*
|
|
914
|
+
* ⭐⭐⭐ BC-44 — quando la caduta è transitoria la frase cambia, e cambia in due punti: dice
|
|
915
|
+
* CHI è caduto (mai «la ricerca non ce l'ha fatta»: non è stata lei) e dice che si riprende.
|
|
916
|
+
* ⛔ La frase NON nomina il codice né il messaggio del fornitore: «Upstream idle timeout
|
|
917
|
+
* exceeded» a schermo sarebbe un nome tecnico, e quelli non entrano nella UI.
|
|
918
|
+
*/
|
|
919
|
+
if (stato === 'failed' && motivoErrore?.transitorio === true && CLAUSOLA_DI_CLASSE.has(motivoErrore.classe)) {
|
|
920
|
+
return `La ricerca si è interrotta a metà: ${CLAUSOLA_DI_CLASSE.get(motivoErrore.classe)}. Il lavoro già fatto è conservato e può riprendere da lì.`;
|
|
921
|
+
}
|
|
922
|
+
switch (stato) {
|
|
923
|
+
case 'bloccata-dal-permesso':
|
|
924
|
+
return 'La sessione era in sola lettura e non ha potuto depositare il rapporto: il lavoro è stato fatto, la consegna no.';
|
|
925
|
+
case 'senza-rapporto':
|
|
926
|
+
return dettaglio
|
|
927
|
+
? `La ricerca è finita ma il rapporto non è leggibile: ${dettaglio}.`
|
|
928
|
+
: 'La ricerca è finita senza depositare un rapporto leggibile.';
|
|
929
|
+
case 'giri-esauriti':
|
|
930
|
+
return 'La ricerca ha esaurito i giri a disposizione prima di depositare il rapporto.';
|
|
931
|
+
case 'cancelled':
|
|
932
|
+
return 'La ricerca è stata fermata per sempre: quello che aveva raccolto resta leggibile.';
|
|
933
|
+
case 'paused':
|
|
934
|
+
return 'La ricerca è in pausa: può riprendere da dove si era fermata.';
|
|
935
|
+
case 'failed':
|
|
936
|
+
return dettaglio ? `La ricerca non è arrivata in fondo: ${dettaglio}.` : 'La ricerca non è arrivata in fondo.';
|
|
937
|
+
default:
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function clampNumero(valore, min, max, difetto) {
|
|
943
|
+
const numero = Number(valore);
|
|
944
|
+
return Number.isFinite(numero) ? Math.min(Math.max(numero, min), max) : difetto;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
export function creaResearchOrchestrator({
|
|
948
|
+
sessioni, avviaESeguiFn,
|
|
949
|
+
creaRicercaFn, leggiRicercaFn, aggiornaRicercaFn, eliminaRicercaFn, elencaRicercheFn,
|
|
950
|
+
salvaVoceLibreriaFn, leggiVoceLibreriaFn, eliminaVoceLibreriaFn,
|
|
951
|
+
randomUUIDFn,
|
|
952
|
+
/*
|
|
953
|
+
* ⭐⭐⭐ L2 §6.5 (11/09) — IL PUNTO DI INNESTO DEL LETTORE DI RAPPORTI.
|
|
954
|
+
*
|
|
955
|
+
* `leggiRapportoFn({cartella, id}) => Promise<string|null>` legge il file depositato;
|
|
956
|
+
* `rileggiRapportoFn(testo) => {ok, intestazione, affermazioni, fonti, motivo}` lo giudica.
|
|
957
|
+
*
|
|
958
|
+
* ⛔ Sono DUE e non uno per la stessa ragione per cui `salvaVoceLibreriaFn` è iniettata: il
|
|
959
|
+
* default legge il disco vero (`research-store.mjs`), i test non toccano un filesystem.
|
|
960
|
+
* ⛔ E `rileggiRapportoFn` è iniettabile perché il record recintato vero
|
|
961
|
+
* (`src/research/report.mjs`, portato dal mobile da un altro agente in parallelo) non
|
|
962
|
+
* esiste ancora in questo repo: il giorno in cui esiste si cambia QUESTA riga, non la
|
|
963
|
+
* macchina degli stati qui sotto. La forma minima di oggi è dichiarata, non implicita —
|
|
964
|
+
* vedi `rileggiRapportoMinimo`.
|
|
965
|
+
*/
|
|
966
|
+
leggiRapportoFn = leggiRapporto,
|
|
967
|
+
/*
|
|
968
|
+
* ⭐⭐⭐ L4 — IL PUNTO DI INNESTO È STATO USATO. L2 aveva lasciato qui `rileggiRapportoMinimo`
|
|
969
|
+
* scrivendo «il giorno in cui il record recintato esiste si cambia QUESTA riga, non la
|
|
970
|
+
* macchina degli stati». Quel giorno è oggi: il default è il lettore del record vero, e la
|
|
971
|
+
* forma minima resta dietro, come ripiego dichiarato per le ricerche vecchie.
|
|
972
|
+
* ⛔ La firma è cresciuta di un argomento (`{ripiegoConsentito}`): un lettore iniettato che lo
|
|
973
|
+
* ignora continua a funzionare — è il motivo per cui è un oggetto di opzioni e non un
|
|
974
|
+
* secondo parametro posizionale obbligatorio.
|
|
975
|
+
*/
|
|
976
|
+
rileggiRapportoFn = rileggiRapportoRecintato,
|
|
977
|
+
/*
|
|
978
|
+
* ⭐⭐⭐ L4 — LE QUATTRO PORTE NUOVE SUL DISCO DELLA RICERCA, tutte iniettabili come le altre.
|
|
979
|
+
*
|
|
980
|
+
* `accodaEventoFn` scrive UNA riga nel giornale (solo append);
|
|
981
|
+
* `leggiGiornaleFn` lo rilegge tollerando le righe mozzate — è ciò da cui `riprendi()` rigioca;
|
|
982
|
+
* `leggiPianoFn` il piano approvato, quando c'è;
|
|
983
|
+
* `statRapportoFn` `mtimeMs`+`size` del rapporto: la chiave della cache di `elenca()`.
|
|
984
|
+
*
|
|
985
|
+
* ⛔ Iniettabili per la ragione di sempre, e stavolta con un caso già visto: senza queste
|
|
986
|
+
* porte un test dell'orchestratore andrebbe a scrivere sul filesystem VERO della macchina
|
|
987
|
+
* che lo esegue — cioè misurerebbe l'ambiente invece dell'oggetto (lezione 10/09).
|
|
988
|
+
*/
|
|
989
|
+
accodaEventoFn = accodaEvento,
|
|
990
|
+
leggiGiornaleFn = leggiGiornale,
|
|
991
|
+
leggiPianoFn = leggiPiano,
|
|
992
|
+
statRapportoFn = statRapporto,
|
|
993
|
+
elencaFontiFn = elencaFonti,
|
|
994
|
+
/*
|
|
995
|
+
* ⭐ L4+L6 — la cache del fetch, persistita accanto al giornale (`<id>/cache.json`).
|
|
996
|
+
* `creaCacheFetchFn` è la fabbrica, iniettabile come tutto il resto: i test non devono
|
|
997
|
+
* dipendere dall'orologio vero né dai tetti di default.
|
|
998
|
+
*/
|
|
999
|
+
leggiIstantaneaCacheFn = leggiIstantaneaCache,
|
|
1000
|
+
scriviIstantaneaCacheFn = scriviIstantaneaCache,
|
|
1001
|
+
creaCacheFetchFn = talosResearchFetchCache,
|
|
1002
|
+
/*
|
|
1003
|
+
* ⭐⭐⭐⭐ L9 (12/09/2026) — LE PORTE NUOVE, e perché sono tutte iniettabili come le altre.
|
|
1004
|
+
*
|
|
1005
|
+
* Le quattro del DISCO (`scriviPianoFn`, `scriviFonteFn`, `leggiFonteFn`, e le due
|
|
1006
|
+
* dell'indice) per la ragione di sempre, già pagata: senza, un test di questo modulo
|
|
1007
|
+
* scriverebbe nel filesystem VERO della macchina che lo esegue — «misuravo l'ambiente invece
|
|
1008
|
+
* dell'oggetto», lezione del 10/09, trovata dal vivo proprio in L4.
|
|
1009
|
+
*
|
|
1010
|
+
* ⛔⛔ Le due del MODELLO (`pianificaFn`, `chiediAlModelloFn`) sono `null` per default, e il
|
|
1011
|
+
* `null` è un comportamento dichiarato, non un buco:
|
|
1012
|
+
* - senza `pianificaFn` il piano è quello DETERMINISTICO di `plan.mjs` (rami per
|
|
1013
|
+
* profondità, facce ordinate). È già un piano vero e non costa un token;
|
|
1014
|
+
* - senza `chiediAlModelloFn` NON C'È GIUDICE: le affermazioni escono `unchecked` col
|
|
1015
|
+
* motivo scritto («nessun giudice indipendente disponibile: l'autore non può verificare
|
|
1016
|
+
* sé stesso») e il rapporto porta `judge: null`. ⛔ Mai il ripiego opposto — far
|
|
1017
|
+
* timbrare al modello le proprie affermazioni — che è il guasto misurato da
|
|
1018
|
+
* Panickssery/Bowman/Feng (arXiv:2404.13076): «a linear correlation between
|
|
1019
|
+
* self-recognition capability and the strength of self-preference bias».
|
|
1020
|
+
* ⇒ i test di questo file girano con modello e rete FINTI, sempre, e il giro vero lo lancia
|
|
1021
|
+
* l'owner sul 4174.
|
|
1022
|
+
*/
|
|
1023
|
+
scriviPianoFn = scriviPiano,
|
|
1024
|
+
scriviFonteFn = scriviFonte,
|
|
1025
|
+
leggiFonteFn = leggiFonte,
|
|
1026
|
+
scriviIndiceFontiFn = scriviIndiceFonti,
|
|
1027
|
+
leggiIndiceFontiFn = leggiIndiceFonti,
|
|
1028
|
+
pianificaFn = null,
|
|
1029
|
+
chiediAlModelloFn = null,
|
|
1030
|
+
/*
|
|
1031
|
+
* I modelli che potrebbero fare da giudice, nell'ordine in cui vale la pena interpellarli.
|
|
1032
|
+
* ⛔ La scelta NON si fa qui: la fa `talosResearchPickJudge`, che è «chiunque tranne
|
|
1033
|
+
* l'autore» e ha i suoi test. Questa funzione dice solo CHI c'è.
|
|
1034
|
+
*/
|
|
1035
|
+
modelliGiudiceFn = () => [],
|
|
1036
|
+
/*
|
|
1037
|
+
* Il prezzo pubblicato del modello, quando lo si è ottenuto. ⛔ `null` = non lo sappiamo, e
|
|
1038
|
+
* allora il costo si dice in LAVORO (ricerche, pagine, token) e mai in denaro: «denaro solo se
|
|
1039
|
+
* un prezzo pubblicato è stato ottenuto» (§6.8, +1.5).
|
|
1040
|
+
*/
|
|
1041
|
+
prezzoFn = () => null,
|
|
1042
|
+
/*
|
|
1043
|
+
* ⭐⭐⭐ L5 (12/09/2026) — LA LETTURA DI UNA PAGINA, per la ri-verifica nel tempo.
|
|
1044
|
+
*
|
|
1045
|
+
* `leggiPaginaFn(url) => Promise<{url, stato, corpo}>`. Il default è **null**, e non è
|
|
1046
|
+
* pigrizia: qui dentro non si costruisce un secondo lettore del web. Quello vero esiste già
|
|
1047
|
+
* (`agent-service.leggiPaginaPerLaVista` → `leggiPaginaSicura` del kernel) e porta con sé la
|
|
1048
|
+
* validazione contro gli indirizzi interni che l'attrezzo `naviga` usa da mesi — allowlist di
|
|
1049
|
+
* schema, nessun indirizzo privato, catena di redirect limitata (OWASP «Server Side Request
|
|
1050
|
+
* Forgery Prevention Cheat Sheet», letta il 12/09/2026: «verify the value against an allowed
|
|
1051
|
+
* list of protocols (HTTP or HTTPS)», «Disable the support for the following of the
|
|
1052
|
+
* redirection … to prevent the bypass of the input validation»). Scriverne un secondo qui
|
|
1053
|
+
* vorrebbe dire due validazioni che divergono.
|
|
1054
|
+
* ⛔ Lo inietta `session-registry.mjs`, che è dove vive il cablaggio. Se resta `null` la
|
|
1055
|
+
* ri-verifica **lo dice** invece di provarci: una rotta che finge di aver guardato è peggio
|
|
1056
|
+
* di una che ammette di non poter guardare.
|
|
1057
|
+
*/
|
|
1058
|
+
leggiPaginaFn = null,
|
|
1059
|
+
/*
|
|
1060
|
+
* ⭐⭐⭐⭐ BC-44 (12/09/2026) — le due porte della ripresa automatica, iniettabili come tutto
|
|
1061
|
+
* il resto e per una ragione precisa, non per abitudine:
|
|
1062
|
+
*
|
|
1063
|
+
* `dormiFn` l'attesa fra la caduta e il secondo tentativo. ⛔ Iniettabile perché un
|
|
1064
|
+
* test che aspettasse davvero venti secondi non è un test: misurerebbe
|
|
1065
|
+
* l'orologio. Il default non trattiene il processo (`unref`): una ricerca
|
|
1066
|
+
* caduta non deve tenere in piedi un server che sta chiudendo.
|
|
1067
|
+
* `ripresaAutomatica` l'interruttore. `true` di serie; a `false` il comportamento è quello di
|
|
1068
|
+
* ieri byte per byte — la classificazione e la ripresa A MANO restano,
|
|
1069
|
+
* perché quelle non spendono niente da sole.
|
|
1070
|
+
*/
|
|
1071
|
+
dormiFn = (ms) => new Promise((risolvi) => { const t = setTimeout(risolvi, ms); t.unref?.(); }),
|
|
1072
|
+
ripresaAutomatica = true,
|
|
1073
|
+
clock = () => new Date(),
|
|
1074
|
+
}) {
|
|
1075
|
+
/*
|
|
1076
|
+
* ⭐⭐⭐ L4 — LA CACHE DEL GIUDIZIO SUL RAPPORTO, e perché `elenca()` adesso rilegge.
|
|
1077
|
+
*
|
|
1078
|
+
* ⛔ Il difetto che chiude, visto dall'owner: `elenca()` NON rileggeva i rapporti e `leggi()`
|
|
1079
|
+
* sì, quindi la ricerca `d2a453a8` compariva **«Conclusa» in lista** e
|
|
1080
|
+
* **«bloccata dal permesso» quando la si apriva**. Due viste sullo stesso fatto che si
|
|
1081
|
+
* contraddicono: la lista mentiva e il dettaglio la smentiva. L2 aveva chiamato quella
|
|
1082
|
+
* divergenza «voluta» per non pagare venti letture a ogni apertura della sezione — il costo
|
|
1083
|
+
* era vero, la conclusione no: si paga una volta e si mette in cache.
|
|
1084
|
+
*
|
|
1085
|
+
* ⛔ La chiave NON è l'id: è l'id PIÙ l'impronta del file (`mtimeMs`+`size`). Una cache a
|
|
1086
|
+
* chiave-id sola servirebbe un giudizio vecchio su un rapporto ri-depositato — cioè
|
|
1087
|
+
* riprodurrebbe in memoria la stessa bugia che sta togliendo dal disco. `null` (nessun
|
|
1088
|
+
* rapporto) è anch'esso un'impronta valida e si mette in cache come le altre: appena il file
|
|
1089
|
+
* nasce, l'impronta cambia e il giudizio si rifà.
|
|
1090
|
+
*
|
|
1091
|
+
* ⛔ E la mappa non cresce per sempre: oltre 200 voci si svuota tutta. Una LRU vera qui
|
|
1092
|
+
* sarebbe codice in più per un limite che nessun progetto reale tocca (20 ricerche per
|
|
1093
|
+
* cartella è già il tetto della pagina); quello che NON si può fare è lasciarla illimitata
|
|
1094
|
+
* dentro un processo che vive per giorni.
|
|
1095
|
+
*/
|
|
1096
|
+
const giudiziRapporto = new Map();
|
|
1097
|
+
const TETTO_CACHE_GIUDIZI = 200;
|
|
1098
|
+
/*
|
|
1099
|
+
* ⛔ L5 — QUANTE PAGINE una sola ri-verifica ha il diritto di andare a riaprire. Non è
|
|
1100
|
+
* burocrazia: `talosResearchRecheckReport` gira **in sequenza** (lo dichiara: «una dozzina di
|
|
1101
|
+
* richieste simultanee è il modo in cui una connessione domestica e un sito di notizie
|
|
1102
|
+
* decidono entrambi che sei uno scraper»), quindi un rapporto con cinquanta fonti terrebbe
|
|
1103
|
+
* aperta una richiesta HTTP per minuti. Si guardano le prime venti e la risposta dice
|
|
1104
|
+
* `troncata: true` con quante erano in tutto — mai un silenzio che somigli a «erano venti».
|
|
1105
|
+
*/
|
|
1106
|
+
const TETTO_FONTI_RIVERIFICA = 20;
|
|
1107
|
+
/**
|
|
1108
|
+
* ⭐ L4 — UN EVENTO NEL GIORNALE, e un guasto del giornale NON ferma la ricerca.
|
|
1109
|
+
*
|
|
1110
|
+
* ⛔ La scelta è deliberata e va detta: il giornale è la prova di ciò che è stato speso, non
|
|
1111
|
+
* la condizione perché si possa spendere. Se il disco è pieno, la ricerca deve continuare a
|
|
1112
|
+
* girare e a consegnare — perderemmo la ripresa, non il lavoro. L'errore si inghiotte **qui
|
|
1113
|
+
* e solo qui**, e questo è l'unico `catch` muto di questo file.
|
|
1114
|
+
*/
|
|
1115
|
+
/*
|
|
1116
|
+
* ⭐⭐⭐ L4+L6 — LA CACHE DEL FETCH DI UNA CORSA, che sopravvive al riavvio.
|
|
1117
|
+
*
|
|
1118
|
+
* Una ricerca ripresa non deve ripagare le pagine che aveva già aperto: `fetch-cache.mjs`
|
|
1119
|
+
* (L6) sa tenerle e sa serializzarsi (`snapshot()`/`restore()`), ma dichiara di non scrivere
|
|
1120
|
+
* niente su disco — la persistenza è di chi orchestra, cioè di qui. La mappa tiene
|
|
1121
|
+
* l'istanza VIVA di una corsa; `cache.json` la tiene fra una vita e l'altra.
|
|
1122
|
+
*
|
|
1123
|
+
* ⛔⛔ DICHIARATO, non lasciato credere: **oggi nessuno riempie questa cache**. Il collettore
|
|
1124
|
+
* (`collector.mjs`) non è agganciato, e le pagine le apre il kernel con `naviga`, che non
|
|
1125
|
+
* passa di qui. ⇒ il giro completo salva-e-ripristina è provato nei due versi, ma sui dati
|
|
1126
|
+
* VERI l'istantanea di oggi è vuota. È il punto di innesto pronto, non un risparmio già
|
|
1127
|
+
* misurato: dire il contrario sarebbe vendere un numero che non esiste.
|
|
1128
|
+
*/
|
|
1129
|
+
const cacheDelleRicerche = new Map();
|
|
1130
|
+
|
|
1131
|
+
/** @returns {number} quante voci sono rientrate dall'istantanea su disco (0 se non ce n'era una valida). */
|
|
1132
|
+
async function ripristinaCacheFetch({ cartella, id }) {
|
|
1133
|
+
const cache = creaCacheFetchFn();
|
|
1134
|
+
cacheDelleRicerche.set(id, cache);
|
|
1135
|
+
let rientrate = 0;
|
|
1136
|
+
try {
|
|
1137
|
+
/*
|
|
1138
|
+
* ⛔ Il VERSO CONTRARIO è già garantito dal modulo che possiede il formato: `restore()`
|
|
1139
|
+
* torna 0 su un'istantanea assente, di versione sconosciuta o malformata — «un formato
|
|
1140
|
+
* più nuovo non deve impedire a una ricerca di RIPARTIRE, al massimo la fa ripagare».
|
|
1141
|
+
* Qui NON si ricontrolla la versione: duplicarla creerebbe due numeri che divergono.
|
|
1142
|
+
*/
|
|
1143
|
+
rientrate = cache.restore(await leggiIstantaneaCacheFn({ cartella, id })) ?? 0;
|
|
1144
|
+
} catch {
|
|
1145
|
+
rientrate = 0; // un'istantanea illeggibile costa una ri-lettura delle pagine, mai la ripresa.
|
|
1146
|
+
}
|
|
1147
|
+
/*
|
|
1148
|
+
* ⭐⭐⭐⭐ L9 — LA RACCOLTA SI RIMONTA QUI, e con lei il piano e l'indice delle fonti.
|
|
1149
|
+
*
|
|
1150
|
+
* ⛔ Senza queste tre righe una ricerca RIPRESA tornerebbe esattamente allo stato di ieri:
|
|
1151
|
+
* nessun passo nel giornale, nessuna spesa contata, nessun testo ritrovabile per la
|
|
1152
|
+
* verifica. Cioè la cura funzionerebbe solo finché il server non si riavvia — e una
|
|
1153
|
+
* ricerca lunga è proprio quella che il server riavviato interrompe.
|
|
1154
|
+
* ⛔ Il piano si rilegge dal DISCO (`piano.json`), non si ricalcola: ricalcolarlo darebbe
|
|
1155
|
+
* rami con lo stesso id ma, se un giorno il pianificatore col modello sarà acceso, con
|
|
1156
|
+
* domande diverse — e i passi già a registro punterebbero a linee che non esistono più.
|
|
1157
|
+
*/
|
|
1158
|
+
const pianoSuDisco = await leggiPianoFn({ cartella, id });
|
|
1159
|
+
if (Array.isArray(pianoSuDisco) && pianoSuDisco.length > 0) pianiDelleRicerche.set(id, pianoSuDisco);
|
|
1160
|
+
const indice = await leggiIndiceFontiFn({ cartella, id });
|
|
1161
|
+
const mappa = new Map();
|
|
1162
|
+
for (const voce of Array.isArray(indice) ? indice : []) {
|
|
1163
|
+
if (typeof voce?.url === 'string') mappa.set(voce.url, voce);
|
|
1164
|
+
}
|
|
1165
|
+
indiciDelleFonti.set(id, mappa);
|
|
1166
|
+
montaRaccolta({ cartella, id, cache });
|
|
1167
|
+
return rientrate;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/** Salva l'istantanea a un punto sicuro (pausa, conclusione). Un guasto qui non ferma niente. */
|
|
1171
|
+
async function salvaIstantaneaCache({ cartella, id }) {
|
|
1172
|
+
const cache = cacheDelleRicerche.get(id);
|
|
1173
|
+
if (!cache) return;
|
|
1174
|
+
try {
|
|
1175
|
+
await scriviIstantaneaCacheFn({ cartella, id, istantanea: cache.snapshot() });
|
|
1176
|
+
} catch { /* come il giornale: la cache è un risparmio, non una condizione per lavorare. */ }
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
async function registra(cartella, id, evento) {
|
|
1180
|
+
try {
|
|
1181
|
+
await accodaEventoFn({ cartella, id, evento: { at: clock().toISOString(), ...evento }, ...(evento.kind === 'run_resumed' ? { separaRiga: true } : {}) });
|
|
1182
|
+
} catch { /* vedi sopra: un giornale che non si scrive non deve fermare una corsa già pagata. */ }
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/*
|
|
1186
|
+
* ⭐⭐⭐⭐ L9 — LA RACCOLTA DI UNA CORSA, e le due mappe che la tengono in piedi.
|
|
1187
|
+
*
|
|
1188
|
+
* `raccolteDelleRicerche` è l'oggetto che il kernel riceve come `cacheWeb`: tutto ciò che la
|
|
1189
|
+
* figlia cerca e apre passa di lì, e diventa un passo del giornale (vedi
|
|
1190
|
+
* `src/research/raccolta-viva.mjs`). Vive quanto la corsa, esattamente come
|
|
1191
|
+
* `cacheDelleRicerche`.
|
|
1192
|
+
*
|
|
1193
|
+
* `pianiDelleRicerche` tiene il piano APPROVATO in memoria, per l'attribuzione dei passi ai
|
|
1194
|
+
* rami. ⛔ Non è la fonte della verità: quella è `piano.json` su disco, ed è da lì che
|
|
1195
|
+
* `leggi()` e una ripresa lo rileggono. La copia in memoria esiste perché la raccolta deve
|
|
1196
|
+
* poterlo consultare a ogni ricerca senza una lettura di file per chiamata.
|
|
1197
|
+
*
|
|
1198
|
+
* `indiciDelleFonti` accumula l'indice url → `fonti/<sha256>.txt` e lo riscrive intero a ogni
|
|
1199
|
+
* fonte nuova. ⛔ Riscrittura atomica di tutta la mappa e non append: è una mappa, non un
|
|
1200
|
+
* registro, e due voci contraddittorie per lo stesso indirizzo sarebbero peggio di nessuna.
|
|
1201
|
+
*/
|
|
1202
|
+
const raccolteDelleRicerche = new Map();
|
|
1203
|
+
const pianiDelleRicerche = new Map();
|
|
1204
|
+
const indiciDelleFonti = new Map();
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* @param {{cartella: string, id: string, cache: object}} input
|
|
1208
|
+
* @returns {object} la raccolta viva di quella corsa
|
|
1209
|
+
*/
|
|
1210
|
+
function montaRaccolta({ cartella, id, cache }) {
|
|
1211
|
+
const raccolta = creaRaccoltaViva({
|
|
1212
|
+
cache,
|
|
1213
|
+
registra: (evento) => registra(cartella, id, evento),
|
|
1214
|
+
tieniFonte: (testo) => scriviFonteFn({ cartella, id, testo }),
|
|
1215
|
+
annotaFonte: async (voce) => {
|
|
1216
|
+
const indice = indiciDelleFonti.get(id) ?? new Map();
|
|
1217
|
+
const gia = indice.get(voce.url);
|
|
1218
|
+
// ⛔ Una pagina APERTA non si lascia sostituire dal suo estratto: una prova più debole
|
|
1219
|
+
// non deve poter cancellare una più forte (stessa regola, in memoria, in raccolta-viva).
|
|
1220
|
+
if (gia?.ottenuta === 'page' && voce.ottenuta !== 'page') return;
|
|
1221
|
+
indice.set(voce.url, voce);
|
|
1222
|
+
indiciDelleFonti.set(id, indice);
|
|
1223
|
+
await scriviIndiceFontiFn({ cartella, id, voci: [...indice.values()] });
|
|
1224
|
+
},
|
|
1225
|
+
piano: () => pianiDelleRicerche.get(id) ?? [],
|
|
1226
|
+
});
|
|
1227
|
+
raccolteDelleRicerche.set(id, raccolta);
|
|
1228
|
+
return raccolta;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* ⭐ La porta che `session-registry.mjs` passa al kernel. `null` per ogni sessione che NON è
|
|
1233
|
+
* una ricerca — e allora il kernel si comporta bit-per-bit come ieri: nessuna cache, nessun
|
|
1234
|
+
* budget, nessun passo nel giornale. È la garanzia che TALOS-BANCO non veda cambiare un byte.
|
|
1235
|
+
*/
|
|
1236
|
+
function raccoltaDellaRicerca(id) {
|
|
1237
|
+
return raccolteDelleRicerche.get(id) ?? null;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* ⭐⭐⭐⭐ L9 §6.8 (+1.5) — IL PIANO, E IL COSTO DETTO PRIMA.
|
|
1242
|
+
*
|
|
1243
|
+
* Tre passi, in quest'ordine, e ognuno ha una ragione che non è l'ordine alfabetico:
|
|
1244
|
+
* 1. `talosResearchPlanFor` dà i rami per PROFONDITÀ — 2 per «rapida», 4 per «approfondita»,
|
|
1245
|
+
* 6 per «esaustiva» — ognuno la stessa domanda vista da una faccia diversa, con la sua
|
|
1246
|
+
* stima di ricerche/pagine/token. ⛔ Deterministico: non costa un token, e un piano che
|
|
1247
|
+
* costa prima ancora di aver cercato qualcosa è il primo posto dove una corsa si allunga.
|
|
1248
|
+
* 2. `pianificaFn`, SE c'è, può riformulare le domande dei rami col modello. ⛔ Può cambiare
|
|
1249
|
+
* solo il TESTO: il numero dei rami resta quello della profondità (la persona ha scelto
|
|
1250
|
+
* quello) e le stime restano quelle calcolate — un modello che si stima da solo il costo
|
|
1251
|
+
* è un modello che dichiara quello che gli conviene. Un guasto qui NON ferma la corsa: si
|
|
1252
|
+
* tiene il piano deterministico e si va avanti.
|
|
1253
|
+
* 3. Il giornale registra `plan_proposed` e poi `plan_approved`, e il piano approvato va su
|
|
1254
|
+
* disco. ⛔⛔ L'approvazione oggi è AUTOMATICA e lo dice (`auto: true` nell'evento): il
|
|
1255
|
+
* pulsante con cui una persona toglie, aggiunge o riformula un ramo prima che parta è un
|
|
1256
|
+
* lotto di UI a parte, e non è questo. Scriverlo qui come se ci fosse sarebbe la bugia
|
|
1257
|
+
* che §6.5 esiste per togliere — due eventi distinti, invece, lasciano il posto già
|
|
1258
|
+
* pronto: quando il pulsante ci sarà, `plan_approved` arriverà da lui e `auto` sparirà.
|
|
1259
|
+
*
|
|
1260
|
+
* @returns {Promise<readonly object[]>}
|
|
1261
|
+
*/
|
|
1262
|
+
async function costruisciPiano({ question, depth }) {
|
|
1263
|
+
/*
|
|
1264
|
+
* ⛔ La profondità si normalizza QUI e contro la tabella vera: `talosResearchPlanFor` legge
|
|
1265
|
+
* `TALOS_RESEARCH_DEPTHS[depth]` e su una parola sconosciuta esploderebbe DENTRO `avvia`,
|
|
1266
|
+
* cioè farebbe fallire l'avvio di una ricerca per una stringa arrivata da una rotta. Il
|
|
1267
|
+
* ripiego è `deep`, che è anche il default del prodotto.
|
|
1268
|
+
*/
|
|
1269
|
+
const profondita = Object.hasOwn(TALOS_RESEARCH_DEPTHS, depth) ? depth : 'deep';
|
|
1270
|
+
const base = talosResearchPlanFor(question, profondita);
|
|
1271
|
+
if (typeof pianificaFn !== 'function') return base;
|
|
1272
|
+
try {
|
|
1273
|
+
const proposto = await pianificaFn({ question, depth: profondita, piano: base });
|
|
1274
|
+
if (!Array.isArray(proposto) || proposto.length === 0) return base;
|
|
1275
|
+
return base.map((ramo, i) => {
|
|
1276
|
+
const testo = typeof proposto[i] === 'string' ? proposto[i].trim()
|
|
1277
|
+
: typeof proposto[i]?.question === 'string' ? proposto[i].question.trim() : '';
|
|
1278
|
+
return testo ? { ...ramo, question: testo } : ramo;
|
|
1279
|
+
});
|
|
1280
|
+
} catch {
|
|
1281
|
+
/*
|
|
1282
|
+
* ⛔ Il piano deterministico è un ripiego COMPLETO, non degradato: è quello che il mobile
|
|
1283
|
+
* usa da sempre. Un pianificatore che cade costa una riformulazione, mai una corsa.
|
|
1284
|
+
*/
|
|
1285
|
+
return base;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Il costo ATTESO, in parole, prima di partire.
|
|
1291
|
+
*
|
|
1292
|
+
* ⛔ Lavoro sempre, denaro SOLO se un prezzo pubblicato è stato ottenuto (§6.8, +1.5). Un
|
|
1293
|
+
* «≈ 0,02 $» stampato su un prezzo indovinato è peggio di nessuna cifra: chi lo legge
|
|
1294
|
+
* decide con quello.
|
|
1295
|
+
* ⛔ E si dice che è una STIMA, con la parola. La spesa vera si conta dai passi e si mostra
|
|
1296
|
+
* accanto — «il divario stimato/speso è esso stesso una misura».
|
|
1297
|
+
*/
|
|
1298
|
+
function costoDetto(piano) {
|
|
1299
|
+
const totali = talosResearchPlanTotals(piano);
|
|
1300
|
+
let prezzo = null;
|
|
1301
|
+
try { prezzo = prezzoFn(); } catch { prezzo = null; }
|
|
1302
|
+
const costo = talosResearchPlanCost(totali, prezzo);
|
|
1303
|
+
const denaro = costo?.known
|
|
1304
|
+
? ` ≈ ${costo.amount.toFixed(4)} ${costo.currency} at the published price`
|
|
1305
|
+
: '';
|
|
1306
|
+
return {
|
|
1307
|
+
totali,
|
|
1308
|
+
costo,
|
|
1309
|
+
frase: `Planned: ${piano.length} lines of inquiry, an estimated ${totali.searches} search(es), `
|
|
1310
|
+
+ `${totali.pages} page(s) and ~${totali.tokens} tokens${denaro}. `
|
|
1311
|
+
+ 'Those are estimates made before starting; what is actually spent is counted step by step and shown next to them.',
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* ⭐⭐⭐ L9 — IL TESTO TENUTO, PER INDIRIZZO, anche dopo un riavvio.
|
|
1317
|
+
*
|
|
1318
|
+
* Prima la memoria della corsa (gratis, e più fresca); poi il disco, via
|
|
1319
|
+
* `indice-fonti.json` + `fonti/<sha256>.txt`. ⛔ L'ordine conta: una corsa viva ha in memoria
|
|
1320
|
+
* anche le fonti che l'indice non ha ancora ricevuto (una scrittura fallita, un disco pieno),
|
|
1321
|
+
* e chiedere prima al disco le perderebbe.
|
|
1322
|
+
* ⛔ Se non c'è niente da nessuna parte la mappa è VUOTA, e la verifica lo dirà con il motivo
|
|
1323
|
+
* giusto («il passaggio non è nel testo della fonte») invece di inventare un verdetto.
|
|
1324
|
+
*/
|
|
1325
|
+
async function testiTenutiPerUrl({ cartella, id }) {
|
|
1326
|
+
const mappa = new Map();
|
|
1327
|
+
const indice = await leggiIndiceFontiFn({ cartella, id });
|
|
1328
|
+
for (const voce of Array.isArray(indice) ? indice : []) {
|
|
1329
|
+
if (typeof voce?.url !== 'string' || typeof voce?.ref !== 'string') continue;
|
|
1330
|
+
const testo = await leggiFonteFn({ cartella, id, ref: voce.ref });
|
|
1331
|
+
if (typeof testo === 'string' && testo.length > 0) mappa.set(voce.url, testo);
|
|
1332
|
+
}
|
|
1333
|
+
const viva = raccolteDelleRicerche.get(id);
|
|
1334
|
+
if (viva) for (const [url, testo] of viva.testiPerUrl()) mappa.set(url, testo);
|
|
1335
|
+
return mappa;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* ⭐⭐⭐ L4 — IL GIUDIZIO SUL RAPPORTO, **UNA SOLA VOLTA E IN UN SOLO POSTO**.
|
|
1340
|
+
*
|
|
1341
|
+
* La chiamano `elenca()` e `leggi()`, e questo è il punto: prima ce n'erano due di fatto —
|
|
1342
|
+
* l'elenco che non guardava niente e il dettaglio che guardava — e le due viste si
|
|
1343
|
+
* contraddicevano a schermo. Una funzione sola non può contraddirsi.
|
|
1344
|
+
*
|
|
1345
|
+
* Ordine di lettura, e il perché di ognuno:
|
|
1346
|
+
* 1. il file depositato (`.harness-ui-research/<id>/rapporto.md`) — la via di oggi;
|
|
1347
|
+
* 2. la voce di Libreria, **solo** se il file non c'è — la via di ieri, l'unica che hanno
|
|
1348
|
+
* le ricerche già su disco. Costa una lettura in più ed è per questo che è seconda.
|
|
1349
|
+
*
|
|
1350
|
+
* ⛔ `ripiegoConsentito` si ricava dal campo `formato` della voce, non da un'euristica: una
|
|
1351
|
+
* ricerca nata oggi (`formato: 2`) DEVE portare il record; una nata prima non può, e non le
|
|
1352
|
+
* si chiede l'impossibile.
|
|
1353
|
+
* ⛔ E il file su disco **non si riscrive mai**: la correzione vive nella lettura. È la stessa
|
|
1354
|
+
* forma di «TRE RIPETIZIONI PAGATE, UNA USATA» (22/8), dove la cura stava nel lettore.
|
|
1355
|
+
*
|
|
1356
|
+
* @returns {Promise<{stato:'done'|'senza-rapporto', motivoDettaglio:string|null, contenutoRapporto:string|null, letto:object|null}>}
|
|
1357
|
+
*/
|
|
1358
|
+
/**
|
|
1359
|
+
* ⭐⭐⭐⭐ L9 §6.8 (+1.2 · +1.3) — LA VERIFICA VERA, PRIMA DEL DEPOSITO.
|
|
1360
|
+
*
|
|
1361
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
1362
|
+
* Che cosa cambia rispetto a ieri, in una riga
|
|
1363
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
1364
|
+
* L8 aveva tolto al modello la possibilità di timbrare sé stesso: `judge: null` e
|
|
1365
|
+
* `claimSupported: 'unchecked'` non erano più argomenti dell'attrezzo, li metteva il server.
|
|
1366
|
+
* Giusto, e a metà: il risultato era che **nessuno** timbrava, e il giro vero del 12/09 è
|
|
1367
|
+
* uscito con 38 affermazioni su 38 «non verificate». `unchecked` onesto è meglio di un
|
|
1368
|
+
* verdetto falso, ma non è il prodotto — il prodotto è il verdetto VERO.
|
|
1369
|
+
*
|
|
1370
|
+
* ⇒ Qui, fra il «il modello ha chiamato `research_deposit`» e il «il file è sul disco», i tre
|
|
1371
|
+
* livelli girano davvero:
|
|
1372
|
+
* L1 la fonte citata esiste fra quelle raccolte, e com'è stata ottenuta (pagina/estratto);
|
|
1373
|
+
* L2 il passaggio citato si RITROVA nel testo tenuto della pagina — `talosResearchLocate`,
|
|
1374
|
+
* nessuna frase simile, nessuna approssimazione gentile: «un verificatore che
|
|
1375
|
+
* gentilmente trova un'approssimazione è un verificatore che fabbrica attribuzioni»;
|
|
1376
|
+
* L3 un GIUDICE, che non è l'autore, dice se quel passaggio da solo sostiene
|
|
1377
|
+
* l'affermazione; e se ha detto sì o in parte, si va a CERCARE la contraria.
|
|
1378
|
+
*
|
|
1379
|
+
* ⛔⛔ IL GIUDICE NON È L'AUTORE, ed è la ragione per cui un `modelloGiudice` sta sulla
|
|
1380
|
+
* metadata fin dalla nascita della ricerca. Misura, non opinione: Panickssery, Bowman e
|
|
1381
|
+
* Feng, «LLM Evaluators Recognize and Favor Their Own Generations» (arXiv:2404.13076,
|
|
1382
|
+
* 15/04/2024, letta il 12/09/2026) — gli LLM riconoscono i propri testi, e «a linear
|
|
1383
|
+
* correlation between self-recognition capability and the strength of self-preference bias».
|
|
1384
|
+
* ⛔ Se non c'è nessun altro modello, `judge: null` e **lo dice**: `talosResearchVerify`
|
|
1385
|
+
* scrive il motivo per esteso, e il bilancio conta quelle affermazioni fra le «non
|
|
1386
|
+
* verificate» — mai fra le sostenute.
|
|
1387
|
+
*
|
|
1388
|
+
* ⛔ E il passaggio dichiarato NON è una prova finché non lo si ritrova: «even the best models
|
|
1389
|
+
* lack complete citation support 50% of the time» (Gao et al., arXiv:2305.14627, benchmark
|
|
1390
|
+
* ALCE, letta il 12/09/2026). È metà delle citazioni: esattamente il motivo per cui il testo
|
|
1391
|
+
* delle pagine si TIENE, e per cui questo controllo non è un lusso.
|
|
1392
|
+
*
|
|
1393
|
+
* ⛔ Un guasto della verifica NON butta il deposito. Se il giudice non risponde, se il disco
|
|
1394
|
+
* non dà il testo, se qualsiasi cosa cade: si torna al documento **senza verdetti**, che è
|
|
1395
|
+
* ciò che sarebbe stato depositato ieri. Perdere il rapporto pagato per far fallire il suo
|
|
1396
|
+
* controllo sarebbe il guasto introdotto dalla cura, cioè il peggiore.
|
|
1397
|
+
*
|
|
1398
|
+
* @returns {Promise<object>} lo stesso contratto di `componiRapportoRicerca`, più `bilancio`,
|
|
1399
|
+
* `giudice`, `proveDistinte` e `fedelta` quando la verifica è girata.
|
|
1400
|
+
*/
|
|
1401
|
+
async function componiRapporto(argomenti) {
|
|
1402
|
+
if (argomenti.parte !== undefined) {
|
|
1403
|
+
return depositaParteRapporto(argomenti, {
|
|
1404
|
+
leggiGiornaleFn, accodaEventoFn, leggiRapportoFn, clock,
|
|
1405
|
+
valida: unito => componiRapportoRicerca({ domanda: argomenti.domanda, ...unito }),
|
|
1406
|
+
finalizza: unito => verificaEComponiRapporto({ ...argomenti, ...unito }),
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
return verificaEComponiRapporto(argomenti);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
async function verificaEComponiRapporto({ cartella, id, domanda, testo, affermazioni, fonti }) {
|
|
1413
|
+
/*
|
|
1414
|
+
* ⛔ Il testo tenuto si cerca solo se abbiamo un id di ricerca: `research_deposit` fuori da
|
|
1415
|
+
* una ricerca non esiste (il kernel lo rifiuta a monte), ma questa funzione è anche il
|
|
1416
|
+
* `componiRapportoRicercaFn` di una sessione qualunque — e un id assente deve dare
|
|
1417
|
+
* esattamente il comportamento di ieri, non un errore.
|
|
1418
|
+
*/
|
|
1419
|
+
let testiPerUrl = null;
|
|
1420
|
+
if (typeof id === 'string' && id.length > 0 && typeof cartella === 'string' && cartella.length > 0) {
|
|
1421
|
+
try { testiPerUrl = await testiTenutiPerUrl({ cartella, id }); } catch { testiPerUrl = null; }
|
|
1422
|
+
}
|
|
1423
|
+
const composto = componiRapportoRicerca({ domanda, testo, affermazioni, fonti, testiPerUrl });
|
|
1424
|
+
if (!composto.ok) return composto;
|
|
1425
|
+
if (!testiPerUrl) return composto;
|
|
1426
|
+
|
|
1427
|
+
let record = null;
|
|
1428
|
+
try { record = await leggiRicercaFn({ cartella, id }); } catch { record = null; }
|
|
1429
|
+
const modelloGiudice = typeof record?.modelloGiudice === 'string' && record.modelloGiudice.trim()
|
|
1430
|
+
? record.modelloGiudice.trim() : null;
|
|
1431
|
+
/*
|
|
1432
|
+
* ⛔ La DOPPIA condizione, e nessuna delle due è ridondante: serve un modello giudice
|
|
1433
|
+
* scelto alla nascita (che è già «diverso dall'autore» per costruzione, via
|
|
1434
|
+
* `talosResearchPickJudge`) E una porta per parlargli. Senza la seconda il giudice
|
|
1435
|
+
* esisterebbe sulla carta e ogni chiamata cadrebbe, cioè `unchecked` con un motivo
|
|
1436
|
+
* sbagliato («il giudice non ha risposto» invece di «non ce n'era uno»).
|
|
1437
|
+
*/
|
|
1438
|
+
const giudice = modelloGiudice && typeof chiediAlModelloFn === 'function'
|
|
1439
|
+
? { id: modelloGiudice, provider: 'openrouter', model: modelloGiudice }
|
|
1440
|
+
: null;
|
|
1441
|
+
|
|
1442
|
+
const passo = 'verifica:verify';
|
|
1443
|
+
await registra(cartella, id, { kind: 'step_started', stepId: passo, branchId: 'verifica', stepKind: 'verify' });
|
|
1444
|
+
let caratteri = 0;
|
|
1445
|
+
/** @param {string} prompt */
|
|
1446
|
+
const chiedi = async (prompt) => {
|
|
1447
|
+
caratteri += prompt.length;
|
|
1448
|
+
const risposta = await chiediAlModelloFn({ modello: modelloGiudice, prompt, scopo: 'giudice-ricerca' });
|
|
1449
|
+
const detto = typeof risposta === 'string' ? risposta : String(risposta ?? '');
|
|
1450
|
+
caratteri += detto.length;
|
|
1451
|
+
return detto;
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
let verificati = null;
|
|
1455
|
+
try {
|
|
1456
|
+
verificati = await talosResearchVerify({
|
|
1457
|
+
judge: giudice,
|
|
1458
|
+
ask: (claim, passaggio) => chiedi(talosResearchJudgePrompt(claim, passaggio)),
|
|
1459
|
+
/*
|
|
1460
|
+
* ⭐ §6.8 (+1.3) — LA CONTRARIA SI CERCA APPOSTA, e solo dove ha senso cercarla:
|
|
1461
|
+
* `talosResearchVerify` la chiede soltanto dopo un «sì» o un «in parte», perché
|
|
1462
|
+
* «la contesa è disaccordo: senza un accordo prima non c'è niente con cui essere in
|
|
1463
|
+
* disaccordo». ⛔ Senza giudice non si chiede: `askOpposing` resta `undefined`, e il
|
|
1464
|
+
* modulo salta il giro invece di pagarlo per niente.
|
|
1465
|
+
*/
|
|
1466
|
+
...(giudice ? { askOpposing: (claim, passaggio) => chiedi(talosResearchOpposingPrompt(claim, passaggio)) } : {}),
|
|
1467
|
+
at: () => clock().toISOString(),
|
|
1468
|
+
}, composto.claims.map((c) => c.claim), composto.sources);
|
|
1469
|
+
} catch {
|
|
1470
|
+
verificati = null;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
if (!verificati) {
|
|
1474
|
+
await registra(cartella, id, {
|
|
1475
|
+
kind: 'step_failed', stepId: passo, error: 'la verifica non è girata: il rapporto è stato depositato senza verdetti',
|
|
1476
|
+
});
|
|
1477
|
+
return composto;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/*
|
|
1481
|
+
* ⭐ §6.8 (+1.2) — LE PROVE SI CONTANO A GRUPPI, NON A URL. «7 prove distinte su 14
|
|
1482
|
+
* indirizzi» dice una cosa che «14 fonti» non dice, e la regola sta in un posto solo
|
|
1483
|
+
* (`independence.mjs`), pubblicata e verificabile.
|
|
1484
|
+
*/
|
|
1485
|
+
const origini = composto.sources.map((s) => ({ url: s.url }));
|
|
1486
|
+
const indipendenza = talosResearchIndependentSources(origini);
|
|
1487
|
+
const fedelta = talosResearchFidelity({ claims: verificati, sources: origini });
|
|
1488
|
+
const bilancio = talosResearchVerifiedStanding(verificati);
|
|
1489
|
+
|
|
1490
|
+
const documento = talosResearchReportDocument({
|
|
1491
|
+
question: composto.intestazione,
|
|
1492
|
+
summary: String(testo).trim(),
|
|
1493
|
+
// ⛔ Il nome del giudice nel rapporto, così il lettore lo possa PESARE: un giudice su un
|
|
1494
|
+
// fornitore diverso e uno sullo stesso fornitore non valgono uguale, e il rapporto deve
|
|
1495
|
+
// dire quale dei due è stato.
|
|
1496
|
+
judge: giudice?.id ?? null,
|
|
1497
|
+
claims: verificati,
|
|
1498
|
+
sources: composto.sources,
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
await registra(cartella, id, {
|
|
1502
|
+
kind: 'step_finished', stepId: passo,
|
|
1503
|
+
// ⛔ La spesa della verifica è VERA e va contata: è la parte del costo che nessun
|
|
1504
|
+
// concorrente ha, e nasconderla farebbe sembrare gratis la cosa che ci distingue.
|
|
1505
|
+
spend: { searches: 0, pages: 0, tokens: Math.ceil(caratteri / 4) },
|
|
1506
|
+
resultRef: null,
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
return {
|
|
1510
|
+
...composto,
|
|
1511
|
+
documento,
|
|
1512
|
+
bilancio,
|
|
1513
|
+
giudice: giudice?.id ?? null,
|
|
1514
|
+
proveDistinte: indipendenza.independent,
|
|
1515
|
+
fedelta,
|
|
1516
|
+
verificate: verificati.filter((v) => v.checks.judge !== null).length,
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
async function giudicaRapporto({ cartella, record }) {
|
|
1521
|
+
const id = record.id;
|
|
1522
|
+
const chiave = `${cartella}::${id}`;
|
|
1523
|
+
const impronta = await statRapportoFn({ cartella, id });
|
|
1524
|
+
const inCache = giudiziRapporto.get(chiave);
|
|
1525
|
+
const stessaImpronta = inCache
|
|
1526
|
+
&& ((inCache.impronta === null && impronta === null)
|
|
1527
|
+
|| (inCache.impronta && impronta && inCache.impronta.mtimeMs === impronta.mtimeMs && inCache.impronta.size === impronta.size));
|
|
1528
|
+
if (stessaImpronta) return inCache.esito;
|
|
1529
|
+
|
|
1530
|
+
const ripiegoConsentito = !(Number(record.formato) >= 2);
|
|
1531
|
+
let testo = await leggiRapportoFn({ cartella, id });
|
|
1532
|
+
/*
|
|
1533
|
+
* ⛔ `daDeposito` distingue «il modello ha consegnato» da «esiste una copia in Libreria da
|
|
1534
|
+
* una vita precedente», e serve a una cosa sola ma importante: al momento della
|
|
1535
|
+
* conclusione, «non ha depositato» ha tre diagnosi diverse (permesso, giri, altro) e non
|
|
1536
|
+
* deve essere confuso con «ha depositato una cosa che non passa». Senza questo campo una
|
|
1537
|
+
* ricerca vecchia ripresa avrebbe perso la diagnosi `bloccata-dal-permesso`.
|
|
1538
|
+
*/
|
|
1539
|
+
const daDeposito = testo !== null && testo !== undefined;
|
|
1540
|
+
if (!daDeposito && record.reportLibraryId && leggiVoceLibreriaFn) {
|
|
1541
|
+
try {
|
|
1542
|
+
const voce = await leggiVoceLibreriaFn({ cartella, id: record.reportLibraryId });
|
|
1543
|
+
testo = voce?.testo ?? null;
|
|
1544
|
+
} catch {
|
|
1545
|
+
testo = null; // il rapporto è dichiarato pronto ma illeggibile ORA — onesto, mai un crash.
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
const letto = (testo === null || testo === undefined) ? null : rileggiRapportoFn(testo, { ripiegoConsentito });
|
|
1549
|
+
const esito = letto?.ok
|
|
1550
|
+
? { stato: 'done', motivoDettaglio: null, contenutoRapporto: testo, contenutoRespinto: null, letto, daDeposito }
|
|
1551
|
+
: {
|
|
1552
|
+
stato: 'senza-rapporto',
|
|
1553
|
+
daDeposito,
|
|
1554
|
+
motivoDettaglio: letto?.motivo ?? 'non c\'è nessun file di rapporto',
|
|
1555
|
+
/*
|
|
1556
|
+
* ⛔⛔ `contenutoRapporto` resta NULL quando il cancello dice di no — e il testo respinto
|
|
1557
|
+
* esce da un'altra porta, `contenutoRespinto`. Due nomi perché sono due cose: se la
|
|
1558
|
+
* scusa da 290 byte uscisse dal campo che si chiama «il rapporto», il frontend la
|
|
1559
|
+
* disegnerebbe come tale e avremmo rifatto il guasto dell'11/09 dentro la sua cura.
|
|
1560
|
+
* ⛔ Ma non si butta: un rapporto che non porta il record è comunque il prodotto di una
|
|
1561
|
+
* corsa pagata, e nasconderlo perderebbe 484.171 token di lavoro per una forma
|
|
1562
|
+
* mancante. Il cancello decide lo STATO; non decide cosa si può leggere.
|
|
1563
|
+
*/
|
|
1564
|
+
contenutoRapporto: null,
|
|
1565
|
+
contenutoRespinto: (testo === null || testo === undefined) ? null : testo,
|
|
1566
|
+
letto,
|
|
1567
|
+
};
|
|
1568
|
+
if (giudiziRapporto.size >= TETTO_CACHE_GIUDIZI) giudiziRapporto.clear();
|
|
1569
|
+
giudiziRapporto.set(chiave, { impronta, esito });
|
|
1570
|
+
return esito;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/**
|
|
1574
|
+
* ⭐⭐⭐ L2 §6.5 — IL CANCELLO DI CONSEGNA.
|
|
1575
|
+
*
|
|
1576
|
+
* Prima d'oggi questa funzione faceva due cose: prendeva l'ultimo messaggio con del testo e,
|
|
1577
|
+
* se `comeFinita === 'concluso'`, scriveva `terminata:'done'`. Cioè decideva sul PROCESSO
|
|
1578
|
+
* («la corsa è finita da sola») e raccontava il PRODOTTO («c'è un rapporto»). L'11/09 le due
|
|
1579
|
+
* cose divergevano e la sezione mostrava «Conclusa» su 290 byte di scusa.
|
|
1580
|
+
*
|
|
1581
|
+
* Adesso l'ordine è questo, e ogni ramo ha un nome suo:
|
|
1582
|
+
* pausa/annullamento → invariato (una pausa non finalizza niente)
|
|
1583
|
+
* rapporto depositato e RILEGGIBILE → 'done'
|
|
1584
|
+
* rapporto depositato ma non rileggibile/vuoto → 'senza-rapporto'
|
|
1585
|
+
* niente rapporto + un REFUSED di permesso → 'bloccata-dal-permesso'
|
|
1586
|
+
* niente rapporto + giri finiti → 'giri-esauriti'
|
|
1587
|
+
* niente rapporto, nient'altro → 'failed'
|
|
1588
|
+
*
|
|
1589
|
+
* ⛔ `'done'` non si può più ottenere con una scusa: serve un artefatto che si rilegga.
|
|
1590
|
+
* ⛔ L'ordine NON è arbitrario: il rapporto si guarda PER PRIMO, prima di `comeFinita`. Una
|
|
1591
|
+
* ricerca che deposita un rapporto valido e poi esaurisce i giri ha consegnato — e dirle
|
|
1592
|
+
* «giri esauriti» butterebbe via una consegna vera. È l'errore opposto di quello di
|
|
1593
|
+
* stasera, e va evitato con la stessa cura.
|
|
1594
|
+
*/
|
|
1595
|
+
async function onConclusioneRicerca({ cartella, id, risultato }) {
|
|
1596
|
+
const voceSessione = sessioni.get(id);
|
|
1597
|
+
const richiesta = voceSessione?._ricercaTerminataRichiesta ?? null;
|
|
1598
|
+
// ⛔ SEMPRE azzerato qui, su OGNI conclusione — mai lasciato sporco per il giro successivo (vedi la doc di testa: pausa→ripresa→conclusione naturale non deve essere scambiata per una seconda pausa).
|
|
1599
|
+
if (voceSessione) voceSessione._ricercaTerminataRichiesta = null;
|
|
1600
|
+
if (richiesta === 'paused') {
|
|
1601
|
+
/*
|
|
1602
|
+
* ⭐ L4 — «fermo» si registra, e non è la stessa riga di «chiesto di fermarsi» (quella
|
|
1603
|
+
* l'ha scritta `mettiInPausa`). `run.mjs` tiene separati `pause_requested` e `paused`
|
|
1604
|
+
* «perché in mezzo c'è del denaro»: il passo in volo viene drenato prima del punto sicuro.
|
|
1605
|
+
* Senza questa riga, un giornale rigiocato direbbe che la ricerca stava ancora fermandosi.
|
|
1606
|
+
*/
|
|
1607
|
+
await registra(cartella, id, { kind: 'run_paused' });
|
|
1608
|
+
// ⭐ L6 — il punto sicuro è ANCHE il punto in cui si salva ciò che è già stato scaricato.
|
|
1609
|
+
await salvaIstantaneaCache({ cartella, id });
|
|
1610
|
+
return; // terminata resta null: resumable, il bucket "paused" lo deriva statoVivo() dal vivo.
|
|
1611
|
+
}
|
|
1612
|
+
if (richiesta === 'cancelled') {
|
|
1613
|
+
await aggiornaRicercaFn({ cartella, id, terminata: 'cancelled', conclusaAlle: clock().toISOString() });
|
|
1614
|
+
await registra(cartella, id, { kind: 'run_cancelled' });
|
|
1615
|
+
// ⛔ Nessuna istantanea su un annullamento: `cancelled` è terminale e non si riprende mai
|
|
1616
|
+
// (`run.mjs`), quindi scrivere un file che nessuno rileggerà sarebbe solo disco sporcato.
|
|
1617
|
+
cacheDelleRicerche.delete(id);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
const record = await leggiRicercaFn({ cartella, id });
|
|
1621
|
+
const domanda = record?.domanda ?? 'research';
|
|
1622
|
+
const messaggi = risultato?.esito?.messaggiFinali;
|
|
1623
|
+
/*
|
|
1624
|
+
* ⛔ L'ultimo messaggio si conserva SEMPRE, anche quando è una scusa — ma come ALLEGATO,
|
|
1625
|
+
* in un campo che si chiama `ultimoMessaggio`. Buttarlo sarebbe perdere la diagnosi (la
|
|
1626
|
+
* scusa del 11/09 dice esattamente cosa è andato storto); chiamarlo «rapporto» era la
|
|
1627
|
+
* bugia. Troncato: è una frase da mostrare, non un documento da custodire.
|
|
1628
|
+
*/
|
|
1629
|
+
const ultimoMessaggio = (ultimoMessaggioDelModello(messaggi) ?? '').slice(0, 2_000) || null;
|
|
1630
|
+
/*
|
|
1631
|
+
* ⭐⭐⭐ BC-44 — `motivoErrore: null` sta in `comune`, cioè si AZZERA su ogni conclusione che
|
|
1632
|
+
* non sia un `failed`. Senza, una ricerca caduta per un timeout e poi ripresa fino a
|
|
1633
|
+
* `done` si porterebbe dietro per sempre il motivo di una caduta che è già stata curata —
|
|
1634
|
+
* e la sezione mostrerebbe «riprendibile» su un rapporto consegnato.
|
|
1635
|
+
*/
|
|
1636
|
+
const comune = { conclusaAlle: clock().toISOString(), ultimoMessaggio, motivoErrore: null };
|
|
1637
|
+
/*
|
|
1638
|
+
* ⭐ L6 — una conclusione è un punto sicuro come la pausa, e vale ANCHE per i guasti:
|
|
1639
|
+
* `bloccata-dal-permesso` e `giri-esauriti` sono proprio i casi che si riprendono, e chi
|
|
1640
|
+
* riprende non deve ripagare le pagine. L'istantanea si salva PRIMA di decidere lo stato,
|
|
1641
|
+
* così nessun ramo di ritorno anticipato può saltarla.
|
|
1642
|
+
*/
|
|
1643
|
+
await salvaIstantaneaCache({ cartella, id });
|
|
1644
|
+
cacheDelleRicerche.delete(id);
|
|
1645
|
+
|
|
1646
|
+
/*
|
|
1647
|
+
* ⭐⭐⭐ L4 — LO STESSO CANCELLO DI `elenca()` E `leggi()`, non una terza copia.
|
|
1648
|
+
* ⛔ La cache si invalida da sola: `giudicaRapporto` chiave sull'impronta del file, e qui il
|
|
1649
|
+
* file è appena stato depositato ⇒ impronta nuova ⇒ giudizio rifatto. Nessuna riga di
|
|
1650
|
+
* invalidazione a mano, cioè nessuna riga da ricordarsi di scrivere la prossima volta.
|
|
1651
|
+
*/
|
|
1652
|
+
const giudizio = record ? await giudicaRapporto({ cartella, record }) : null;
|
|
1653
|
+
/*
|
|
1654
|
+
* ⛔ Alla CONCLUSIONE conta il deposito, non una copia di Libreria ereditata: se il modello
|
|
1655
|
+
* non ha depositato, la domanda giusta è «perché» (permesso? giri?) e le tre diagnosi
|
|
1656
|
+
* sotto sono l'unica risposta utile. Un rapporto che passa il cancello vale comunque —
|
|
1657
|
+
* anche se arriva dalla Libreria di una vita precedente — perché consegnare è consegnare.
|
|
1658
|
+
*/
|
|
1659
|
+
const testoRapporto = (giudizio?.daDeposito || giudizio?.letto?.ok) ? (giudizio.contenutoRapporto ?? giudizio.contenutoRespinto ?? null) : null;
|
|
1660
|
+
if (testoRapporto !== null && testoRapporto !== undefined) {
|
|
1661
|
+
const letto = giudizio.letto;
|
|
1662
|
+
if (letto?.ok) {
|
|
1663
|
+
/*
|
|
1664
|
+
* ⛔ La voce di Libreria si scrive DAL RAPPORTO VERO, non dall'ultimo messaggio: è la
|
|
1665
|
+
* stessa Libreria di prima (i rapporti di ricerca SONO file di Libreria, vedi la doc
|
|
1666
|
+
* di testa di research-store.mjs), ma adesso il contenuto è l'artefatto depositato.
|
|
1667
|
+
*/
|
|
1668
|
+
let reportLibraryId;
|
|
1669
|
+
try {
|
|
1670
|
+
/*
|
|
1671
|
+
* ⭐ BC-38 (12/09/2026) — da QUALE sessione nasce questo rapporto. `id` È il sessionId
|
|
1672
|
+
* della sessione che esegue la ricerca (doc di testa di questo file, riga 21): non c'è
|
|
1673
|
+
* niente da dedurre, si passa quello che già si ha. Senza, il dettaglio della Libreria
|
|
1674
|
+
* direbbe «sessione non registrata» sul file che più di tutti ha una sessione sua.
|
|
1675
|
+
* ⛔ Solo l'id: il NOME leggibile della sessione lo risolve chi disegna, con l'elenco
|
|
1676
|
+
* vivo delle sessioni — una ricerca rinominata domani deve leggersi col nome di domani.
|
|
1677
|
+
*/
|
|
1678
|
+
reportLibraryId = await salvaVoceLibreriaFn({ cartella, sessionId: id, nome: nomeRapporto(letto.intestazione || domanda), mediaType: 'text/markdown', origine: 'generated', testo: testoRapporto });
|
|
1679
|
+
} catch {
|
|
1680
|
+
/*
|
|
1681
|
+
* ⛔ Il rapporto ESISTE su disco: non è «senza rapporto». È solo la copia di Libreria
|
|
1682
|
+
* che non si è potuta scrivere — e il posto vero del rapporto è la sua cartella, non
|
|
1683
|
+
* la Libreria. Quindi `'done'` con `reportLibraryId: null`, non un falso fallimento:
|
|
1684
|
+
* ciò che è costato denaro non si dichiara perso perché una copia non è riuscita.
|
|
1685
|
+
*/
|
|
1686
|
+
await aggiornaRicercaFn({ cartella, id, terminata: 'done', reportLibraryId: null, ...comune });
|
|
1687
|
+
await registra(cartella, id, { kind: 'run_finished' });
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
await aggiornaRicercaFn({ cartella, id, terminata: 'done', reportLibraryId, ...comune });
|
|
1691
|
+
await registra(cartella, id, { kind: 'run_finished' });
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
await aggiornaRicercaFn({ cartella, id, terminata: 'senza-rapporto', motivoDettaglio: letto?.motivo ?? null, ...comune });
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// Nessun rapporto depositato: si dice PERCHÉ, e i tre perché si curano in modo diverso.
|
|
1699
|
+
if (trovaRifiutoDiPermesso(messaggi)) {
|
|
1700
|
+
await aggiornaRicercaFn({ cartella, id, terminata: 'bloccata-dal-permesso', ...comune });
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (risultato?.esito?.comeFinita === 'giri-esauriti') {
|
|
1704
|
+
await aggiornaRicercaFn({ cartella, id, terminata: 'giri-esauriti', ...comune });
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
/*
|
|
1708
|
+
* ⭐⭐⭐⭐ BC-44 (12/09/2026) — L'ULTIMO RAMO SMETTE DI ESSERE MUTO.
|
|
1709
|
+
*
|
|
1710
|
+
* Fino a ieri qui si scriveva `failed` e basta, e da lì in poi nessuno poteva più sapere se
|
|
1711
|
+
* quella corsa fosse caduta per un guasto di rete di dieci secondi o per un guasto vero. La
|
|
1712
|
+
* riga `{"type":"RunError","message":"Upstream idle timeout exceeded","code":"internal-error"}`
|
|
1713
|
+
* era già sul disco della SESSIONE, e la ricerca non la leggeva: due file, nessun ponte.
|
|
1714
|
+
*
|
|
1715
|
+
* ⛔ Il codice si legge PRIMA del messaggio solo quando dice davvero qualcosa: `internal-error`
|
|
1716
|
+
* è il default di `agent-service.mjs` per ogni guasto del servizio, quindi lì decide la
|
|
1717
|
+
* frase del fornitore. `comeFinita` entra al suo posto quando la corsa è tornata con un
|
|
1718
|
+
* esito invece che con un'eccezione (`fermato`), perché quello È il codice di quel caso.
|
|
1719
|
+
*/
|
|
1720
|
+
const caduta = classificaErroreDiCorsa({
|
|
1721
|
+
codice: typeof risultato?.codiceErrore === 'string' && risultato.codiceErrore
|
|
1722
|
+
? risultato.codiceErrore
|
|
1723
|
+
: (risultato?.esito?.comeFinita ?? null),
|
|
1724
|
+
messaggio: typeof risultato?.erroreInterno === 'string' ? risultato.erroreInterno : null,
|
|
1725
|
+
});
|
|
1726
|
+
const motivoErrore = {
|
|
1727
|
+
classe: caduta.classe,
|
|
1728
|
+
transitorio: caduta.transitorio,
|
|
1729
|
+
codice: typeof risultato?.codiceErrore === 'string' ? risultato.codiceErrore : null,
|
|
1730
|
+
// ⛔ Troncato: è una diagnosi da rileggere, non un documento da custodire — e un fornitore può rispondere con una pagina HTML intera.
|
|
1731
|
+
messaggio: typeof risultato?.erroreInterno === 'string' ? risultato.erroreInterno.slice(0, 500) : null,
|
|
1732
|
+
};
|
|
1733
|
+
await aggiornaRicercaFn({ cartella, id, terminata: 'failed', ...comune, motivoErrore });
|
|
1734
|
+
/*
|
|
1735
|
+
* ⛔⛔ L'ORDINE NON È DI COMODO: lo stato onesto si scrive PRIMA di aspettare. Se il processo
|
|
1736
|
+
* muore durante l'attesa, sul disco resta un `failed` con la sua causa — cioè una ricerca
|
|
1737
|
+
* che una persona può riprendere a mano. Se aspettassimo prima di scrivere, un crash nel
|
|
1738
|
+
* mezzo lascerebbe una ricerca senza stato e senza motivo.
|
|
1739
|
+
*/
|
|
1740
|
+
if (caduta.transitorio) await riprendiDaSolaUnaVolta({ cartella, id, caduta });
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/*
|
|
1744
|
+
* ⭐⭐⭐⭐ BC-44 — LA RIPRESA AUTOMATICA, e i quattro cancelli che la tengono onesta.
|
|
1745
|
+
*
|
|
1746
|
+
* Perché esiste: Anthropic, «How we built our multi-agent research system» (letta 12/09/2026)
|
|
1747
|
+
* — «we can't just restart from the beginning: restarts are expensive and frustrating for
|
|
1748
|
+
* users … we built systems that can resume from where the agent was when the errors occurred»,
|
|
1749
|
+
* con «deterministic safeguards like retry logic and regular checkpoints». Il checkpoint qui è
|
|
1750
|
+
* il giornale; questa funzione è il «retry logic» che finora mancava del tutto.
|
|
1751
|
+
*
|
|
1752
|
+
* ⛔ E perché è così stretta: riprendere COSTA. Non è ritentare una chiamata — è riaprire una
|
|
1753
|
+
* corsa che può spendere venti minuti. Quindi quattro cancelli, e ognuno toglie un caso in
|
|
1754
|
+
* cui la ripresa sarebbe uno spreco o una prepotenza:
|
|
1755
|
+
* 1. **transitoria** (deciso da chi chiama): un guasto deterministico si ripeterebbe uguale;
|
|
1756
|
+
* 2. **c'è lavoro da salvare** — almeno un `step_finished` nel giornale. Una corsa caduta
|
|
1757
|
+
* al primo respiro non ha niente da riprendere: ripartire non salverebbe nulla e
|
|
1758
|
+
* spenderebbe il doppio;
|
|
1759
|
+
* 3. **UNA SOLA VOLTA** — e il tetto è per RICERCA, non per giro: si guarda tutto il
|
|
1760
|
+
* giornale, non solo dopo l'ultimo `run_started`. È il più stretto dei due letture
|
|
1761
|
+
* possibili, scelto apposta: se due riprese automatiche non bastano, la terza è una
|
|
1762
|
+
* decisione di una persona, non di un timer;
|
|
1763
|
+
* 4. **lo stato si rilegge DOPO l'attesa**: in quei secondi qualcuno può aver annullato,
|
|
1764
|
+
* eliminato o ripreso a mano quella ricerca, e ripartirci sopra sarebbe scrivere
|
|
1765
|
+
* addosso a una scelta appena presa.
|
|
1766
|
+
*
|
|
1767
|
+
* ⛔ La riga nel giornale la scrive `riprendi()` e porta `auto:true` e la `causa`: senza, un
|
|
1768
|
+
* giornale rigiocato direbbe che a riprendere è stata una persona — e il cancello (3), che
|
|
1769
|
+
* quella riga la legge, non avrebbe più nessun tetto da far rispettare.
|
|
1770
|
+
*/
|
|
1771
|
+
async function riprendiDaSolaUnaVolta({ cartella, id, caduta }) {
|
|
1772
|
+
if (!ripresaAutomatica) return false;
|
|
1773
|
+
let eventi = [];
|
|
1774
|
+
try { ({ eventi } = await leggiGiornaleFn({ cartella, id })); } catch { return false; }
|
|
1775
|
+
if (!eventi.some((e) => e?.kind === 'step_finished' || e?.kind === 'deposit_part')) return false;
|
|
1776
|
+
if (eventi.some((e) => e?.kind === 'run_resumed' && e.auto === true)) return false;
|
|
1777
|
+
try { await dormiFn(ATTESA_RIPRESA_AUTOMATICA_MS); } catch { return false; }
|
|
1778
|
+
const adesso = await leggiRicercaFn({ cartella, id });
|
|
1779
|
+
if (!adesso || adesso.terminata !== 'failed') return false;
|
|
1780
|
+
const esito = await riprendi({ id, automatica: caduta.classe });
|
|
1781
|
+
return Boolean(esito?.ok);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/*
|
|
1785
|
+
* ⛔⛔ NESSUN `run_finished` sui tre rami di guasto, ed è una scelta, non una dimenticanza.
|
|
1786
|
+
* `talosResearchApply` porta `run_finished` a `status:'done'`, cioè a uno stato TERMINALE
|
|
1787
|
+
* da cui `talosResearchNextStep` non restituisce più niente: scriverlo su una ricerca
|
|
1788
|
+
* bloccata dal permesso o rimasta senza giri la renderebbe **non riprendibile** nel giornale,
|
|
1789
|
+
* pur essendo esattamente il caso che deve potersi riprendere. La metadata dice com'è finita;
|
|
1790
|
+
* il giornale dice che il lavoro è ancora dovuto. Le due cose non si contraddicono: rispondono
|
|
1791
|
+
* a due domande diverse (`run.mjs`, `talosResearchWorkLeft`: «i due rispondono a domande
|
|
1792
|
+
* diverse e servono entrambi»).
|
|
1793
|
+
*/
|
|
1794
|
+
|
|
1795
|
+
/**
|
|
1796
|
+
* Avvia — torna `{ok, esito, id}` SUBITO, mai atteso il .then() (vedi
|
|
1797
|
+
* la doc di testa). L'id è generato QUI, PRIMA di chiamare
|
|
1798
|
+
* avviaESeguiFn, e la riga di metadata è scritta PRIMA che la sessione
|
|
1799
|
+
* parta: elimina per costruzione la race in cui una conclusione
|
|
1800
|
+
* fulminea (un mock nei test, o un modello istantaneo) potrebbe far
|
|
1801
|
+
* scattare onConclusioneRicerca prima che il file esista.
|
|
1802
|
+
*
|
|
1803
|
+
* ⛔⛔⛔ Trovato dal vivo (30/8), non da lettura: le prime versioni di
|
|
1804
|
+
* questa funzione tornavano solo `{id}` — il DISPATCH del kernel per
|
|
1805
|
+
* `research_start` (talosHarness.mjs) si aspetta lo STESSO contratto
|
|
1806
|
+
* `{ok,esito}` degli altri 5 mutanti (`String(risultato?.esito ??
|
|
1807
|
+
* (risultato?.ok ? 'started' : 'failed'))`): senza `ok`/`esito`,
|
|
1808
|
+
* `risultato?.ok` era `undefined` (falsy) e il messaggio mostrato al
|
|
1809
|
+
* modello era SEMPRE "failed" — anche quando la ricerca era
|
|
1810
|
+
* DAVVERO partita (confermato dal `research_list` immediatamente
|
|
1811
|
+
* successivo, nello stesso giro, che la mostrava "running"). Un
|
|
1812
|
+
* bug che NESSUN test a unità poteva vedere: i test del kernel
|
|
1813
|
+
* mockano `onRicercaAvvia` con la forma già corretta, i test di
|
|
1814
|
+
* `session-registry.mjs` verificano il wiring ma non il messaggio
|
|
1815
|
+
* finale — solo una sessione VERA, con un modello VERO, l'ha
|
|
1816
|
+
* mostrato.
|
|
1817
|
+
*/
|
|
1818
|
+
/*
|
|
1819
|
+
* ⭐⭐⭐⭐ L8 (12/09/2026) — LA FIGLIA EREDITA IL MODELLO DELLA MADRE.
|
|
1820
|
+
*
|
|
1821
|
+
* Il guasto, misurato sul giro vero e non dedotto: la chat `c8e9b07b` girava con
|
|
1822
|
+
* `z-ai/glm-5.3-flash`, ha chiamato `research_start`, e la figlia `3029dea2` è partita con
|
|
1823
|
+
* `z-ai/glm-4.7-flash` — il modello di serie del server (`config.mjs:25`,
|
|
1824
|
+
* `MODELLI_AMMESSI[0]`). Le due intestazioni nello store lo dicono alla lettera
|
|
1825
|
+
* (`.sessions-store/<id>.jsonl`, riga 1, campo `modello`). `avvia()` non passava nessun
|
|
1826
|
+
* modello, quindi `avviaESegui` ricadeva sul default: `modelloEffettivo = modelIdEffettivo ||
|
|
1827
|
+
* modelloRichiesta || voceEsistente?.modello || modello` (`session-registry.mjs`), e i primi
|
|
1828
|
+
* tre erano tutti assenti.
|
|
1829
|
+
*
|
|
1830
|
+
* ⛔ Tre conseguenze, tutte e tre vere insieme:
|
|
1831
|
+
* 1. la regola dell'owner «giri reali SOLO con glm-5.3-flash» era violata DAL PRODOTTO, non
|
|
1832
|
+
* da chi lo usa: nessuna schermata permetteva di scegliere il modello della ricerca;
|
|
1833
|
+
* 2. la cache non poteva prendere. OpenRouter, «Prompt Caching» (letto 12/09/2026):
|
|
1834
|
+
* «Sticky routing is tracked at the account level, **per model**, and per conversation» —
|
|
1835
|
+
* un modello diverso è un'altra chiave di cache, e infatti il giro ha misurato
|
|
1836
|
+
* **265.670 token dentro con `cached_tokens: 0`**;
|
|
1837
|
+
* 3. il rapporto lo scriveva un modello che la persona non ha scelto — cioè la ricerca
|
|
1838
|
+
* approfondita, la parte più cara del prodotto, girava sul modello più economico
|
|
1839
|
+
* proprio dove la qualità conta di più.
|
|
1840
|
+
*
|
|
1841
|
+
* ⛔ La cura è UN PASSAGGIO DI PARAMETRO, ed è per questo che nessun test la vedeva: non
|
|
1842
|
+
* c'era nessun ramo sbagliato da far scattare, c'era un argomento assente. Un test che
|
|
1843
|
+
* monta l'orchestratore e guarda cosa arriva ad `avviaESeguiFn` è l'unico che morde.
|
|
1844
|
+
*
|
|
1845
|
+
* ⛔⛔ E il modello NON si eredita quando la madre gira su un runtime LOCALE: lì
|
|
1846
|
+
* `voce.modello` è l'id di un GGUF sul disco (`modelId`), non un modello di OpenRouter, e
|
|
1847
|
+
* la figlia parte comunque `provider:'cloud'` (questa funzione non passa né `provider` né
|
|
1848
|
+
* `runtimeId`). Passarglielo trasformerebbe l'eredità in un guasto garantito alla prima
|
|
1849
|
+
* chiamata. Il filtro sta in `session-registry.mjs`, dove il provider si conosce.
|
|
1850
|
+
*/
|
|
1851
|
+
async function avvia({ cartella, question, depth, padreId = null, modello = null, reasoning = null }) {
|
|
1852
|
+
const id = randomUUIDFn();
|
|
1853
|
+
const nome = nomeDallaDomanda(question);
|
|
1854
|
+
/*
|
|
1855
|
+
* ⭐⭐⭐⭐ L9 — IL GIUDICE SI SCEGLIE ADESSO, prima ancora che la corsa parta.
|
|
1856
|
+
*
|
|
1857
|
+
* ⛔ «Chiunque tranne l'autore», e l'autore è il modello di QUESTA corsa: la scelta è di
|
|
1858
|
+
* `talosResearchPickJudge`, qui si dice solo chi era disponibile. Il perché è misurato e
|
|
1859
|
+
* vecchio: Panickssery, Bowman e Feng, «LLM Evaluators Recognize and Favor Their Own
|
|
1860
|
+
* Generations» (arXiv:2404.13076, 15/04/2024, letta il 12/09/2026) — gli LLM riconoscono
|
|
1861
|
+
* i propri testi e li premiano, con «a linear correlation between self-recognition
|
|
1862
|
+
* capability and the strength of self-preference bias».
|
|
1863
|
+
* ⛔ `null` è una risposta vera e va scritta come tale: nessun altro modello ammesso ⇒
|
|
1864
|
+
* nessun giudizio, e il rapporto lo dichiara. Mai l'autore che timbra sé stesso.
|
|
1865
|
+
*/
|
|
1866
|
+
const autore = { id: modello ?? 'autore', provider: 'openrouter', model: modello ?? '' };
|
|
1867
|
+
let candidati = [];
|
|
1868
|
+
try { candidati = modelliGiudiceFn({ autore }) ?? []; } catch { candidati = []; }
|
|
1869
|
+
const giudiceScelto = talosResearchPickJudge(autore, candidati);
|
|
1870
|
+
await creaRicercaFn({
|
|
1871
|
+
cartella, id, domanda: question, profondita: depth || 'deep', padreId, nome, modello,
|
|
1872
|
+
modelloGiudice: giudiceScelto?.model ?? giudiceScelto?.id ?? null,
|
|
1873
|
+
});
|
|
1874
|
+
/*
|
|
1875
|
+
* ⭐⭐⭐ L4 — LA PRIMA RIGA DEL GIORNALE, e l'ordine conta.
|
|
1876
|
+
*
|
|
1877
|
+
* Scritta PRIMA di `avviaESeguiFn`, per la stessa ragione per cui la metadata lo è: una
|
|
1878
|
+
* conclusione fulminea (un mock, o un modello istantaneo) non deve poter scrivere
|
|
1879
|
+
* `run_finished` su un giornale che non ha ancora il suo `run_started`. `talosResearchApply`
|
|
1880
|
+
* su un giornale che comincia senza `run_started` torna `null` a ogni evento — cioè un
|
|
1881
|
+
* giro che non si può rigiocare, che è esattamente il guasto da cui tutto questo nasce.
|
|
1882
|
+
*
|
|
1883
|
+
* ⛔ `engine: 'device'` non è una bugia sul desktop: è il valore che `run.mjs` usa per «gira
|
|
1884
|
+
* qui, in locale» contro `'cloud'` (R1b, la migrazione su server). Qui gira in locale.
|
|
1885
|
+
*/
|
|
1886
|
+
await registra(cartella, id, {
|
|
1887
|
+
kind: 'run_started', id, sessionId: id, question, depth: depth || 'deep', engine: 'device',
|
|
1888
|
+
});
|
|
1889
|
+
// ⭐ L6 — la cache di QUESTA corsa nasce qui e vive finché la corsa vive (vedi `cacheDelleRicerche`).
|
|
1890
|
+
const cache = creaCacheFetchFn();
|
|
1891
|
+
cacheDelleRicerche.set(id, cache);
|
|
1892
|
+
/*
|
|
1893
|
+
* ⭐⭐⭐⭐ L9 — IL PIANO, PRIMA CHE LA FIGLIA PARLI.
|
|
1894
|
+
*
|
|
1895
|
+
* ⛔ L'ordine è quello e non un altro: piano → `plan_proposed` → `piano.json` →
|
|
1896
|
+
* `plan_approved` → consegna → `avviaESegui`. La consegna PORTA le linee d'indagine
|
|
1897
|
+
* (vedi `promptRicerca`), quindi il piano dev'essere pronto prima che la sessione
|
|
1898
|
+
* esista; e il giornale deve avere i due eventi prima che un passo possa arrivarci,
|
|
1899
|
+
* altrimenti il replay vedrebbe un `step_started` su un giro ancora in `planning`.
|
|
1900
|
+
* ⛔ `plan_approved` porta `auto: true` — il campo non esiste in `run.mjs` e non gli serve
|
|
1901
|
+
* (l'`apply` ignora ciò che non conosce, per costruzione), ma chi rilegge il giornale
|
|
1902
|
+
* deve poter distinguere «approvato da una persona» da «approvato perché il pulsante non
|
|
1903
|
+
* c'è ancora». È un DEBITO DICHIARATO, non un silenzio.
|
|
1904
|
+
*/
|
|
1905
|
+
const piano = await costruisciPiano({ question, depth });
|
|
1906
|
+
pianiDelleRicerche.set(id, piano);
|
|
1907
|
+
montaRaccolta({ cartella, id, cache });
|
|
1908
|
+
await registra(cartella, id, { kind: 'plan_proposed', branches: piano });
|
|
1909
|
+
try { await scriviPianoFn({ cartella, id, piano }); } catch { /* come il giornale: il piano su disco è una prova, non una condizione per lavorare. */ }
|
|
1910
|
+
await registra(cartella, id, { kind: 'plan_approved', branches: piano, auto: true });
|
|
1911
|
+
const costo = costoDetto(piano);
|
|
1912
|
+
avviaESeguiFn({
|
|
1913
|
+
sessionId: id, cartella, taskId: 'ricerca',
|
|
1914
|
+
/*
|
|
1915
|
+
* ⛔⛔⛔ L1 — `ricercaId` VIAGGIA DENTRO IL TASK, e questa è la riga che rende sicuro
|
|
1916
|
+
* `research_deposit`. Il kernel costruisce il percorso del rapporto da qui
|
|
1917
|
+
* (`.harness-ui-research/<ricercaId>/rapporto.md`), non da un argomento del modello: il
|
|
1918
|
+
* modello non vede mai questo valore e non può quindi scegliere DOVE depositare.
|
|
1919
|
+
* ⛔ Dentro `task` e non in un parametro nuovo di `avviaESegui` per una ragione precisa:
|
|
1920
|
+
* `task` è persistito nell'intestazione della sessione (`session-registry.mjs`), quindi
|
|
1921
|
+
* sopravvive a un riavvio del server e a un resume. Un parametro in più si sarebbe
|
|
1922
|
+
* perso alla prima ripresa, e il deposito avrebbe smesso di funzionare proprio nel caso
|
|
1923
|
+
* in cui la ricerca è più lunga.
|
|
1924
|
+
*/
|
|
1925
|
+
/*
|
|
1926
|
+
* ⭐ L8 — `ricercaDomanda` viaggia accanto a `ricercaId`, e per la stessa ragione: il
|
|
1927
|
+
* kernel deve poter mettere la DOMANDA dentro il record del rapporto
|
|
1928
|
+
* (`record.question`, il campo che il cancello legge come `intestazione`) senza
|
|
1929
|
+
* chiederla al modello, che potrebbe riscriverla. Dentro `task` perché `task` è
|
|
1930
|
+
* persistito nell'intestazione della sessione e sopravvive a un riavvio e a un resume.
|
|
1931
|
+
*/
|
|
1932
|
+
task: { consegna: promptRicerca(question, depth, piano), ricercaId: id, ricercaDomanda: question },
|
|
1933
|
+
/*
|
|
1934
|
+
* ⭐⭐⭐ L8 — il modello e il reasoning della MADRE. `null` = «non passato», e
|
|
1935
|
+
* `avviaESegui` ricade sul default esattamente come prima: l'eredità è additiva, non
|
|
1936
|
+
* cambia il comportamento di chi non la usa (TALOS-BANCO, i test, le riprese).
|
|
1937
|
+
*/
|
|
1938
|
+
modelloRichiesta: modello ?? null,
|
|
1939
|
+
reasoningRichiesto: reasoning ?? null,
|
|
1940
|
+
/*
|
|
1941
|
+
* ⛔⛔⛔ L1 §6.3 — `'Research'`, non più `'Read only'` scritto a mano.
|
|
1942
|
+
*
|
|
1943
|
+
* La riga di prima era giusta nell'intenzione («la ricerca non deve MAI scrivere nel
|
|
1944
|
+
* progetto ospite») e sbagliata nella conseguenza, che nessuno aveva visto: una sessione
|
|
1945
|
+
* che non può scrivere NON PUÒ CONSEGNARE. L'11/09 la ricerca `d2a453a8` ha speso
|
|
1946
|
+
* 484.171 token, aperto 14 pagine, e ha salvato come rapporto la frase con cui si
|
|
1947
|
+
* scusava di non poterlo scrivere.
|
|
1948
|
+
*
|
|
1949
|
+
* `'Research'` nega tutto esattamente come `'Read only'` — tranne il deposito del
|
|
1950
|
+
* proprio rapporto, dentro la propria cartella. L'intenzione originale resta intatta;
|
|
1951
|
+
* quello che cambia è che adesso esiste una via per consegnare.
|
|
1952
|
+
*/
|
|
1953
|
+
permessiRichiesti: 'Research',
|
|
1954
|
+
/*
|
|
1955
|
+
* ⭐ §6.6 — la ricerca è FIGLIA della chat che l'ha ordinata. Prima `padreId` era `null`
|
|
1956
|
+
* e nell'albero sessione la ricerca non compariva sotto nessuno: all'owner è sembrata
|
|
1957
|
+
* «una sessione nuova», e lo era davvero anche nel registro.
|
|
1958
|
+
* ⛔ `profonditaDelega` resta 0 (il default): una ricerca non è una delega, e contarla
|
|
1959
|
+
* come tale consumerebbe il tetto di profondità dei sotto-agenti.
|
|
1960
|
+
*/
|
|
1961
|
+
padreId,
|
|
1962
|
+
onConclusioneFn: (risultato) => onConclusioneRicerca({ cartella, id, risultato }),
|
|
1963
|
+
});
|
|
1964
|
+
/*
|
|
1965
|
+
* ⭐ §6.6 — il nome, sulla voce di sessione appena creata. `avviaESegui` non ha un
|
|
1966
|
+
* parametro `nome` e non glielo aggiungo da qui: la voce esiste già al ritorno (è creata
|
|
1967
|
+
* in modo sincrono, prima del primo `await`), quindi si scrive direttamente.
|
|
1968
|
+
* ⛔ DEBITO DICHIARATO, non nascosto: questo nome vive in memoria e non passa da
|
|
1969
|
+
* `registro.rinomina()`, quindi NON sopravvive a un riavvio del server. Il nome che
|
|
1970
|
+
* sopravvive è quello sulla metadata della ricerca (`creaRicercaFn`, campo `nome`), che
|
|
1971
|
+
* è quello che la sezione legge. Chiuderlo del tutto vuole una riga `nome-sessione` nel
|
|
1972
|
+
* registro, cioè toccare `session-registry.mjs` fuori dal perimetro di questo lotto.
|
|
1973
|
+
*/
|
|
1974
|
+
const voceSessione = sessioni.get(id);
|
|
1975
|
+
if (voceSessione && !voceSessione.nome) voceSessione.nome = nome;
|
|
1976
|
+
return {
|
|
1977
|
+
ok: true,
|
|
1978
|
+
/*
|
|
1979
|
+
* ⭐⭐⭐⭐ L9 §6.8 (+1.5) — IL COSTO SI DICE PRIMA, a chi ha chiesto la ricerca.
|
|
1980
|
+
*
|
|
1981
|
+
* ⛔ Non è cortesia: una corsa multi-agente costa «about 15× more tokens than chats»
|
|
1982
|
+
* (Anthropic, «How we built our multi-agent research system», letta il 12/09/2026), e
|
|
1983
|
+
* un ordine di grandezza scoperto dopo non è una misura — è un conto. La frase dice
|
|
1984
|
+
* LAVORO (ricerche, pagine, token) e dice denaro **solo** se un prezzo pubblicato è
|
|
1985
|
+
* stato ottenuto; e dice, con la parola, che sono stime.
|
|
1986
|
+
*/
|
|
1987
|
+
esito: `Started the research «${question}» (id ${id}). It runs in the background and keeps going even if the app is closed. ${costo.frase}`,
|
|
1988
|
+
id,
|
|
1989
|
+
piano,
|
|
1990
|
+
costoAtteso: costo.totali,
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function mettiInPausa({ id }) {
|
|
1995
|
+
const voce = sessioni.get(id);
|
|
1996
|
+
if (!voce) return { ok: false, esito: 'There is no research with that id. Call research_list to see the current ones.' };
|
|
1997
|
+
if (voce.conclusa) {
|
|
1998
|
+
return { ok: false, esito: 'That research is not running: it may already be paused, cancelled or done. Call research_list to see how it stands.' };
|
|
1999
|
+
}
|
|
2000
|
+
voce._ricercaTerminataRichiesta = 'paused';
|
|
2001
|
+
voce.controller.abort();
|
|
2002
|
+
/*
|
|
2003
|
+
* ⭐ L4 — `run_pause_requested`: l'INTENZIONE, registrata adesso. Il `run_paused` lo scrive
|
|
2004
|
+
* `onConclusioneRicerca` quando il punto sicuro è raggiunto. ⛔ Non atteso (`mettiInPausa` è
|
|
2005
|
+
* sincrona per contratto col kernel): se la riga non arriva, la pausa resta comunque vera
|
|
2006
|
+
* nella metadata — il giornale è la prova, non la condizione.
|
|
2007
|
+
*/
|
|
2008
|
+
registra(voce.cartella, id, { kind: 'run_pause_requested' });
|
|
2009
|
+
return { ok: true, esito: 'That research is paused. Everything it collected is kept, and it can be resumed.' };
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
function annulla({ id }) {
|
|
2013
|
+
const voce = sessioni.get(id);
|
|
2014
|
+
if (!voce) return { ok: false, esito: 'There is no research with that id. Call research_list to see the current ones.' };
|
|
2015
|
+
if (voce.conclusa) {
|
|
2016
|
+
// ⭐ una ricerca già ferma (in pausa, o già conclusa) si annulla lo stesso: cambia solo la metadata (terminata:'cancelled'), nessun abort da fare — mai un rifiuto per un caso che mobile stesso permette (research_cancel su una "paused"/"unfinished").
|
|
2017
|
+
return aggiornaRicercaFn({ cartella: voce.cartella, id, terminata: 'cancelled' })
|
|
2018
|
+
.then(() => registra(voce.cartella, id, { kind: 'run_cancelled' }))
|
|
2019
|
+
.then(() => ({ ok: true, esito: 'That research is stopped for good. What it collected is still readable.' }));
|
|
2020
|
+
}
|
|
2021
|
+
voce._ricercaTerminataRichiesta = 'cancelled';
|
|
2022
|
+
voce.controller.abort();
|
|
2023
|
+
return { ok: true, esito: 'That research is stopped for good. What it collected is still readable.' };
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/**
|
|
2027
|
+
* ⭐⭐⭐ L4 §6.6 — LA RIPRESA VERA, DAL GIORNALE E NON DALLA MEMORIA.
|
|
2028
|
+
*
|
|
2029
|
+
* ⛔ Cosa faceva prima, e perché era poco: riprendeva la CONVERSAZIONE (`voce.messaggiFinali`)
|
|
2030
|
+
* e rifiutava quando quella mancava — cioè **dopo ogni riavvio del server**, con un messaggio
|
|
2031
|
+
* che diceva «start a new one». Rifarla da capo costa di nuovo tutto: sulla ricerca
|
|
2032
|
+
* dell'11/09 sarebbero stati 484.171 token di ingresso, 9 ricerche e 14 pagine, ripagati per
|
|
2033
|
+
* un processo morto.
|
|
2034
|
+
*
|
|
2035
|
+
* Adesso ci sono DUE vie, in quest'ordine, e la prima è la migliore quando c'è:
|
|
2036
|
+
*
|
|
2037
|
+
* A. **la conversazione è ancora in memoria** ⇒ si riprende quella, com'è sempre stato.
|
|
2038
|
+
* È superiore perché il modello ritrova il proprio contesto esatto, non un riassunto.
|
|
2039
|
+
* B. **la conversazione non c'è più (riavvio), ma il giornale sì** ⇒ si rigioca
|
|
2040
|
+
* `giornale.jsonl` con `talosResearchReplay`, si deducono i passi rimasti in volo con
|
|
2041
|
+
* `talosResearchRecover` («un evento che nessuno è vivo per aggiungere è una bugia nel
|
|
2042
|
+
* giornale»), e si riparte **dal passo dopo l'ultimo committato** con una consegna che
|
|
2043
|
+
* dice al modello dove eravamo e cosa manca.
|
|
2044
|
+
*
|
|
2045
|
+
* ⛔ Una ricerca il cui giornale è già TERMINALE non si riprende, e non è un dettaglio:
|
|
2046
|
+
* `run.mjs` rifiuta `run_resumed` da uno stato terminale «perché cancellato vuol dire
|
|
2047
|
+
* cancellato, e una ripresa che lo riaprisse spenderebbe denaro su un giro che la persona ha
|
|
2048
|
+
* chiuso». Qui la stessa regola si fa rispettare **prima** di spendere, non dentro il replay.
|
|
2049
|
+
*
|
|
2050
|
+
* ⛔⛔ E la GUARDIA fra le due vie legge il GIORNALE, non il registro vivo (cura del 12/09,
|
|
2051
|
+
* dettaglio per esteso accanto alla riga): una ricerca in pausa sopravvissuta a un riavvio
|
|
2052
|
+
* torna `conclusa: true` ⇒ `interrotta: false`, e la vecchia guardia la rifiutava con
|
|
2053
|
+
* «still running» — cioè negava la ripresa proprio a chi la pausa l'aveva chiesta.
|
|
2054
|
+
*
|
|
2055
|
+
* ⛔ Niente `cartella` fra gli argomenti: arriva da `voce.cartella`, che dopo un riavvio
|
|
2056
|
+
* `ripristina()` rimette a posto dall'intestazione della sessione. Un parametro nuovo
|
|
2057
|
+
* avrebbe voluto una riga in `session-registry.mjs` fuori dal perimetro di questo lotto.
|
|
2058
|
+
*/
|
|
2059
|
+
/*
|
|
2060
|
+
* ⭐⭐⭐⭐ BC-44 — le due righe che riaprono una corsa, e perché sono DUE.
|
|
2061
|
+
*
|
|
2062
|
+
* `terminata: null` rimette la ricerca fra le vive: `statoVivo()` legge PRIMA `terminata`, e
|
|
2063
|
+
* senza questa riga una ricerca ripresa continuerebbe a mostrarsi `failed` mentre gira davvero
|
|
2064
|
+
* — lo schermo direbbe il contrario del disco.
|
|
2065
|
+
* ⛔ `motivoErrore: null` insieme, e non dopo: il motivo di una caduta superata è una frase che
|
|
2066
|
+
* non descrive più niente. Se la corsa ricadrà, `onConclusioneRicerca` ne scriverà una nuova.
|
|
2067
|
+
* ⛔ Vale per ENTRAMBE le vie (conversazione in memoria e giornale): la via A non ci passava, e
|
|
2068
|
+
* una ripresa dalla memoria dopo un errore transitorio avrebbe lasciato `failed` per sempre.
|
|
2069
|
+
*/
|
|
2070
|
+
async function riapriLaMetadata(cartella, id) {
|
|
2071
|
+
try { await aggiornaRicercaFn({ cartella, id, terminata: null, motivoErrore: null }); }
|
|
2072
|
+
catch { /* la metadata potrebbe essere stata eliminata mentre riprendevamo: la corsa riparte comunque, ed è il giornale la prova di ciò che è stato fatto. */ }
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
async function riprendi({ id, automatica = null }) {
|
|
2076
|
+
const voce = sessioni.get(id);
|
|
2077
|
+
if (!voce) return { ok: false, esito: 'There is no research with that id. Call research_list to see the current ones.' };
|
|
2078
|
+
const cartella = voce.cartella;
|
|
2079
|
+
/* ⛔ `auto` e `causa` solo quando la ripresa è davvero automatica: una riga che dicesse `auto:false` su ogni ripresa a mano sarebbe rumore, e il cancello della ripresa automatica legge proprio `auto === true`. */
|
|
2080
|
+
const rigaDiRipresa = automatica ? { kind: 'run_resumed', auto: true, causa: automatica } : { kind: 'run_resumed' };
|
|
2081
|
+
|
|
2082
|
+
/*
|
|
2083
|
+
* ⭐⭐⭐⭐ BC-44 — IL CANCELLO NUOVO, e sta PRIMA dei due vecchi perché risponde a una domanda
|
|
2084
|
+
* che quelli non sanno nemmeno porsi: «questa corsa è finita, e come?».
|
|
2085
|
+
*
|
|
2086
|
+
* Il caso vero (ricerca `dec896c0`, 12/09): `terminata:'failed'`, la sessione in memoria c'è
|
|
2087
|
+
* ancora ed è `conclusa` ma NON `interrotta` (quel campo lo scrive solo `ripristina()`, cioè
|
|
2088
|
+
* solo dopo un riavvio) ⇒ la guardia «né in pausa né interrotta» concludeva **«That research
|
|
2089
|
+
* is still running: nothing to resume»**. Falsa due volte: non stava girando, ed era caduta
|
|
2090
|
+
* un minuto prima. Da lì il 409 della rotta.
|
|
2091
|
+
*
|
|
2092
|
+
* ⛔ Si legge il DISCO e non il registro vivo, per la stessa ragione già imparata sulla
|
|
2093
|
+
* pausa: `conclusa` dice «quel GIRO è finito», non «quella RICERCA è finita». La seconda ha
|
|
2094
|
+
* una risposta sola, e sta in `meta.json`.
|
|
2095
|
+
* ⛔ Questo cancello AGGIUNGE un permesso, non ne toglie nessuno, ed è scritto in due pezzi
|
|
2096
|
+
* apposta per garantirlo:
|
|
2097
|
+
* · `done`/`cancelled` diventano un NO esplicito. Non è una restrizione nuova: il
|
|
2098
|
+
* giornale li rifiutava già da terminale (`run_finished`/`run_cancelled`) sulla via B.
|
|
2099
|
+
* Quello che cambia è che adesso il no vale anche sulla **via A** — dove non c'era
|
|
2100
|
+
* nessun controllo e una ricerca già consegnata poteva ripartire. Buco preesistente,
|
|
2101
|
+
* chiuso qui perché è la stessa domanda.
|
|
2102
|
+
* · gli altri (`senza-rapporto`, `bloccata-dal-permesso`, `giri-esauriti`) NON si
|
|
2103
|
+
* toccano: restano esattamente com'erano, e la guardia più sotto decide per loro.
|
|
2104
|
+
* ⛔ Un `failed` SENZA causa registrata — cioè ogni ricerca caduta prima di oggi — si
|
|
2105
|
+
* comporta come ieri: `motivoErrore` assente ⇒ nessun permesso nuovo.
|
|
2106
|
+
*/
|
|
2107
|
+
const record = await leggiRicercaFn({ cartella, id });
|
|
2108
|
+
/* ⛔ La causa REGISTRATA se c'è, altrimenti quella DEDOTTA dal `RunError` che la sessione ha già in `voce.eventi`: senza la seconda, la cura non curerebbe nessuna delle ricerche già cadute — compresa quella che l'ha fatta scrivere. */
|
|
2109
|
+
const cadutaRiprendibile = record?.terminata === 'failed' && causaDellaCaduta(record, voce)?.transitorio === true;
|
|
2110
|
+
if (record?.terminata === 'done' || record?.terminata === 'cancelled') {
|
|
2111
|
+
return { ok: false, esito: `That research is ${record.terminata} and will not be resumed: start a new one if you need more.` };
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// BC-49: anche la ripresa con conversazione deve riconciliare le conferme dal disco.
|
|
2115
|
+
const { eventi, righeSaltate } = await leggiGiornaleFn({ cartella, id, rigoroso: true });
|
|
2116
|
+
let deposito;
|
|
2117
|
+
try { deposito = consegnaPartiRapporto(eventi); }
|
|
2118
|
+
catch { return { ok: false, esito: 'Le parti conservate non superano il controllo di integrità. Ripristina il giornale prima di riprendere.' }; }
|
|
2119
|
+
if (voce.messaggiFinali) {
|
|
2120
|
+
await riapriLaMetadata(cartella, id);
|
|
2121
|
+
await registra(cartella, id, rigaDiRipresa);
|
|
2122
|
+
avviaESeguiFn({
|
|
2123
|
+
sessionId: id, taskId: voce.taskId, cartella, task: voce.task, comandoProva: voce.comandoProva,
|
|
2124
|
+
messaggiIniziali: [...voce.messaggiFinali, { role: 'user', content: `${PROMPT_RIPRESA}\n${deposito}` }],
|
|
2125
|
+
forkDa: voce.forkDa, voceEsistente: voce,
|
|
2126
|
+
onConclusioneFn: (risultato) => onConclusioneRicerca({ cartella, id, risultato }),
|
|
2127
|
+
});
|
|
2128
|
+
return { ok: true, esito: 'That research is running again, from where it had stopped.' };
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/*
|
|
2132
|
+
* ⛔⛔⛔⭐⭐⭐ 12/09/2026 — IL GIORNALE SI LEGGE **PRIMA** DELLA GUARDIA, e non è un riordino
|
|
2133
|
+
* di comodo: è la cura di un difetto che rifiutava esattamente il caso per cui la pausa
|
|
2134
|
+
* esiste.
|
|
2135
|
+
*
|
|
2136
|
+
* Com'era, e cosa faceva. La guardia chiedeva `voce.interrotta`, cioè un campo del registro
|
|
2137
|
+
* VIVO, e `ripristina()` lo calcola così: `interrotta: !conclusa`. Una ricerca messa in
|
|
2138
|
+
* PAUSA conclude il suo giro (il punto sicuro emette `RunFinished`), quindi dopo un riavvio
|
|
2139
|
+
* torna `conclusa: true` ⇒ `interrotta: false` ⇒ questa riga rispondeva
|
|
2140
|
+
* **«That research is still running: nothing to resume»**. Falsa due volte: non stava
|
|
2141
|
+
* girando — era ferma perché qualcuno l'aveva fermata — e il rifiuto colpiva **l'unico caso
|
|
2142
|
+
* che la pausa serve a creare**.
|
|
2143
|
+
*
|
|
2144
|
+
* ⛔ Perché il registro vivo non può saperlo, per costruzione: `conclusa` dice «quel GIRO è
|
|
2145
|
+
* finito», non «quella RICERCA è finita». Sono due domande diverse, e la seconda ha una
|
|
2146
|
+
* risposta sola sul disco — `run_paused` nel giornale, che un riavvio non cancella. In
|
|
2147
|
+
* produzione il difetto era **mascherato**: dopo un riavvio `messaggiFinali` viene
|
|
2148
|
+
* ripristinato dal JSONL e la via A prende il comando; si vede solo quando quella manca,
|
|
2149
|
+
* cioè quando il processo è morto prima di persistere la conversazione — che è, di nuovo,
|
|
2150
|
+
* proprio il caso disperato.
|
|
2151
|
+
*
|
|
2152
|
+
* ⇒ Adesso lo stato lo dice il GIORNALE. Tre risposte, in quest'ordine, e ognuna per una
|
|
2153
|
+
* ragione sua:
|
|
2154
|
+
* 1. giornale TERMINALE (`done`/`cancelled`/`failed`) → mai: «cancellato vuol dire
|
|
2155
|
+
* cancellato» (`run.mjs`), e riaprirlo spenderebbe denaro su un giro chiuso;
|
|
2156
|
+
* 2. né in pausa secondo il giornale, né interrotta secondo il registro → sta davvero
|
|
2157
|
+
* girando, e non c'è niente da riprendere. ⛔ La seconda metà della condizione RESTA,
|
|
2158
|
+
* e deve: un processo morto a metà giro non lascia nessun evento («un evento che
|
|
2159
|
+
* nessuno è vivo per aggiungere è una bugia nel giornale»), quindi lì l'unico a
|
|
2160
|
+
* saperlo è il registro. Le due fonti non si sostituiscono, si sommano;
|
|
2161
|
+
* 3. nessun giornale → la verità di prima, e nessun `run_started` inventato adesso.
|
|
2162
|
+
*
|
|
2163
|
+
* ⛔ `pause_requested` conta come «in pausa» quanto `paused`: se il processo è morto fra la
|
|
2164
|
+
* richiesta e il punto sicuro, la persona aveva comunque premuto Pausa — e rifiutarle la
|
|
2165
|
+
* ripresa perché il giro non ha fatto in tempo a scrivere la seconda riga sarebbe punirla
|
|
2166
|
+
* per un crash.
|
|
2167
|
+
*/
|
|
2168
|
+
const giro = talosResearchReplay(eventi);
|
|
2169
|
+
const inPausa = Boolean(giro) && (giro.status === 'paused' || giro.status === 'pause_requested');
|
|
2170
|
+
if (giro && talosResearchIsTerminal(giro.status)) {
|
|
2171
|
+
return { ok: false, esito: `That research is ${giro.status} and will not be resumed: start a new one if you need more.` };
|
|
2172
|
+
}
|
|
2173
|
+
/*
|
|
2174
|
+
* ⭐⭐⭐⭐ BC-44 — la TERZA fonte, e si SOMMA alle due, non le sostituisce.
|
|
2175
|
+
* · il giornale sa che era in pausa;
|
|
2176
|
+
* · il registro vivo sa che il processo è morto a metà giro (nessun evento da scrivere);
|
|
2177
|
+
* · la METADATA sa che la corsa è finita con un guasto, e con quale — è l'unica delle tre
|
|
2178
|
+
* che poteva rispondere per la ricerca `dec896c0`, dove il giro era finito in modo
|
|
2179
|
+
* ordinato (RunError ⇒ `conclusa:true`) e nessuna delle altre due vedeva niente.
|
|
2180
|
+
*/
|
|
2181
|
+
if (!inPausa && !voce.interrotta && !cadutaRiprendibile) return { ok: false, esito: 'That research is still running: nothing to resume.' };
|
|
2182
|
+
|
|
2183
|
+
// Via B — dal giornale. Da qui in poi la conversazione non esiste più: esiste il registro.
|
|
2184
|
+
if (!giro) {
|
|
2185
|
+
/*
|
|
2186
|
+
* ⛔ Nessun giornale (una ricerca nata prima dell'11/09) o un giornale che non comincia con
|
|
2187
|
+
* `run_started`: non c'è niente da cui ripartire, e si dice la verità di prima. ⛔ Non si
|
|
2188
|
+
* inventa un `run_started` adesso per «sistemare» il file: sarebbe scrivere nel registro
|
|
2189
|
+
* un fatto che nessuno ha osservato.
|
|
2190
|
+
*/
|
|
2191
|
+
return { ok: false, esito: 'That research was interrupted by a server restart and has no journal to resume from: start a new one.' };
|
|
2192
|
+
}
|
|
2193
|
+
const recuperato = talosResearchRecover(giro, clock().toISOString());
|
|
2194
|
+
const prossimo = talosResearchNextStep(recuperato);
|
|
2195
|
+
const rimasti = talosResearchWorkLeft(recuperato);
|
|
2196
|
+
const speso = talosResearchSpent(recuperato);
|
|
2197
|
+
const fonti = await elencaFontiFn({ cartella, id });
|
|
2198
|
+
const ripristinateDallaCache = await ripristinaCacheFetch({ cartella, id });
|
|
2199
|
+
|
|
2200
|
+
await riapriLaMetadata(cartella, id);
|
|
2201
|
+
await registra(cartella, id, rigaDiRipresa);
|
|
2202
|
+
if (prossimo) {
|
|
2203
|
+
/*
|
|
2204
|
+
* ⛔ Il passo che era IN VOLO quando il processo è morto viene ri-annunciato come iniziato:
|
|
2205
|
+
* `talosResearchApply` su `step_started` di un passo già `done` non fa niente («già
|
|
2206
|
+
* pagato: ricominciarlo è l'errore che tutto questo file esiste per rendere
|
|
2207
|
+
* impossibile»), quindi questa riga non può far ripagare un passo concluso.
|
|
2208
|
+
*/
|
|
2209
|
+
await registra(cartella, id, { kind: 'step_started', stepId: prossimo.id, branchId: prossimo.branchId, stepKind: prossimo.kind });
|
|
2210
|
+
}
|
|
2211
|
+
avviaESeguiFn({
|
|
2212
|
+
sessionId: id, taskId: voce.taskId, cartella, task: voce.task, comandoProva: voce.comandoProva,
|
|
2213
|
+
messaggiIniziali: [{ role: 'user', content: consegnaDiRipresa({ giro: recuperato, prossimo, rimasti, speso, fonti, task: voce.task, deposito }) }],
|
|
2214
|
+
forkDa: voce.forkDa, voceEsistente: voce,
|
|
2215
|
+
onConclusioneFn: (risultato) => onConclusioneRicerca({ cartella, id, risultato }),
|
|
2216
|
+
});
|
|
2217
|
+
return {
|
|
2218
|
+
ok: true,
|
|
2219
|
+
esito: `That research is running again from its journal (${eventi.length} recorded events${righeSaltate ? `, ${righeSaltate} unreadable lines skipped` : ''}${ripristinateDallaCache ? `, ${ripristinateDallaCache} cached pages restored` : ''}). It restarts from the step after the last committed one.`,
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
async function rinomina({ cartella, id, title }) {
|
|
2224
|
+
const aggiornata = await aggiornaRicercaFn({ cartella, id, titolo: title });
|
|
2225
|
+
if (!aggiornata) return { ok: false, esito: 'There is no research with that id. Call research_list to see the current ones.' };
|
|
2226
|
+
// ⭐ L4 — `run_renamed` è uno degli undici eventi: un giro rigiocato deve riprendere anche il suo nome, non solo il suo stato.
|
|
2227
|
+
await registra(cartella, id, { kind: 'run_renamed', title: title ?? null });
|
|
2228
|
+
return {
|
|
2229
|
+
ok: true,
|
|
2230
|
+
esito: title === null ? 'That research shows its question again.' : `Renamed that research to «${title}».`,
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
async function elimina({ cartella, id }) {
|
|
2235
|
+
const record = await leggiRicercaFn({ cartella, id });
|
|
2236
|
+
if (!record) return { ok: true, esito: 'There was no research with that id — nothing to delete.' };
|
|
2237
|
+
if (record.reportLibraryId && eliminaVoceLibreriaFn) {
|
|
2238
|
+
try { await eliminaVoceLibreriaFn({ cartella, id: record.reportLibraryId }); } catch { /* il rapporto potrebbe già essere sparito dalla Libreria per un'altra via (library_delete diretto) — non blocca l'eliminazione della ricerca. */ }
|
|
2239
|
+
}
|
|
2240
|
+
await eliminaRicercaFn({ cartella, id });
|
|
2241
|
+
return { ok: true, esito: 'That research and its report have been deleted.' };
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/**
|
|
2245
|
+
* ⭐⭐⭐⭐ L5 §6.8 «+1.1» — «DICE ANCORA QUESTO?»
|
|
2246
|
+
*
|
|
2247
|
+
* La domanda non è «il link risponde»: un soft 404 e una pagina riscritta in silenzio
|
|
2248
|
+
* rispondono **200**. La domanda è *quello che abbiamo letto è ancora lì*, e si può porre solo
|
|
2249
|
+
* perché il record del rapporto tiene il **passaggio** citato, non solo l'URL.
|
|
2250
|
+
*
|
|
2251
|
+
* Tre cancelli prima di spendere una sola richiesta HTTP, e ognuno risponde una cosa diversa:
|
|
2252
|
+
* 1. la ricerca non esiste → `{trovata:false}` (la rotta fa 404)
|
|
2253
|
+
* 2. non c'è un record verificabile → `{ok:false, motivo}` (409, e il motivo lo dice)
|
|
2254
|
+
* 3. non c'è niente di misurabile → `{ok:false, motivo}` (409)
|
|
2255
|
+
*
|
|
2256
|
+
* ⛔ Il caso (2) comprende le ricerche VECCHIE, quelle passate col ripiego in prosa: hanno un
|
|
2257
|
+
* rapporto vero e pagato, ma non hanno i passaggi, quindi non c'è niente da ri-trovare. Dire
|
|
2258
|
+
* «ricontrollate, tutto a posto» su quelle sarebbe la bugia più facile di tutta la funzione.
|
|
2259
|
+
* ⛔ Nessun evento nel giornale. Il giornale è la prova di ciò che la CORSA ha speso
|
|
2260
|
+
* (`run.mjs` conosce undici `kind` e li elenca nel suo typedef): una ri-verifica fatta
|
|
2261
|
+
* settimane dopo non è un passo di quella corsa, e infilarcela dentro cambierebbe il
|
|
2262
|
+
* significato del file — oltre a scrivere un dodicesimo `kind` in un modulo che non è mio.
|
|
2263
|
+
* ⇒ l'esito NON è persistito, e §6.7 («l'esito dell'ultima ri-verifica, con la data») resta
|
|
2264
|
+
* aperto: va un magazzino suo, dichiarato nel rapporto di questo lotto.
|
|
2265
|
+
*
|
|
2266
|
+
* @returns {Promise<{trovata:false}|{trovata:true, ok:false, motivo:string}|{trovata:true, ok:true, riverifica:object}>}
|
|
2267
|
+
*/
|
|
2268
|
+
async function riverifica({ cartella, id }) {
|
|
2269
|
+
const record = await leggiRicercaFn({ cartella, id });
|
|
2270
|
+
if (!record) return { trovata: false };
|
|
2271
|
+
|
|
2272
|
+
const giudizio = await giudicaRapporto({ cartella, record });
|
|
2273
|
+
const recintato = giudizio.letto?.record ?? null;
|
|
2274
|
+
if (!recintato) {
|
|
2275
|
+
const coda = giudizio.letto?.ripiego
|
|
2276
|
+
? 'il suo rapporto è in forma vecchia, senza il record verificabile: non porta i passaggi citati, e senza quelli non c\'è niente da ri-trovare'
|
|
2277
|
+
: (giudizio.motivoDettaglio ?? 'non c\'è nessun file di rapporto');
|
|
2278
|
+
return { trovata: true, ok: false, motivo: `questa ricerca non si può ricontrollare: ${coda}` };
|
|
2279
|
+
}
|
|
2280
|
+
if (typeof leggiPaginaFn !== 'function') {
|
|
2281
|
+
return { trovata: true, ok: false, motivo: 'la lettura delle pagine non è disponibile su questo TALOS: senza di quella non si può andare a vedere se le fonti dicono ancora questo' };
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/*
|
|
2285
|
+
* ⛔⛔ IL TESTO TENUTO, e perché oggi la mappa esce VUOTA (vedi la testa del file). Le fonti
|
|
2286
|
+
* su disco si contano — `testiTenuti` è un numero vero e va nella risposta — ma non si
|
|
2287
|
+
* possono attribuire a un url: `fonti/<sha256>.txt` prende il nome dal proprio contenuto.
|
|
2288
|
+
* Il giorno in cui il collettore scriverà un indice url→ref, questa è l'unica riga da
|
|
2289
|
+
* cambiare, e il resto della funzione comincerà a dire `intatta`/`cambiata` da solo.
|
|
2290
|
+
*/
|
|
2291
|
+
const refs = await elencaFontiFn({ cartella, id });
|
|
2292
|
+
const testoTenutoPerUrl = new Map();
|
|
2293
|
+
|
|
2294
|
+
const passaggiCitati = recintato.claims.filter((c) => typeof c?.passage === 'string' && c.passage.trim().length > 0).length;
|
|
2295
|
+
if (passaggiCitati === 0 && testoTenutoPerUrl.size === 0) {
|
|
2296
|
+
return {
|
|
2297
|
+
trovata: true,
|
|
2298
|
+
ok: false,
|
|
2299
|
+
motivo: 'non c\'è ancora niente da ricontrollare: il rapporto non porta nessun passaggio citato e il testo delle fonti non è stato tenuto',
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
const fontiDaGuardare = recintato.sources.slice(0, TETTO_FONTI_RIVERIFICA);
|
|
2304
|
+
const deps = {
|
|
2305
|
+
/*
|
|
2306
|
+
* ⛔ `read` torna `null` quando la pagina non si è potuta LEGGERE, ed è una risposta
|
|
2307
|
+
* diversa da un lancio: `recheck.mjs` le tratta uguali («unreachable») ma il motivo che
|
|
2308
|
+
* riportiamo cambia. Uno stato ≥ 400 e un corpo vuoto sono entrambi «non l'ho vista»:
|
|
2309
|
+
* confrontare il nulla con il testo tenuto direbbe «cambiata» su una pagina che magari
|
|
2310
|
+
* è intatta e ha solo rifiutato questa richiesta.
|
|
2311
|
+
*/
|
|
2312
|
+
read: async (indirizzo) => {
|
|
2313
|
+
const pagina = await leggiPaginaFn(indirizzo);
|
|
2314
|
+
const corpo = typeof pagina?.corpo === 'string' ? pagina.corpo : '';
|
|
2315
|
+
if (!pagina || Number(pagina.stato) >= 400 || corpo.trim().length === 0) return null;
|
|
2316
|
+
return { text: corpo };
|
|
2317
|
+
},
|
|
2318
|
+
at: () => clock().toISOString(),
|
|
2319
|
+
};
|
|
2320
|
+
const esito = await talosResearchRecheckReport(deps, { ...recintato, sources: fontiDaGuardare }, testoTenutoPerUrl);
|
|
2321
|
+
|
|
2322
|
+
const fonti = esito.sources.map((f) => {
|
|
2323
|
+
const tenuto = testoTenutoPerUrl.has(f.url);
|
|
2324
|
+
const stato = f.state === 'unreachable'
|
|
2325
|
+
? 'irraggiungibile'
|
|
2326
|
+
: (tenuto ? (f.state === 'intact' ? 'intatta' : 'cambiata') : 'non-misurabile');
|
|
2327
|
+
return {
|
|
2328
|
+
url: f.url,
|
|
2329
|
+
titolo: f.title,
|
|
2330
|
+
stato,
|
|
2331
|
+
// ⛔ `null`, mai `1`, quando non c'era testo da confrontare: uno e «non misurato» non sono lo stesso numero.
|
|
2332
|
+
sopravvissuto: tenuto && f.state !== 'unreachable' ? f.survived : null,
|
|
2333
|
+
motivoLettura: f.reason ? String(f.reason).slice(0, 200) : null,
|
|
2334
|
+
passaggiRitrovati: f.passagesStanding,
|
|
2335
|
+
passaggiPersi: f.passagesLost,
|
|
2336
|
+
};
|
|
2337
|
+
});
|
|
2338
|
+
/*
|
|
2339
|
+
* ⛔ `talosResearchRecheckStanding` NON è il lettore giusto oggi, e va detto invece di
|
|
2340
|
+
* lasciarlo credere: conta gli stati del modulo, dove ogni fonte senza testo tenuto è
|
|
2341
|
+
* `intact` — cioè conterebbe come «intatte» proprio quelle che non abbiamo potuto
|
|
2342
|
+
* misurare. Il bilancio qui si fa sugli stati NORMALIZZATI, che sono quelli pubblicati.
|
|
2343
|
+
*/
|
|
2344
|
+
const conta = (valore) => fonti.filter((f) => f.stato === valore).length;
|
|
2345
|
+
return {
|
|
2346
|
+
trovata: true,
|
|
2347
|
+
ok: true,
|
|
2348
|
+
riverifica: {
|
|
2349
|
+
id,
|
|
2350
|
+
fattaAlle: esito.at,
|
|
2351
|
+
misurabile: testoTenutoPerUrl.size > 0,
|
|
2352
|
+
avvertenza: testoTenutoPerUrl.size > 0
|
|
2353
|
+
? null
|
|
2354
|
+
: 'Il testo delle pagine non era stato tenuto per questa ricerca: «intatta» o «cambiata» non si possono dire. Ciò che si misura è se i passaggi citati sono ancora nella pagina di oggi.',
|
|
2355
|
+
fonti,
|
|
2356
|
+
bilancio: {
|
|
2357
|
+
fonti: fonti.length,
|
|
2358
|
+
intatte: conta('intatta'),
|
|
2359
|
+
cambiate: conta('cambiata'),
|
|
2360
|
+
irraggiungibili: conta('irraggiungibile'),
|
|
2361
|
+
nonMisurabili: conta('non-misurabile'),
|
|
2362
|
+
passaggiCitati,
|
|
2363
|
+
passaggiRitrovati: fonti.reduce((t, f) => t + f.passaggiRitrovati, 0),
|
|
2364
|
+
passaggiPersi: fonti.reduce((t, f) => t + f.passaggiPersi, 0),
|
|
2365
|
+
},
|
|
2366
|
+
troncata: recintato.sources.length > fontiDaGuardare.length,
|
|
2367
|
+
fontiTotali: recintato.sources.length,
|
|
2368
|
+
testiTenuti: refs.length,
|
|
2369
|
+
},
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/**
|
|
2374
|
+
* ⭐⭐⭐ L2 §6.4 (contratto) — LA VOCE CHE LA SEZIONE LEGGE, campo per campo.
|
|
2375
|
+
*
|
|
2376
|
+
* Prima d'oggi erano QUATTRO campi (`id`, `titolo`, `stato`, `avviataAlle`) e la sezione non
|
|
2377
|
+
* poteva fare altro che scrivere a mano, sotto l'elenco: «La consultazione del rapporto e
|
|
2378
|
+
* delle fonti non è ancora disponibile qui» (`public/index.html:1011`). Non era pigrizia del
|
|
2379
|
+
* frontend: `reportLibraryId` non usciva da questa funzione, quindi la sezione non aveva
|
|
2380
|
+
* letteralmente il modo di sapere dove fosse il rapporto.
|
|
2381
|
+
*
|
|
2382
|
+
* id — l'id della ricerca (= il sessionId che la esegue).
|
|
2383
|
+
* domanda / question — la domanda originale. Due nomi lo STESSO valore: `question` è il
|
|
2384
|
+
* nome del contratto verso il frontend, `domanda` quello interno già
|
|
2385
|
+
* in uso. Meglio un alias esplicito che una traduzione muta a metà
|
|
2386
|
+
* strada, dove si perde.
|
|
2387
|
+
* titolo — l'etichetta scelta con `research_rename`, o la domanda. Resta:
|
|
2388
|
+
* `formattaListaRicerche` (kernel) la legge per nome.
|
|
2389
|
+
* nome — la domanda troncata a 80 caratteri, l'etichetta umana della riga.
|
|
2390
|
+
* stato — running|paused|done|cancelled|failed|senza-rapporto|
|
|
2391
|
+
* bloccata-dal-permesso|giri-esauriti.
|
|
2392
|
+
* avviataAlle — ISO.
|
|
2393
|
+
* conclusaAlle — ISO, `null` se sta ancora girando (o se è una voce nata prima che
|
|
2394
|
+
* questo campo esistesse: `null` onesto, mai una data inventata).
|
|
2395
|
+
* reportLibraryId — `null` se non c'è un rapporto in Libreria.
|
|
2396
|
+
* motivo — la frase umana, SOLO quando lo stato non è `done`.
|
|
2397
|
+
* padreId — la sessione che l'ha ordinata, `null` per le vecchie.
|
|
2398
|
+
* ultimoMessaggio — l'ultima frase del modello. ALLEGATO, mai il rapporto.
|
|
2399
|
+
*
|
|
2400
|
+
* ⛔⛔ LA COMPATIBILITÀ ALL'INDIETRO, §6.5 — una ricerca già su disco può avere
|
|
2401
|
+
* `terminata:'done'` e un `reportLibraryId` che punta a una scusa: quelle di prima di oggi
|
|
2402
|
+
* sono TUTTE così. Il loro stato si corregge al volo in `leggi()`, rileggendo il rapporto,
|
|
2403
|
+
* e il file su disco NON si riscrive mai. Ciò che è costato denaro non si sovrascrive —
|
|
2404
|
+
* lezione già pagata in questo repo (il rilancio che cancellava $2,64 di lavoro).
|
|
2405
|
+
* ⇒ `elenca()` non rilegge (sono fino a 20 righe: 20 letture di file per disegnare una
|
|
2406
|
+
* lista sarebbero un costo per ogni apertura della sezione); `leggi()` sì, sulla singola.
|
|
2407
|
+
* È una divergenza VOLUTA fra le due viste, e sta scritta qui perché si veda.
|
|
2408
|
+
*/
|
|
2409
|
+
function voceEsposta(r, stato, letto = null) {
|
|
2410
|
+
const domanda = r.domanda ?? null;
|
|
2411
|
+
/* ⭐ BC-44 — la causa registrata, o quella dedotta dagli eventi della sessione (vedi `causaDellaCaduta`). */
|
|
2412
|
+
const caduta = stato === 'failed' ? causaDellaCaduta(r, sessioni.get(r.id)) : null;
|
|
2413
|
+
return {
|
|
2414
|
+
id: r.id,
|
|
2415
|
+
domanda,
|
|
2416
|
+
question: domanda,
|
|
2417
|
+
titolo: r.titolo || domanda,
|
|
2418
|
+
nome: r.nome || nomeDallaDomanda(domanda),
|
|
2419
|
+
stato,
|
|
2420
|
+
avviataAlle: r.avviataAlle ?? null,
|
|
2421
|
+
conclusaAlle: r.conclusaAlle ?? null,
|
|
2422
|
+
reportLibraryId: r.reportLibraryId ?? null,
|
|
2423
|
+
motivo: stato === 'done' ? null : motivoDelloStato(stato, r.motivoDettaglio ?? null, caduta),
|
|
2424
|
+
padreId: r.padreId ?? null,
|
|
2425
|
+
ultimoMessaggio: r.ultimoMessaggio ?? null,
|
|
2426
|
+
/*
|
|
2427
|
+
* ⭐⭐⭐ L4 — I DUE CAMPI NUOVI, e sono ADDITIVI: nessuno dei dodici di L2 cambia nome,
|
|
2428
|
+
* tipo o significato. Il test `CONTRATTO §6.4` asserisce le chiavi con `deepEqual`
|
|
2429
|
+
* proprio perché una crescita si veda invece di scivolare dentro in silenzio.
|
|
2430
|
+
*
|
|
2431
|
+
* `bilancio` — sostenute / in parte / non sostenute / contese / non verificate.
|
|
2432
|
+
* `null` quando il record recintato non c'è: ⛔ `null` e «tutto a zero»
|
|
2433
|
+
* NON sono la stessa cosa, e mostrare zeri su una ricerca che non è mai
|
|
2434
|
+
* stata misurata sarebbe un verdetto inventato. È la riga con cui §6.7
|
|
2435
|
+
* vuole che la scheda guidi — mai col conteggio delle fonti.
|
|
2436
|
+
* `proveDistinte` — su quante fonti diverse poggia almeno un passaggio davvero ritrovato.
|
|
2437
|
+
*/
|
|
2438
|
+
bilancio: letto?.bilancio ?? null,
|
|
2439
|
+
proveDistinte: letto?.proveDistinte ?? 0,
|
|
2440
|
+
// BC-51: stesso record già letto per il bilancio, in elenco e dettaglio.
|
|
2441
|
+
// Il modello designato non prova che abbia effettivamente giudicato.
|
|
2442
|
+
giudice: letto?.record?.judge ?? null,
|
|
2443
|
+
/*
|
|
2444
|
+
* ⭐⭐⭐ L8 (12/09/2026) — CON CHE COSA È STATA FATTA. Tredicesimo campo, additivo.
|
|
2445
|
+
*
|
|
2446
|
+
* `null` per ogni ricerca nata prima di oggi — e `null` è la risposta giusta: quelle
|
|
2447
|
+
* corse un modello ce l'hanno avuto, ma nessuno l'ha registrato, e scrivere qui quello
|
|
2448
|
+
* di oggi sarebbe attribuire a ieri una scelta di adesso. Il campo esiste perché il
|
|
2449
|
+
* 12/09 una ricerca è girata su un modello diverso da quello della chat che l'aveva
|
|
2450
|
+
* ordinata e dalla sezione non si poteva vedere: due ricerche fatte con due modelli
|
|
2451
|
+
* diversi non sono confrontabili, e la riga deve dirlo.
|
|
2452
|
+
*/
|
|
2453
|
+
modello: r.modello ?? null,
|
|
2454
|
+
/*
|
|
2455
|
+
* ⭐⭐⭐⭐ L9 (12/09/2026) — SEDICESIMO CAMPO: `modelloGiudice`. Additivo come gli altri tre.
|
|
2456
|
+
*
|
|
2457
|
+
* ⛔ Perché una riga in elenco deve dirlo, e non basta il `giudice` che esce da `leggi()`:
|
|
2458
|
+
* sono due fatti DIVERSI. `giudice` (nel record del rapporto) è chi ha giudicato DAVVERO
|
|
2459
|
+
* quella corsa; questo è chi era stato SCELTO alla partenza. Coincidono quando tutto va
|
|
2460
|
+
* bene, e quando divergono è esattamente il caso che si vuole vedere — un giudice
|
|
2461
|
+
* designato che non ha mai risposto lascia `giudice: null` e affermazioni non
|
|
2462
|
+
* verificate, e senza questo campo la sezione non potrebbe distinguerlo da «non c'era
|
|
2463
|
+
* nessun altro modello».
|
|
2464
|
+
* ⛔ `null` per ogni ricerca nata prima di oggi, e `null` è la risposta giusta: quelle
|
|
2465
|
+
* corse non hanno mai avuto un giudice designato, e attribuirgliene uno adesso sarebbe
|
|
2466
|
+
* raccontare una scelta che nessuno ha fatto.
|
|
2467
|
+
*/
|
|
2468
|
+
modelloGiudice: r.modelloGiudice ?? null,
|
|
2469
|
+
/*
|
|
2470
|
+
* ⭐⭐⭐⭐ BC-44 (12/09/2026) — DICIASSETTESIMO e DICIOTTESIMO campo, additivi come tutti gli
|
|
2471
|
+
* altri: nessuno dei sedici cambia nome, tipo o significato.
|
|
2472
|
+
*
|
|
2473
|
+
* `riprendibile` — «se premo Riprendi adesso, il server accetta?». È una domanda sola, e
|
|
2474
|
+
* la risposta la dà QUI il server, non il frontend indovinandola dallo
|
|
2475
|
+
* stato: fino a ieri la sezione offriva «Riprendi» su OGNI `failed` e il
|
|
2476
|
+
* server rispondeva 409 con «non è nello stato giusto» — un pulsante che
|
|
2477
|
+
* promette ciò che nessuna rotta può mantenere.
|
|
2478
|
+
* `motivoErrore` — `{classe, transitorio}`, o `null`. ⛔ Il `messaggio` grezzo del
|
|
2479
|
+
* fornitore e il suo `codice` restano sul DISCO (`meta.json`) e NON
|
|
2480
|
+
* escono di qui: sono diagnosi, e a schermo sarebbero nomi tecnici. La
|
|
2481
|
+
* frase per una persona è già in `motivo`, composta in un posto solo.
|
|
2482
|
+
*
|
|
2483
|
+
* ⛔ I DUE `failed` NON SONO LO STESSO STATO, e la distinzione è tutta in `r.terminata`:
|
|
2484
|
+
* · `terminata` assente e stato `failed` ⇒ è `statoVivo` che l'ha DEDOTTO da una
|
|
2485
|
+
* sessione che non c'è più (riavvio del server). Quella si riprende dal giornale, e
|
|
2486
|
+
* si riprendeva già prima di BC-44 (L4 §6.6, via B);
|
|
2487
|
+
* · `terminata === 'failed'` ⇒ la corsa è finita davvero, e allora decide la causa.
|
|
2488
|
+
* Confondere i due avrebbe tolto la ripresa proprio al caso per cui il giornale esiste.
|
|
2489
|
+
* ⛔ `bloccata-dal-permesso` e `giri-esauriti` NON sono qui dentro: il giornale li tiene
|
|
2490
|
+
* riprendibili apposta (vedi la nota sui tre rami di guasto in `onConclusioneRicerca`),
|
|
2491
|
+
* ma `riprendi()` oggi non li accetta e aprirli è una riga a parte, con la sua verifica.
|
|
2492
|
+
* Dichiarato, non dimenticato.
|
|
2493
|
+
*/
|
|
2494
|
+
riprendibile: stato === 'paused'
|
|
2495
|
+
|| (stato === 'failed' && (r.terminata !== 'failed' || caduta?.transitorio === true)),
|
|
2496
|
+
motivoErrore: caduta
|
|
2497
|
+
? { classe: caduta.classe ?? 'ignoto', transitorio: caduta.transitorio === true }
|
|
2498
|
+
: null,
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
/**
|
|
2503
|
+
* ⛔⛔⛔ L4 — L'ELENCO ADESSO RILEGGE, e questa è una correzione a una scelta di L2.
|
|
2504
|
+
*
|
|
2505
|
+
* Il difetto, visto dall'owner: la ricerca `d2a453a8` compariva **«Conclusa» in lista** e
|
|
2506
|
+
* **«bloccata dal permesso» quando la si apriva**. L2 aveva chiamato quella divergenza
|
|
2507
|
+
* «voluta» — venti letture di file per disegnare una lista sembravano un costo per niente. Il
|
|
2508
|
+
* costo era vero; la conclusione no: **una lista che mente costa di più**, e il costo si paga
|
|
2509
|
+
* una volta sola grazie alla cache su `mtime`+`size` (vedi `giudiziRapporto`).
|
|
2510
|
+
*
|
|
2511
|
+
* ⛔ Rilegge SOLO le voci che si dichiarano `done`: sono le uniche che possono mentire. Una
|
|
2512
|
+
* `running`, una `failed` o una `cancelled` non hanno un rapporto da smentire, e leggerle
|
|
2513
|
+
* sarebbe costo puro.
|
|
2514
|
+
* ⛔ E il file su disco **non si riscrive**, nemmeno adesso che la bugia si vede in due posti:
|
|
2515
|
+
* la correzione vive nella lettura.
|
|
2516
|
+
*/
|
|
2517
|
+
async function elenca({ cartella, status, page_size: pageSize, offset }) {
|
|
2518
|
+
const record = await elencaRicercheFn({ cartella });
|
|
2519
|
+
const conStato = [];
|
|
2520
|
+
for (const r of record) {
|
|
2521
|
+
const stato = statoVivo(r, sessioni.get(r.id));
|
|
2522
|
+
// ⛔ Gli stessi DUE stati di `leggi()`: sono i soli che parlano del rapporto, e sono i soli
|
|
2523
|
+
// che possono mentire. Una `running`/`failed`/`cancelled` non ha un rapporto da smentire.
|
|
2524
|
+
if (stato !== 'done' && stato !== 'senza-rapporto') { conStato.push(voceEsposta(r, stato)); continue; }
|
|
2525
|
+
const giudizio = await giudicaRapporto({ cartella, record: r });
|
|
2526
|
+
conStato.push(voceEsposta({ ...r, motivoDettaglio: giudizio.motivoDettaglio ?? r.motivoDettaglio ?? null }, giudizio.stato, giudizio.letto));
|
|
2527
|
+
}
|
|
2528
|
+
const filtrate = !status || status === 'all' ? conStato : conStato.filter((r) => r.stato === status);
|
|
2529
|
+
const dimensionePagina = clampNumero(pageSize, 1, 20, 10);
|
|
2530
|
+
const salto = clampNumero(offset, 0, Number.MAX_SAFE_INTEGER, 0);
|
|
2531
|
+
const pagina = filtrate.slice(salto, salto + dimensionePagina);
|
|
2532
|
+
return { ricerche: pagina, totale: filtrate.length };
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
async function leggi({ cartella, id }) {
|
|
2536
|
+
const record = await leggiRicercaFn({ cartella, id });
|
|
2537
|
+
if (!record) return { trovata: false };
|
|
2538
|
+
let stato = statoVivo(record, sessioni.get(id));
|
|
2539
|
+
let contenutoRapporto = null;
|
|
2540
|
+
let contenutoRespinto = null;
|
|
2541
|
+
let motivoDettaglio = record.motivoDettaglio ?? null;
|
|
2542
|
+
let letto = null;
|
|
2543
|
+
/*
|
|
2544
|
+
* ⛔ DUE stati, non uno: `done` e `senza-rapporto` sono i due esiti che PARLANO del rapporto,
|
|
2545
|
+
* e vanno riletti entrambi. Guardare solo `done` (come faceva L2) lasciava fuori proprio il
|
|
2546
|
+
* caso in cui l'owner ha più bisogno di vedere: una ricerca che ha depositato qualcosa che
|
|
2547
|
+
* il cancello respinge. La rilettura può anche PROMUOVERE: se nel frattempo è stato
|
|
2548
|
+
* depositato un rapporto valido, `senza-rapporto` torna `done` — sempre nel lettore, mai
|
|
2549
|
+
* riscrivendo il file.
|
|
2550
|
+
*/
|
|
2551
|
+
if (stato === 'done' || stato === 'senza-rapporto') {
|
|
2552
|
+
/*
|
|
2553
|
+
* ⛔⛔⛔ §6.5, LA COMPATIBILITÀ ALL'INDIETRO — CALCOLATA AL VOLO, SENZA RISCRIVERE.
|
|
2554
|
+
*
|
|
2555
|
+
* Ogni ricerca fatta prima dell'11/09 ha `terminata:'done'` e un `reportLibraryId` che
|
|
2556
|
+
* può puntare a qualunque cosa: sulla `d2a453a8` punta a 290 byte di scusa. Il giudizio
|
|
2557
|
+
* lo dà `giudicaRapporto`, la STESSA funzione che usa `elenca()` — mai due lettori dello
|
|
2558
|
+
* stesso file, perché due lettori sono due verità e a schermo si contraddicono.
|
|
2559
|
+
*
|
|
2560
|
+
* ⛔ Il file su disco non si tocca. Mai riscrivere in silenzio un record già pagato: la
|
|
2561
|
+
* correzione vive nella LETTURA, come per «TRE RIPETIZIONI PAGATE, UNA USATA» (22/8),
|
|
2562
|
+
* dove la cura stava nel lettore e non nelle righe.
|
|
2563
|
+
*/
|
|
2564
|
+
const giudizio = await giudicaRapporto({ cartella, record });
|
|
2565
|
+
stato = giudizio.stato;
|
|
2566
|
+
motivoDettaglio = giudizio.motivoDettaglio ?? motivoDettaglio;
|
|
2567
|
+
contenutoRapporto = giudizio.contenutoRapporto;
|
|
2568
|
+
contenutoRespinto = giudizio.contenutoRespinto ?? null;
|
|
2569
|
+
letto = giudizio.letto;
|
|
2570
|
+
}
|
|
2571
|
+
/*
|
|
2572
|
+
* ⭐⭐⭐ L4 — IL GIORNALE ESPOSTO: `piano`, `passi`, `spesa`, `giornale`.
|
|
2573
|
+
*
|
|
2574
|
+
* ⛔ Tutti e quattro ADDITIVI e tutti e quattro DERIVATI: `piano` e `passi` escono dal
|
|
2575
|
+
* replay degli eventi veri, non da un campo salvato che potrebbe divergere. Un giro senza
|
|
2576
|
+
* giornale (una ricerca vecchia) dà `piano: []`, `passi: []` e `giornale: null` — e
|
|
2577
|
+
* `null` sul giornale è la differenza fra «non ne ha uno» e «ne ha uno vuoto».
|
|
2578
|
+
* ⛔ `righeSaltate` esce allo scoperto: se una riga del registro è illeggibile, la sezione
|
|
2579
|
+
* deve poterlo dire. «Si è caricato» e «si è caricato per intero» non sono la stessa frase.
|
|
2580
|
+
*/
|
|
2581
|
+
const { eventi, righeSaltate } = await leggiGiornaleFn({ cartella, id });
|
|
2582
|
+
const giro = talosResearchReplay(eventi);
|
|
2583
|
+
const pianoSuDisco = await leggiPianoFn({ cartella, id });
|
|
2584
|
+
return {
|
|
2585
|
+
trovata: true,
|
|
2586
|
+
...voceEsposta({ ...record, motivoDettaglio }, stato, letto),
|
|
2587
|
+
contenutoRapporto,
|
|
2588
|
+
/*
|
|
2589
|
+
* ⛔ Il testo che il cancello ha RESPINTO, in un campo che dice di esserlo. La sezione può
|
|
2590
|
+
* mostrarlo come «ciò che la ricerca ha depositato, e che non passa il controllo»: si
|
|
2591
|
+
* vede tutto il lavoro pagato, e nessuno lo scambia per un rapporto.
|
|
2592
|
+
*/
|
|
2593
|
+
contenutoRespinto,
|
|
2594
|
+
/*
|
|
2595
|
+
* ⭐⭐⭐⭐ L5 §6.7 — LE AFFERMAZIONI E LE FONTI, STRUTTURATE.
|
|
2596
|
+
*
|
|
2597
|
+
* Fino a ieri di qui usciva `contenutoRapporto`: il markdown intero, col record recintato
|
|
2598
|
+
* dentro un blocco ```talos-research-report. La sezione avrebbe dovuto **ri-parsare** quel
|
|
2599
|
+
* blocco nel browser per disegnare le due viste che §6.7 chiede (Affermazioni e Fonti) —
|
|
2600
|
+
* cioè scrivere un secondo lettore del record, in un altro linguaggio, che diverge dal
|
|
2601
|
+
* primo alla prima modifica del formato. Il lettore è UNO, sta in `report.mjs`, e gira
|
|
2602
|
+
* qui: alla sezione arriva il risultato.
|
|
2603
|
+
*
|
|
2604
|
+
* ⛔ `null` — MAI `[]` — quando il record non c'è (ricerca vecchia, o rapporto respinto):
|
|
2605
|
+
* una lista vuota si disegna come «nessuna affermazione», che è un fatto; `null` è «non
|
|
2606
|
+
* lo sappiamo», che è la verità. Sono le stesse due parole che `bilancio` distingue.
|
|
2607
|
+
* ⛔ `verdettoUmano` esce da `talosResearchSupportLabel`, cioè dalla STESSA funzione che
|
|
2608
|
+
* scrive la prosa del rapporto: due frasari per lo stesso verdetto sono due verdetti.
|
|
2609
|
+
* ⛔ `contrarie` (CONTESA-01) è `null` quando `opposing` è assente, e `[]` quando è stato
|
|
2610
|
+
* guardato e non si è trovato niente: «non guardato» e «guardato, nessuna» si leggono
|
|
2611
|
+
* uguali solo se non importa sbagliare.
|
|
2612
|
+
* ⛔ Costo per il MODELLO: zero. `formattaLetturaRicerca` (kernel) legge solo `trovata`,
|
|
2613
|
+
* `contenutoRapporto` e `stato` — questi campi non entrano mai in un prompt.
|
|
2614
|
+
*/
|
|
2615
|
+
affermazioni: letto?.record
|
|
2616
|
+
? letto.record.claims.map((c, i) => ({
|
|
2617
|
+
numero: i + 1,
|
|
2618
|
+
testo: c?.text ?? '',
|
|
2619
|
+
fonte: c?.sourceIndex ?? null,
|
|
2620
|
+
passaggio: typeof c?.passage === 'string' ? c.passage : '',
|
|
2621
|
+
ritrovato: c?.checks?.quotePresent === true,
|
|
2622
|
+
tratto: c?.checks?.quoteSpan ?? null,
|
|
2623
|
+
verdetto: c?.checks?.claimSupported ?? 'unchecked',
|
|
2624
|
+
verdettoUmano: talosResearchSupportLabel(c?.checks ?? {}),
|
|
2625
|
+
motivoVerdetto: c?.checks?.supportReason ?? null,
|
|
2626
|
+
giudice: c?.checks?.judge ?? null,
|
|
2627
|
+
giudicataAlle: c?.checks?.judgedAt ?? null,
|
|
2628
|
+
contrarie: Array.isArray(c?.checks?.opposing) ? c.checks.opposing : null,
|
|
2629
|
+
}))
|
|
2630
|
+
: null,
|
|
2631
|
+
fonti: letto?.record
|
|
2632
|
+
? letto.record.sources.map((f, i) => ({
|
|
2633
|
+
numero: i + 1,
|
|
2634
|
+
url: f?.url ?? '',
|
|
2635
|
+
titolo: f?.title ?? '',
|
|
2636
|
+
pubblicataAlle: f?.publishedAt ?? null,
|
|
2637
|
+
// 'page' = la pagina è stata aperta e letta; 'snippet' = se n'è visto solo l'estratto della ricerca.
|
|
2638
|
+
ottenuta: f?.obtained ?? null,
|
|
2639
|
+
}))
|
|
2640
|
+
: null,
|
|
2641
|
+
sintesi: letto?.record?.summary ?? null,
|
|
2642
|
+
piano: Array.isArray(pianoSuDisco) ? pianoSuDisco : (giro?.plan ?? []),
|
|
2643
|
+
passi: giro?.steps ?? [],
|
|
2644
|
+
spesa: giro ? talosResearchSpent(giro) : null,
|
|
2645
|
+
/*
|
|
2646
|
+
* ⭐⭐⭐⭐ L9 §6.8 (+1.5) — LO STIMATO, ACCANTO ALLO SPESO.
|
|
2647
|
+
*
|
|
2648
|
+
* ⛔ `null` — mai zeri — quando non c'è un piano: una ricerca vecchia non ha mai avuto
|
|
2649
|
+
* una stima, e «0 ricerche attese» direbbe che era gratis. Il divario fra questi due
|
|
2650
|
+
* numeri È una misura («il divario stimato/speso è esso stesso una misura», §6.8), e
|
|
2651
|
+
* una misura fatta contro uno zero inventato non misura niente.
|
|
2652
|
+
* ⛔ Si ricalcola dal piano invece di salvarlo: il piano su disco è il fatto, la somma è
|
|
2653
|
+
* aritmetica su di lui — un totale salvato a parte è un secondo numero che può
|
|
2654
|
+
* divergere dal primo.
|
|
2655
|
+
*/
|
|
2656
|
+
costoAtteso: Array.isArray(pianoSuDisco) && pianoSuDisco.length > 0
|
|
2657
|
+
? talosResearchPlanTotals(pianoSuDisco)
|
|
2658
|
+
: (giro?.plan?.length ? talosResearchPlanTotals(giro.plan) : null),
|
|
2659
|
+
giornale: giro ? { eventi: eventi.length, righeSaltate, stato: giro.status } : null,
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
/*
|
|
2664
|
+
* ⭐⭐⭐⭐ L9 — DUE METODI NUOVI, e nessuno dei nove di prima cambia.
|
|
2665
|
+
*
|
|
2666
|
+
* `componiRapporto` — quello che il kernel chiama da `research_deposit`: compone il
|
|
2667
|
+
* record E lo verifica, prima che il file esista. Sostituisce
|
|
2668
|
+
* l'uso diretto della funzione pura `componiRapportoRicerca`, che
|
|
2669
|
+
* resta esportata e invariata per chi non ha una ricerca intorno
|
|
2670
|
+
* (il banco, i test del kernel).
|
|
2671
|
+
* `raccoltaDellaRicerca` — la porta `cacheWeb` di quella corsa, o `null`. `session-registry`
|
|
2672
|
+
* la chiede per la sessione che sta per partire e la passa al
|
|
2673
|
+
* kernel; per ogni altra sessione è `null`, cioè il kernel di ieri.
|
|
2674
|
+
*/
|
|
2675
|
+
return Object.freeze({
|
|
2676
|
+
avvia, mettiInPausa, annulla, riprendi, rinomina, elimina, elenca, leggi, riverifica,
|
|
2677
|
+
componiRapporto, raccoltaDellaRicerca,
|
|
2678
|
+
});
|
|
2679
|
+
}
|