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,445 @@
|
|
|
1
|
+
import { ContextEngineError, ContextEventV1, ContextJobV1, ContextVersionV1, TokenMeasurementV1, parseContextRecord, parseContextSettings } from './contracts.mjs';
|
|
2
|
+
import { computeContextBudget, planCompaction, selectClosedPrefix } from './compaction-planner.mjs';
|
|
3
|
+
import { buildSummaryRequest, validateSummary, composeActiveContext } from './summary.mjs';
|
|
4
|
+
import { chunkContextRecords, rankContextSources, selectContextEvidence } from './retrieval.mjs';
|
|
5
|
+
|
|
6
|
+
const terminal = new Set(['committed', 'cancelled', 'failed']);
|
|
7
|
+
const fail = (code, message) => { throw new ContextEngineError(message, code); };
|
|
8
|
+
const hash = async value => Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(value)))), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
9
|
+
const identity = profile => ({ provider: profile.provider, model: profile.model });
|
|
10
|
+
const sameModel = (a, b) => a?.provider === b?.provider && a?.model === b?.model;
|
|
11
|
+
const plainText = content => typeof content === 'string' ? content : Array.isArray(content) ? content.filter(p => ['text', 'input_text', 'output_text'].includes(p?.type) && typeof p.text === 'string').map(p => p.text).join('\n') : '';
|
|
12
|
+
const activeStates = new Set(['queued', 'preparing', 'summarizing', 'validating', 'ready']);
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* 25/09/2026 — ticket della CLI «riassunto rifiutato richiesto a ogni passo»
|
|
16
|
+
* (`docs/talos-cli/2026-09-25-ticket-desktop-ce-summary-retries.md`; la CLI l'ha riprodotto: 5 riassunti pagati in UN giro,
|
|
17
|
+
* nessuno pubblicato, niente a schermo). Causa, in `prepareForRequest`: un lavoro fallito è terminale e la chiave
|
|
18
|
+
* `auto-${revision}` cambia a ogni passo del giro ⇒ ogni richiesta ne apriva uno nuovo.
|
|
19
|
+
* Decisione owner 25/09 «pausa che cresce + segnale»: dopo un fallimento la compattazione AUTOMATICA aspetta 60 s, poi
|
|
20
|
+
* 300, poi 900 — la scala di Hermes (`agent/context_compressor.py:768-772`, clone `65ad529` del 23/09/2026: «Timeouts
|
|
21
|
+
* escalate 60s -> 300s -> 900s … a flat 30s cooldown let every async-completion turn re-issue the same capped request»),
|
|
22
|
+
* con un contatore PER CLASSE d'errore (:776-783) che solo un riassunto riuscito azzera (:2473-2480). La «Compatta» della
|
|
23
|
+
* persona non aspetta (Hermes :2887, «Manual /compress passes force=True»). Lo stato si RICAVA dai lavori già su disco
|
|
24
|
+
* (ultimo `committed` = nessuna pausa): sopravvive a un riavvio senza una riga nuova nello schema.
|
|
25
|
+
*/
|
|
26
|
+
export const RAFFREDDAMENTO_DOPO_RIFIUTO_MS = Object.freeze([60_000, 300_000, 900_000]);
|
|
27
|
+
// Conta l'ultimo ESITO, non l'ultima creazione: un lavoro in pausa (`paused` non blocca un lavoro nuovo) può fallire DOPO
|
|
28
|
+
// una «Compatta» riuscita creata più tardi. `updatedAt` di un lavoro terminale è l'istante dell'esito (per `committed`
|
|
29
|
+
// è `version.createdAt`, `sqlite-worker.mjs::commitContextVersion`).
|
|
30
|
+
const piuRecente = (a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt) || b.baseRevision - a.baseRevision || Date.parse(b.createdAt) - Date.parse(a.createdAt);
|
|
31
|
+
/*
|
|
32
|
+
* Contano per la pausa SOLO i fallimenti del RIASSUNTO (e del fornitore che lo scrive) — la decisione owner era «pausa dopo un
|
|
33
|
+
* riassunto rifiutato», e Hermes raffredda sui fallimenti del riassunto: vuoto, troncato, sovraccarico, tempo scaduto
|
|
34
|
+
* (`agent/context_compressor.py:740-783`). ⛔ Il 25/09 contavano TUTTI i lavori falliti: la CLI ha misurato un riassunto VALIDO
|
|
35
|
+
* perso per `CTX_STALE_REVISION` (lo stato è cambiato durante la compattazione), poi 60 s di pausa e il giro morto per contesto
|
|
36
|
+
* pieno — 2-4 volte su 6 (`r5a-coord-ce-compaction`). Una corsa interna del motore (revisione o modello cambiati a metà, un
|
|
37
|
+
* annullamento, niente da compattare) non costa un riassunto sbagliato, e ritentare subito è ciò che deve succedere.
|
|
38
|
+
* `CTX_COMPACTION_FAILED` è l'errore NON del motore (rete, 5xx, tempo scaduto del fornitore): conta, come in Hermes.
|
|
39
|
+
*/
|
|
40
|
+
const FALLIMENTI_DEL_RIASSUNTO = new Set(['CTX_INVALID_SUMMARY', 'CTX_EMPTY_SUMMARY', 'CTX_TRUNCATED_SUMMARY', 'CTX_INVALID_SOURCE',
|
|
41
|
+
'CTX_NO_REDUCTION', 'CTX_SEGMENT_TOO_LARGE', 'CTX_CONTEXT_TOO_SMALL', 'CTX_COMPACTION_FAILED']);
|
|
42
|
+
export function raffreddamentoCompattazione(jobs, adesso) {
|
|
43
|
+
let ultimo = null; let tentativi = 0;
|
|
44
|
+
for (const job of [...jobs].sort(piuRecente)) {
|
|
45
|
+
if (job.state === 'committed') break;
|
|
46
|
+
// annullati, in pausa e le corse interne del motore non pagano un rifiuto: non aprono e non chiudono la pausa
|
|
47
|
+
if (job.state !== 'failed' || !FALLIMENTI_DEL_RIASSUNTO.has(job.error?.code)) continue;
|
|
48
|
+
ultimo ??= job;
|
|
49
|
+
if (job.error?.code === ultimo.error?.code) tentativi++;
|
|
50
|
+
}
|
|
51
|
+
if (!ultimo) return null;
|
|
52
|
+
const attesaMs = RAFFREDDAMENTO_DOPO_RIFIUTO_MS[Math.min(tentativi, RAFFREDDAMENTO_DOPO_RIFIUTO_MS.length) - 1];
|
|
53
|
+
const fino = Date.parse(ultimo.updatedAt) + attesaMs;
|
|
54
|
+
if (!(Date.parse(adesso) < fino)) return null;
|
|
55
|
+
return { jobId: ultimo.id, code: ultimo.error?.code ?? 'CTX_COMPACTION_FAILED', attempts: tentativi, waitSeconds: attesaMs / 1000, failedAt: ultimo.updatedAt, retryAfter: new Date(fino).toISOString() };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* 24/09/2026 — F4: ARCHIVIO ≠ PROIEZIONE. La proiezione è la storia che l'adapter desktop ha CORRETTO (esiti
|
|
60
|
+
* degli attrezzi riscritti prima della richiesta, `talosHarness.desktop-hotfix.mjs::correggiMessaggiDesktop`):
|
|
61
|
+
* il modello e il riassuntore vedono QUELLA; l'archivio, `sourceHash` e le citazioni della versione restano
|
|
62
|
+
* sugli originali grezzi. Prima di oggi il motore compilava solo dai record (`prepareForRequest`), e il
|
|
63
|
+
* servizio desktop, per far vedere il testo corretto, archiviava la proiezione: alla richiesta dopo l'archivio
|
|
64
|
+
* non combaciava più (`CTX_HISTORY_DIVERGED`, T1/T2 della ricognizione del 24/09). Allineamento 1:1 per
|
|
65
|
+
* sequenza — stessa lunghezza, stessi ruoli — altrimenti si RIFIUTA, non si indovina. È la forma di Hermes:
|
|
66
|
+
* le righe originali restano in tabella e ciò che va al modello è un insieme distinto
|
|
67
|
+
* (`hermes_state_messages.py:735-741`, clone `65ad529` del 23/09/2026: «soft-archive the active rows
|
|
68
|
+
* (active=0, compacted=1: summarized away, still searchable) and insert compacted_messages as fresh active rows»).
|
|
69
|
+
*/
|
|
70
|
+
function checkProjection(records, projection) {
|
|
71
|
+
if (projection === undefined) return;
|
|
72
|
+
if (!Array.isArray(projection) || projection.length !== records.length || projection.some((message, index) => !message || typeof message !== 'object' || message.role !== records[index].message.role)) fail('CTX_PROJECTION_MISALIGNED', 'La proiezione della richiesta non è allineata all’archivio (lunghezza o ruoli diversi).');
|
|
73
|
+
}
|
|
74
|
+
const overlay = (records, projection) => projection === undefined ? records : records.map(record => {
|
|
75
|
+
const message = projection[record.sequence - 1];
|
|
76
|
+
if (message === undefined) return record; // record arrivato dopo la richiesta che ha aperto il job: resta grezzo
|
|
77
|
+
if (message?.role !== record.message.role) fail('CTX_PROJECTION_MISALIGNED', 'La proiezione della richiesta non è allineata all’archivio (lunghezza o ruoli diversi).');
|
|
78
|
+
return { ...record, message };
|
|
79
|
+
});
|
|
80
|
+
/*
|
|
81
|
+
* Le citazioni della sintesi sono state verificate sulla proiezione (è il testo che il riassuntore ha letto);
|
|
82
|
+
* la versione però vive nell'archivio e il verificatore del commit (`node/context-export.mjs:44-62`) le
|
|
83
|
+
* pretende ALLA LETTERA negli originali grezzi. Quelle che stanno solo nel testo corretto passano in
|
|
84
|
+
* `unverifiedSources` (mai spacciate per verificate); se non ne resta nessuna, la versione NON si pubblica.
|
|
85
|
+
*/
|
|
86
|
+
function ricitaSugliOriginali(summary, records) {
|
|
87
|
+
const byId = new Map(records.map(record => [record.id, record]));
|
|
88
|
+
const verified = []; const dropped = [...(summary.unverifiedSources ?? [])];
|
|
89
|
+
for (const source of summary.sources) {
|
|
90
|
+
const record = byId.get(source.recordId);
|
|
91
|
+
const text = record ? plainText(record.message.content) : '';
|
|
92
|
+
const start = record ? text.indexOf(source.quote) : -1;
|
|
93
|
+
if (start < 0) { dropped.push({ recordId: source.recordId, quote: source.quote, reason: record ? 'not-found' : 'unknown-record' }); continue; }
|
|
94
|
+
verified.push({ recordId: source.recordId, quote: source.quote, start, end: start + source.quote.length });
|
|
95
|
+
}
|
|
96
|
+
if (records.length && !verified.length) fail('CTX_INVALID_SOURCE', 'Le citazioni della sintesi stanno solo nel testo corretto della richiesta, non negli originali archiviati. La versione precedente rimane valida.');
|
|
97
|
+
return { ...summary, sources: verified, ...(dropped.length ? { unverifiedSources: dropped } : {}) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The injected store owns durability; this controller owns candidate validity.
|
|
101
|
+
* Inference never receives a context which failed its final measurement. */
|
|
102
|
+
export function createContextEngine({ store, model, tokenCounter, retrieval, embedding, toolCatalog, assets, usagePolicy, clock = () => new Date().toISOString(), idFactory = () => crypto.randomUUID() }) {
|
|
103
|
+
if (!store || !model?.resolveModel || !model?.summarize || !tokenCounter?.countPreparedContext) fail('CTX_PORT_MISSING', 'Archivio, modello e contatore sono necessari.');
|
|
104
|
+
const running = new Map();
|
|
105
|
+
const key = (sessionId, jobId) => JSON.stringify([sessionId, jobId]);
|
|
106
|
+
const state = async sessionId => {
|
|
107
|
+
const snapshot = await store.readContextSnapshot({ sessionId });
|
|
108
|
+
if (!snapshot) fail('CTX_SESSION_NOT_FOUND', 'Conversazione non presente nell’archivio del contesto.');
|
|
109
|
+
return snapshot;
|
|
110
|
+
};
|
|
111
|
+
const originals = async (sessionId, throughSequence) => {
|
|
112
|
+
const result = []; let afterSequence = 0;
|
|
113
|
+
for (;;) {
|
|
114
|
+
const page = await store.readOriginals({ sessionId, afterSequence, throughSequence, limit: 1000 });
|
|
115
|
+
result.push(...page);
|
|
116
|
+
if (page.length < 1000) return result;
|
|
117
|
+
afterSequence = page.at(-1).sequence;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const measure = async (messages, tools, profile, signal) => {
|
|
121
|
+
signal?.throwIfAborted();
|
|
122
|
+
const measurement = TokenMeasurementV1.parse(await tokenCounter.countPreparedContext({ messages, tools, model: profile, signal }));
|
|
123
|
+
signal?.throwIfAborted();
|
|
124
|
+
if (!sameModel(measurement, profile) || measurement.windowTokens !== profile.windowTokens || measurement.responseReserve !== profile.responseReserve) fail('CTX_MEASUREMENT_MISMATCH', 'Il conteggio non corrisponde al modello e alla riserva della richiesta.');
|
|
125
|
+
return measurement;
|
|
126
|
+
};
|
|
127
|
+
const budgetFor = (measurement, settings) => computeContextBudget({ ...measurement, settings });
|
|
128
|
+
function compiled(snapshot, records, { summary = snapshot.activeVersion?.summary, coveredThrough = snapshot.activeVersion?.coveredThrough ?? 0, evidence = [], targetModel } = {}) {
|
|
129
|
+
const systemMessages = records.filter(r => ['system', 'developer'].includes(r.message.role)).map(r => r.message);
|
|
130
|
+
const tailMessages = records.filter(r => r.sequence > coveredThrough && !['system', 'developer'].includes(r.message.role)).map(r => r.message);
|
|
131
|
+
const messages = !summary && !snapshot.facts.some(f => f.status !== 'removed') && !evidence.length
|
|
132
|
+
? structuredClone([...systemMessages, ...tailMessages])
|
|
133
|
+
: composeActiveContext({ systemMessages, summary: summary ?? null, facts: snapshot.facts, tailMessages, evidence });
|
|
134
|
+
if (!model.prepareContext || !targetModel) return messages;
|
|
135
|
+
const prepared = model.prepareContext({ messages, model: targetModel, reset: Boolean(summary) });
|
|
136
|
+
if (!Array.isArray(prepared?.messages)) fail('CTX_PROVIDER_CONTEXT_INVALID', 'Il modello non ha preparato un contesto valido.');
|
|
137
|
+
return prepared.messages;
|
|
138
|
+
}
|
|
139
|
+
async function save(job, patch) {
|
|
140
|
+
const next = ContextJobV1.parse({ ...job, ...patch, updatedAt: clock() });
|
|
141
|
+
return store.saveJobProgress({ sessionId: job.sessionId, job: next });
|
|
142
|
+
}
|
|
143
|
+
async function assertCurrent(job, signal) {
|
|
144
|
+
signal?.throwIfAborted();
|
|
145
|
+
const snapshot = await state(job.sessionId);
|
|
146
|
+
if (snapshot.stateRevision !== job.baseStateRevision) fail('CTX_STALE_REVISION', 'Il contesto è cambiato durante la preparazione.');
|
|
147
|
+
const current = await store.readContextJob({ sessionId: job.sessionId, jobId: job.id });
|
|
148
|
+
if (current?.state === 'cancelled') fail('CTX_JOB_CANCELLED', 'Compattazione annullata.');
|
|
149
|
+
return snapshot;
|
|
150
|
+
}
|
|
151
|
+
async function account(job, operationId, usage) {
|
|
152
|
+
if (usage === undefined) return;
|
|
153
|
+
await store.recordUsage({ sessionId: job.sessionId, jobId: job.id, operationId, usage });
|
|
154
|
+
// The common session service deduplicates this same operation identity.
|
|
155
|
+
await usagePolicy?.record?.({ sessionId: job.sessionId, jobId: job.id, operationId, usage });
|
|
156
|
+
}
|
|
157
|
+
/* 25/09 — l'AVVISO della pausa dopo un rifiuto: uno per lavoro fallito (id deterministico, lo store ignora il doppione),
|
|
158
|
+
così un giro di K passi lo mostra una volta sola. Contenuto fisso per lavoro: la consegna al desktop rifiuta un id già
|
|
159
|
+
visto con un contenuto diverso (`session-registry.mjs::pubblicaEventoContesto`). */
|
|
160
|
+
async function avvisaPausa(sessionId, jobs) {
|
|
161
|
+
const pausa = raffreddamentoCompattazione(jobs ?? (await state(sessionId)).jobs, clock());
|
|
162
|
+
if (!pausa) return null;
|
|
163
|
+
const { jobId, code, attempts, waitSeconds, failedAt, retryAfter } = pausa;
|
|
164
|
+
await store.recordContextNotice({ sessionId, event: ContextEventV1.parse({ schema: 'talos.context.event.v1', id: `cooling-${jobId}`, sessionId, jobId, kind: 'context.compaction.cooling', state: 'failed', createdAt: failedAt, payload: { code, attempts, waitSeconds, retryAfter } }) });
|
|
165
|
+
return pausa;
|
|
166
|
+
}
|
|
167
|
+
async function execute(initial, { sessionModel, tools = [], signal, projection }) {
|
|
168
|
+
let job = initial;
|
|
169
|
+
try {
|
|
170
|
+
const snapshot = await assertCurrent(job, signal);
|
|
171
|
+
const profile = await model.resolveModel({ sessionModel, settings: snapshot.settings });
|
|
172
|
+
if (!sameModel(profile, job.model)) fail('CTX_MODEL_MISMATCH', 'Il modello di sintesi è cambiato.');
|
|
173
|
+
// 24/09 — il riassuntore legge la proiezione (testo corretto), quando la richiesta ne ha portata una.
|
|
174
|
+
const records = overlay(await originals(job.sessionId, job.coveredThrough), projection);
|
|
175
|
+
job = await save(job, { state: 'preparing' });
|
|
176
|
+
// Plan only the immutable covered prefix; the final request reattaches the latest suffix.
|
|
177
|
+
const planning = planCompaction(records, { ...profile, settings: snapshot.settings, retainRecentTurns: 0 });
|
|
178
|
+
const segments = planning.segments;
|
|
179
|
+
if (!segments.length) fail('CTX_NOTHING_TO_COMPACT', 'Non ci sono scambi completi da compattare.');
|
|
180
|
+
job = await save(job, { state: 'summarizing', progress: { completed: job.completedSegments.length, total: Math.max(segments.length, job.completedSegments.length), phase: 'summarizing' } });
|
|
181
|
+
/*
|
|
182
|
+
* 09/09 — `build(compact)` al posto della richiesta già costruita: una sintesi TRONCATA (`length`) si
|
|
183
|
+
* ritenta UNA volta con l'istruzione compatta, mai di più. Trovato dal giro vero: senza limite
|
|
184
|
+
* dichiarato il modello scriveva 2.048 token e la compattazione — e il giro — morivano lì.
|
|
185
|
+
*/
|
|
186
|
+
const invoke = async (build, sourceRecords, label) => {
|
|
187
|
+
const once = async (compact) => {
|
|
188
|
+
const request = build(compact);
|
|
189
|
+
await assertCurrent(job, signal);
|
|
190
|
+
const measured = await measure(request.messages, [], profile, signal);
|
|
191
|
+
if (!budgetFor(measured, snapshot.settings).fits) fail('CTX_SEGMENT_TOO_LARGE', 'Il segmento supera lo spazio del modello di sintesi.');
|
|
192
|
+
const operationId = `${job.id}:${idFactory()}`;
|
|
193
|
+
if (usagePolicy?.authorize && await usagePolicy.authorize({ sessionId: job.sessionId, model: profile, operationId, maxOutputTokens: profile.responseReserve, signal }) !== true) fail('CTX_USAGE_DENIED', 'Il servizio della sessione non autorizza questa sintesi.');
|
|
194
|
+
let response;
|
|
195
|
+
try { response = await model.summarize({ model: profile, messages: request.messages, maxOutputTokens: profile.responseReserve, signal, operationId }); }
|
|
196
|
+
catch (error) { await account(job, operationId, error.usage); throw error; }
|
|
197
|
+
await account(job, operationId, response.usage);
|
|
198
|
+
signal?.throwIfAborted();
|
|
199
|
+
return validateSummary(response, { records: sourceRecords });
|
|
200
|
+
};
|
|
201
|
+
try { return await once(false); }
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error?.code !== 'CTX_TRUNCATED_SUMMARY') throw error;
|
|
204
|
+
return once(true);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const summaries = [];
|
|
208
|
+
for (let index = 0; index < segments.length; index++) {
|
|
209
|
+
const segment = segments[index];
|
|
210
|
+
const fingerprint = await hash({ segment, profile, focus: snapshot.settings.focus });
|
|
211
|
+
const prior = job.completedSegments.find(entry => entry.fingerprint === fingerprint);
|
|
212
|
+
if (prior) { summaries.push(validateSummary({ text: JSON.stringify(prior.summary), finishReason: 'stop' }, { records })); continue; }
|
|
213
|
+
const build = compact => buildSummaryRequest({ segment, focus: snapshot.settings.focus, maxOutputTokens: profile.responseReserve, compact });
|
|
214
|
+
const request = build(false);
|
|
215
|
+
const measured = await measure(request.messages, [], profile, signal);
|
|
216
|
+
if (!budgetFor(measured, snapshot.settings).fits) {
|
|
217
|
+
if (segment.text.length < 512) fail('CTX_CONTEXT_TOO_SMALL', 'Istruzioni e segmento minimo non entrano nella finestra.');
|
|
218
|
+
let midpoint = Math.floor(segment.text.length / 2);
|
|
219
|
+
if (/[\uD800-\uDBFF]/u.test(segment.text[midpoint - 1])) midpoint--;
|
|
220
|
+
segments.splice(index, 1, { ...segment, id: `${segment.id}.a`, text: segment.text.slice(0, midpoint) }, { ...segment, id: `${segment.id}.b`, text: segment.text.slice(midpoint) });
|
|
221
|
+
index--; continue;
|
|
222
|
+
}
|
|
223
|
+
const summary = await invoke(build, records.filter(record => segment.sourceIds.includes(record.id)), segment.id);
|
|
224
|
+
summaries.push(summary);
|
|
225
|
+
job = await save(job, { completedSegments: [...job.completedSegments, { fingerprint, segmentId: segment.id, summary }], progress: { completed: job.completedSegments.length + 1, total: Math.max(segments.length, job.completedSegments.length + 1), phase: 'summarizing' } });
|
|
226
|
+
}
|
|
227
|
+
// Hierarchical reduction is measured at every level; no oversized merge is sent.
|
|
228
|
+
let level = summaries;
|
|
229
|
+
for (let depth = 0; level.length > 1; depth++) {
|
|
230
|
+
if (depth >= 12) fail('CTX_NO_REDUCTION', 'La sintesi non riduce il contesto dopo i passaggi consentiti.');
|
|
231
|
+
const next = [];
|
|
232
|
+
for (let index = 0; index < level.length; index += 2) {
|
|
233
|
+
if (index + 1 === level.length) { next.push(level[index]); continue; }
|
|
234
|
+
const pair = level.slice(index, index + 2);
|
|
235
|
+
const merged = await invoke(compact => buildSummaryRequest({ summaries: pair, focus: snapshot.settings.focus, maxOutputTokens: profile.responseReserve, compact }), records, `merge-${depth}-${index}`);
|
|
236
|
+
if (JSON.stringify(merged).length >= JSON.stringify(pair).length) fail('CTX_NO_REDUCTION', 'La fusione non libera spazio.');
|
|
237
|
+
next.push(merged);
|
|
238
|
+
}
|
|
239
|
+
level = next;
|
|
240
|
+
}
|
|
241
|
+
job = await save(job, { state: 'validating', progress: { ...job.progress, phase: 'validating' } });
|
|
242
|
+
const latest = await assertCurrent(job, signal);
|
|
243
|
+
const grezzi = await originals(job.sessionId);
|
|
244
|
+
const all = overlay(grezzi, projection);
|
|
245
|
+
if (selectClosedPrefix(all, { retainRecentTurns: 0 }).pendingCalls.length) fail('CTX_PENDING_TOOLS', 'Attendere i risultati degli strumenti prima della pubblicazione.');
|
|
246
|
+
// 24/09 — la versione è un fatto d'ARCHIVIO: prefisso, `sourceHash` e citazioni sugli originali grezzi.
|
|
247
|
+
const prefix = grezzi.filter(record => record.sequence <= job.coveredThrough);
|
|
248
|
+
const summary = projection === undefined ? level[0] : ricitaSugliOriginali(level[0], prefix);
|
|
249
|
+
const active = compiled(latest, all, { summary, coveredThrough: job.coveredThrough, targetModel: sessionModel });
|
|
250
|
+
const before = await measure(compiled(latest, all, { targetModel: sessionModel }), tools, sessionModel, signal);
|
|
251
|
+
const measurement = await measure(active, tools, sessionModel, signal);
|
|
252
|
+
const budget = budgetFor(measurement, latest.settings);
|
|
253
|
+
if (!budget.fits || measurement.inputTokens >= before.inputTokens) fail('CTX_NO_REDUCTION', 'La sintesi non libera spazio sufficiente. La versione precedente rimane valida.');
|
|
254
|
+
job = await save(job, { state: 'ready', progress: { ...job.progress, phase: 'ready' } });
|
|
255
|
+
await assertCurrent(job, signal);
|
|
256
|
+
const version = ContextVersionV1.parse({ schema: 'talos.context.version.v1', id: idFactory(), sessionId: job.sessionId, coveredThrough: job.coveredThrough, sourceIds: prefix.map(r => r.id), sourceHash: await hash(prefix.map(({ id, sha256 }) => ({ id, sha256 }))), summary, activeMessages: compiled(latest, prefix, { summary, coveredThrough: job.coveredThrough, targetModel: sessionModel }), model: job.model, measurement, createdAt: clock() });
|
|
257
|
+
signal?.throwIfAborted();
|
|
258
|
+
await store.commitContextVersion({ sessionId: job.sessionId, expectedRevision: latest.revision, expectedStateRevision: latest.stateRevision, jobId: job.id, version });
|
|
259
|
+
return store.readContextJob({ sessionId: job.sessionId, jobId: job.id });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
const current = await store.readContextJob({ sessionId: job.sessionId, jobId: job.id });
|
|
262
|
+
if (current && terminal.has(current.state)) return current;
|
|
263
|
+
const cancelled = signal?.aborted || error.code === 'CTX_JOB_CANCELLED';
|
|
264
|
+
const paused = ['CTX_PENDING_TOOLS', 'CTX_USAGE_DENIED', 'CTX_RESOURCE_BUSY'].includes(error.code);
|
|
265
|
+
const finale = await save(current ?? job, { state: cancelled ? 'cancelled' : paused ? 'paused' : 'failed', error: { code: cancelled ? 'CTX_JOB_CANCELLED' : error.code?.startsWith('CTX_') ? error.code : 'CTX_COMPACTION_FAILED', message: cancelled ? 'Compattazione annullata.' : error.code?.startsWith('CTX_') ? error.message : 'Preparazione del contesto non riuscita.' } });
|
|
266
|
+
// 25/09 — l'avviso della pausa nasce QUI, quando il lavoro fallisce: anche l'ultimo passo di un giro lo lascia, e
|
|
267
|
+
// una richiesta che arriva a pausa già scaduta non lo perde. Nessun catch: un archivio che non scrive è un guasto vero.
|
|
268
|
+
if (finale.state === 'failed') await avvisaPausa(job.sessionId);
|
|
269
|
+
return finale;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function launch(job, options) {
|
|
273
|
+
const jobKey = key(job.sessionId, job.id);
|
|
274
|
+
if (running.has(jobKey) || terminal.has(job.state)) return;
|
|
275
|
+
const controller = new AbortController();
|
|
276
|
+
const abort = () => controller.abort(options.signal.reason);
|
|
277
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
278
|
+
if (options.signal?.aborted) abort();
|
|
279
|
+
const entry = { controller, promise: null, error: null };
|
|
280
|
+
// Keep a handled result even when the persistence worker dies during failure reporting.
|
|
281
|
+
entry.promise = Promise.resolve().then(() => execute(job, { ...options, signal: controller.signal })).catch(error => { entry.error = error; return null; }).finally(() => options.signal?.removeEventListener('abort', abort));
|
|
282
|
+
running.set(jobKey, entry);
|
|
283
|
+
}
|
|
284
|
+
/*
|
|
285
|
+
* 24/09/2026 — F4, CTX-RESTART-ACTIVE-JOB-RECOVERY (nominato nel dossier Codex del 23/09, mai esistito).
|
|
286
|
+
* Un job `queued/preparing/summarizing/validating/ready` che NON sta in `running` non ha un processo dietro:
|
|
287
|
+
* la mappa `running` muore col processo, la riga SQLite no. Prima di oggi restava «in corso» per sempre —
|
|
288
|
+
* `waitForCompaction` lo ritornava com'era, `prepareForRequest` moriva `CTX_COMPACTION_REQUIRED` a ogni
|
|
289
|
+
* richiesta e il worker rifiutava ogni job nuovo (`sqlite-worker.mjs:146`, `CTX_JOB_ACTIVE`). Qui diventa
|
|
290
|
+
* `paused` con motivo `CTX_JOB_INTERRUPTED`: la stessa semantica della pausa già esistente (si riprende in
|
|
291
|
+
* automatico con i segmenti già pagati, e un job nuovo può partire). Nessuna versione è mai stata scritta
|
|
292
|
+
* da un job non `committed` (`commitContextVersion` è una transazione sola). Come Hermes, che recupera la
|
|
293
|
+
* lease di un pid morto (`hermes_state_compression.py:455-472`, «Reclaimed stale compression lock») e
|
|
294
|
+
* BullMQ («stalled jobs»): un job senza processo si dichiara, non si aspetta.
|
|
295
|
+
* ⛔ In questo processo un job attivo è SEMPRE in `running`: `startCompaction` e `resumeCompaction` lanciano
|
|
296
|
+
* nella stessa continuazione della `claimContextJob`, senza un `await` in mezzo.
|
|
297
|
+
*/
|
|
298
|
+
async function recoverInterrupted(sessionId, snapshot) {
|
|
299
|
+
const orfani = snapshot.jobs.filter(job => activeStates.has(job.state) && !running.has(key(sessionId, job.id)));
|
|
300
|
+
if (!orfani.length) return snapshot;
|
|
301
|
+
for (const job of orfani) {
|
|
302
|
+
await store.saveJobProgress({ sessionId, job: ContextJobV1.parse({ ...job, state: 'paused', updatedAt: clock(), error: { code: 'CTX_JOB_INTERRUPTED', message: 'Compattazione interrotta: il processo che la eseguiva non c’è più (riavvio del server). Nessuna versione è stata pubblicata; la compattazione può ripartire.' } }) });
|
|
303
|
+
}
|
|
304
|
+
return state(sessionId);
|
|
305
|
+
}
|
|
306
|
+
const api = {
|
|
307
|
+
async recoverInterruptedJobs({ sessionId }) { return recoverInterrupted(sessionId, await state(sessionId)); },
|
|
308
|
+
async appendOriginal({ sessionId, record }) {
|
|
309
|
+
const parsed = parseContextRecord(record);
|
|
310
|
+
await store.initSession({ sessionId, settings: parseContextSettings({}) });
|
|
311
|
+
for (const id of parsed.assetRefs) if (!await store.readBlob({ sessionId, id })) fail('CTX_ASSET_MISSING', 'Conservare l’allegato prima di archiviare il messaggio.');
|
|
312
|
+
return store.appendOriginalBatch({ sessionId, records: [parsed] });
|
|
313
|
+
},
|
|
314
|
+
async startCompaction({ sessionId, idempotencyKey, sessionModel, kind = 'compact', tools = [], signal, projection }) {
|
|
315
|
+
signal?.throwIfAborted();
|
|
316
|
+
const snapshot = await recoverInterrupted(sessionId, await state(sessionId));
|
|
317
|
+
const existing = snapshot.jobs.find(job => job.idempotencyKey === idempotencyKey);
|
|
318
|
+
if (existing) {
|
|
319
|
+
const profile = await model.resolveModel({ sessionModel, settings: snapshot.settings });
|
|
320
|
+
if (!sameModel(existing.model, profile) || existing.kind !== kind) fail('CTX_IDEMPOTENCY_CONFLICT', 'Questa chiave identifica una richiesta diversa.');
|
|
321
|
+
return existing;
|
|
322
|
+
}
|
|
323
|
+
const records = await originals(sessionId);
|
|
324
|
+
const selection = selectClosedPrefix(records, { retainRecentTurns: snapshot.settings.retainRecentTurns, force: true });
|
|
325
|
+
if (selection.pendingCalls.length) fail('CTX_PENDING_TOOLS', 'Attendere i risultati degli strumenti.');
|
|
326
|
+
if (!selection.prefix.length) fail('CTX_NOTHING_TO_COMPACT', 'Non ci sono scambi precedenti da compattare mantenendo intero l’ultimo scambio. Nessun messaggio è stato modificato.');
|
|
327
|
+
const profile = await model.resolveModel({ sessionModel, settings: snapshot.settings });
|
|
328
|
+
const fingerprint = await hash({ sessionId, revision: snapshot.revision, settings: snapshot.settings, profile, kind, tools });
|
|
329
|
+
const job = ContextJobV1.parse({ schema: 'talos.context.job.v1', id: idFactory(), sessionId, idempotencyKey, requestFingerprint: fingerprint, kind, state: 'queued', baseRevision: snapshot.revision, baseStateRevision: snapshot.stateRevision, coveredThrough: selection.coveredThrough, model: identity(profile), createdAt: clock(), updatedAt: clock(), completedSegments: [], progress: { completed: 0, total: 0, phase: 'queued' } });
|
|
330
|
+
const claimed = await store.claimContextJob({ sessionId, job });
|
|
331
|
+
launch(claimed, { sessionModel, tools, signal, projection });
|
|
332
|
+
return claimed;
|
|
333
|
+
},
|
|
334
|
+
async waitForCompaction({ sessionId, jobId }) {
|
|
335
|
+
const entry = running.get(key(sessionId, jobId));
|
|
336
|
+
if (entry) { await entry.promise; if (entry.error) throw entry.error; }
|
|
337
|
+
const job = await store.readContextJob({ sessionId, jobId });
|
|
338
|
+
if (!job) fail('CTX_JOB_NOT_FOUND', 'Compattazione non trovata.');
|
|
339
|
+
return job;
|
|
340
|
+
},
|
|
341
|
+
async cancelCompaction({ sessionId, jobId }) {
|
|
342
|
+
const job = await store.readContextJob({ sessionId, jobId });
|
|
343
|
+
if (!job) fail('CTX_JOB_NOT_FOUND', 'Compattazione non trovata.');
|
|
344
|
+
if (terminal.has(job.state)) return job;
|
|
345
|
+
try { return await save(job, { state: 'cancelled' }); }
|
|
346
|
+
finally { running.get(key(sessionId, jobId))?.controller.abort(new ContextEngineError('Compattazione annullata.', 'CTX_JOB_CANCELLED')); }
|
|
347
|
+
},
|
|
348
|
+
async resumeCompaction({ sessionId, jobId, sessionModel, tools = [], signal, projection }) {
|
|
349
|
+
let job = await store.readContextJob({ sessionId, jobId });
|
|
350
|
+
if (!job) fail('CTX_JOB_NOT_FOUND', 'Compattazione non trovata.');
|
|
351
|
+
if (terminal.has(job.state)) return job;
|
|
352
|
+
const entry = running.get(key(sessionId, jobId));
|
|
353
|
+
if (entry) { await entry.promise; running.delete(key(sessionId, jobId)); }
|
|
354
|
+
job = await store.readContextJob({ sessionId, jobId });
|
|
355
|
+
if (terminal.has(job.state)) return job;
|
|
356
|
+
await assertCurrent(job, signal);
|
|
357
|
+
if (job.state !== 'paused') job = await save(job, { state: 'paused' });
|
|
358
|
+
const { error: previousError, ...resumable } = job;
|
|
359
|
+
job = await store.claimContextJob({ sessionId, job: { ...resumable, state: 'queued', updatedAt: clock() } });
|
|
360
|
+
launch(job, { sessionModel, tools, signal, projection });
|
|
361
|
+
return job;
|
|
362
|
+
},
|
|
363
|
+
/** `projection`: la storia corretta, allineata 1:1 ai record (vedi `checkProjection`); `messages` resta il controllo d'archivio di prima. */
|
|
364
|
+
async prepareForRequest({ sessionId, messages, projection, tools = [], sessionModel, signal }) {
|
|
365
|
+
let snapshot = await recoverInterrupted(sessionId, await state(sessionId));
|
|
366
|
+
const records = await originals(sessionId);
|
|
367
|
+
if (messages && JSON.stringify(messages) !== JSON.stringify(records.map(r => r.message))) fail('CTX_UNARCHIVED_CONTEXT', 'Archiviare i nuovi messaggi prima di preparare la richiesta.');
|
|
368
|
+
checkProjection(records, projection);
|
|
369
|
+
let prepared = compiled(snapshot, overlay(records, projection), { targetModel: sessionModel });
|
|
370
|
+
let measurement = await measure(prepared, tools, sessionModel, signal);
|
|
371
|
+
/* 09/09 — si salva SUBITO, prima di qualunque automazione: se la compattazione automatica fallisce
|
|
372
|
+
(CTX_NO_REDUCTION su un solo messaggio enorme, per esempio) la misura che ha fatto scattare tutto
|
|
373
|
+
è comunque un fatto vero, ed è quello che la modale deve poter mostrare. */
|
|
374
|
+
const record = m => store.recordMeasurement({ sessionId, revision: snapshot.revision, measuredAt: clock(), measurement: m });
|
|
375
|
+
await record(measurement);
|
|
376
|
+
const budget = budgetFor(measurement, snapshot.settings);
|
|
377
|
+
let job; let compactionCooling = null;
|
|
378
|
+
if (snapshot.settings.auto && budget.shouldPrepare) {
|
|
379
|
+
job = snapshot.jobs.find(entry => !terminal.has(entry.state));
|
|
380
|
+
// 25/09 — in pausa dopo un rifiuto: niente lavoro nuovo (l'avviso si riscrive con lo stesso id: copre i lavori
|
|
381
|
+
// falliti prima di questa versione)
|
|
382
|
+
if (!job) compactionCooling = await avvisaPausa(sessionId, snapshot.jobs);
|
|
383
|
+
if (!job && !compactionCooling) {
|
|
384
|
+
try { job = await api.startCompaction({ sessionId, idempotencyKey: `auto-${snapshot.revision}-${await hash(identity(sessionModel))}`, sessionModel, tools, signal, projection }); }
|
|
385
|
+
catch (error) { if (error.code !== 'CTX_NOTHING_TO_COMPACT') throw error; }
|
|
386
|
+
}
|
|
387
|
+
if (!budget.fits && job) {
|
|
388
|
+
if (job.state === 'paused') job = await api.resumeCompaction({ sessionId, jobId: job.id, sessionModel, tools, signal, projection });
|
|
389
|
+
const result = await api.waitForCompaction({ sessionId, jobId: job.id });
|
|
390
|
+
if (result.state !== 'committed') fail(result.error?.code ?? 'CTX_COMPACTION_REQUIRED', result.error?.message ?? 'Il contesto richiede una compattazione completata.');
|
|
391
|
+
snapshot = await state(sessionId);
|
|
392
|
+
prepared = compiled(snapshot, overlay(await originals(sessionId), projection), { targetModel: sessionModel });
|
|
393
|
+
measurement = await measure(prepared, tools, sessionModel, signal);
|
|
394
|
+
await record(measurement); // dopo una compattazione riuscita la misura nuova sostituisce quella vecchia
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (!budgetFor(measurement, snapshot.settings).fits) {
|
|
398
|
+
// il tempo che RESTA, non la durata del gradino: a pausa quasi scaduta «per 15 min» mentirebbe
|
|
399
|
+
if (compactionCooling) fail('CTX_CONTEXT_OVERFLOW', `Il contesto supera la finestra e la compattazione automatica è in pausa ancora per circa ${Math.max(1, Math.ceil((Date.parse(compactionCooling.retryAfter) - Date.parse(clock())) / 60_000))} min dopo un riassunto non riuscito (${compactionCooling.code}). Usa «Compatta» per riprovare subito, o modifica le informazioni protette.`);
|
|
400
|
+
fail('CTX_CONTEXT_OVERFLOW', 'Il contesto supera la finestra. Compattare o modificare le informazioni protette.');
|
|
401
|
+
}
|
|
402
|
+
return { messages: prepared, measurement, versionId: snapshot.activeVersion?.id ?? null, ...(job ? { job } : {}), ...(compactionCooling ? { compactionCooling } : {}), waiting: false };
|
|
403
|
+
},
|
|
404
|
+
getContextState: async ({ sessionId }) => recoverInterrupted(sessionId, await state(sessionId)),
|
|
405
|
+
async updateContextSettings({ sessionId, patch, expectedRevision }) {
|
|
406
|
+
const snapshot = await state(sessionId);
|
|
407
|
+
const settings = parseContextSettings({ ...snapshot.settings, ...patch });
|
|
408
|
+
return store.updateSessionSettings({ sessionId, settings, expectedRevision });
|
|
409
|
+
},
|
|
410
|
+
listContextVersions: options => store.listContextVersions(options),
|
|
411
|
+
restoreContextVersion: options => store.restoreContextVersion({ ...options, newVersionId: idFactory(), createdAt: clock() }),
|
|
412
|
+
async upsertProtectedFact({ sessionId, fact, expectedRevision, actor }) {
|
|
413
|
+
if (!['owner', 'model'].includes(actor)) fail('CTX_FACT_ACTOR_INVALID', 'Indicare l’origine della modifica.');
|
|
414
|
+
const snapshot = await state(sessionId);
|
|
415
|
+
const prior = snapshot.facts.find(entry => entry.id === fact.id);
|
|
416
|
+
const updated = actor === 'model' && prior && prior.status !== 'removed' && prior.text !== fact.text
|
|
417
|
+
? { ...prior, revision: prior.revision + 1, status: 'conflict', conflict: { proposedText: fact.text, sources: fact.sources ?? [] } }
|
|
418
|
+
: { id: fact.id, text: fact.text, sources: fact.sources ?? [], status: 'active', revision: (prior?.revision ?? 0) + 1 };
|
|
419
|
+
return store.upsertProtectedFact({ sessionId, fact: updated, expectedRevision });
|
|
420
|
+
},
|
|
421
|
+
removeProtectedFact: options => store.removeProtectedFact(options),
|
|
422
|
+
async resolveFactConflict({ sessionId, factId, accept, expectedRevision }) {
|
|
423
|
+
if (typeof accept !== 'boolean') fail('CTX_INVALID_INPUT', 'Confermare o rifiutare la proposta.');
|
|
424
|
+
const prior = (await state(sessionId)).facts.find(f => f.id === factId);
|
|
425
|
+
if (!prior || prior.status !== 'conflict') fail('CTX_FACT_CONFLICT_NOT_FOUND', 'Nessun conflitto aperto.');
|
|
426
|
+
return api.upsertProtectedFact({ sessionId, expectedRevision, actor: 'owner', fact: { id: factId, text: accept ? prior.conflict.proposedText : prior.text, sources: accept ? prior.conflict.sources : prior.sources } });
|
|
427
|
+
},
|
|
428
|
+
async searchContext({ sessionId, query }) {
|
|
429
|
+
await state(sessionId);
|
|
430
|
+
if (retrieval?.searchContext) return retrieval.searchContext({ sessionId, query });
|
|
431
|
+
const records = await originals(sessionId);
|
|
432
|
+
await store.replaceSearchChunks({ sessionId, chunks: chunkContextRecords(records) });
|
|
433
|
+
const lexical = await store.searchLexical({ sessionId, query });
|
|
434
|
+
return selectContextEvidence(rankContextSources({ lexical }));
|
|
435
|
+
},
|
|
436
|
+
async readContextSource({ sessionId, sourceId }) {
|
|
437
|
+
const [record] = await store.readOriginals({ sessionId, ids: [sourceId], limit: 1 });
|
|
438
|
+
if (!record) fail('CTX_SOURCE_NOT_FOUND', 'Fonte non presente in questa conversazione.');
|
|
439
|
+
return record;
|
|
440
|
+
},
|
|
441
|
+
exportContext: options => store.exportSession(options),
|
|
442
|
+
importContext: options => store.importSession(options),
|
|
443
|
+
};
|
|
444
|
+
return Object.freeze(api);
|
|
445
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { ContextStoreError } from './sqlite-store.mjs';
|
|
3
|
+
|
|
4
|
+
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
5
|
+
const fail = message => { throw new ContextStoreError(message, 'CTX_ARCHIVE_INVALID'); };
|
|
6
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
7
|
+
const id = value => typeof value === 'string' && value.length > 0 && value.length <= 256 && !value.includes('\0');
|
|
8
|
+
const integer = value => Number.isSafeInteger(value) && value >= 0;
|
|
9
|
+
const date = value => typeof value === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?(?:Z|[+-]\d\d:\d\d)$/.test(value) && Number.isFinite(Date.parse(value));
|
|
10
|
+
const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
11
|
+
const textOf = message => typeof message.content === 'string' ? message.content : Array.isArray(message.content) ? message.content.filter(part => part && ['text', 'input_text', 'output_text'].includes(part.type) && typeof part.text === 'string').map(part => part.text).join('\n') : '';
|
|
12
|
+
|
|
13
|
+
function jsonData(value, ancestors = new Set()) {
|
|
14
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) return;
|
|
15
|
+
if ((!object(value) && !Array.isArray(value)) || ancestors.has(value)) fail('Archive must contain finite, acyclic JSON values');
|
|
16
|
+
ancestors.add(value);
|
|
17
|
+
if (Array.isArray(value) && Object.keys(value).length !== value.length) fail('Sparse arrays are not JSON data');
|
|
18
|
+
for (const child of Object.values(value)) jsonData(child, ancestors);
|
|
19
|
+
ancestors.delete(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function settings(value) {
|
|
23
|
+
const keys = ['auto', 'model', 'triggerRatio', 'targetRatio', 'retainRecentTurns', 'focus', 'nativeMode', 'semanticSearch'];
|
|
24
|
+
if (!object(value) || Object.keys(value).length !== keys.length || keys.some(key => !Object.hasOwn(value, key))) fail('Invalid context settings');
|
|
25
|
+
if (typeof value.auto !== 'boolean' || typeof value.semanticSearch !== 'boolean' || typeof value.targetRatio !== 'number' || typeof value.triggerRatio !== 'number' || !(value.targetRatio > 0 && value.targetRatio < value.triggerRatio && value.triggerRatio < 1) || !integer(value.retainRecentTurns) || value.retainRecentTurns > 100 || typeof value.focus !== 'string' || value.focus.length > 8000 || !['off', 'qualified'].includes(value.nativeMode)) fail('Invalid context settings values');
|
|
26
|
+
if (!object(value.model) || !['follow-session', 'explicit'].includes(value.model.mode)) fail('Invalid context model');
|
|
27
|
+
const modelKeys = value.model.mode === 'explicit' ? ['mode', 'provider', 'model'] : ['mode'];
|
|
28
|
+
if (Object.keys(value.model).length !== modelKeys.length || modelKeys.some(key => !id(value.model[key]))) fail('Invalid context model fields');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function message(value) {
|
|
32
|
+
if (!object(value) || !['system', 'developer', 'user', 'assistant', 'tool'].includes(value.role) || (!Object.hasOwn(value, 'content') && !(value.role === 'assistant' && Array.isArray(value.tool_calls) && value.tool_calls.length))) fail('Invalid message');
|
|
33
|
+
if (value.role === 'tool' && !id(value.tool_call_id)) fail('Tool message has no call id');
|
|
34
|
+
if (value.tool_calls !== undefined) {
|
|
35
|
+
if (value.role !== 'assistant' || !Array.isArray(value.tool_calls)) fail('Invalid tool calls');
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
for (const call of value.tool_calls) {
|
|
38
|
+
if (!object(call) || !id(call.id) || seen.has(call.id) || call.type !== 'function' || !object(call.function) || !id(call.function.name) || typeof call.function.arguments !== 'string') fail('Malformed tool call');
|
|
39
|
+
seen.add(call.id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sources(values, records) {
|
|
45
|
+
if (!Array.isArray(values)) fail('Invalid source references');
|
|
46
|
+
for (const source of values) {
|
|
47
|
+
if (!object(source) || !id(source.recordId) || typeof source.quote !== 'string' || !source.quote || !records.has(source.recordId)) fail('Source has no original');
|
|
48
|
+
const text = textOf(records.get(source.recordId).message);
|
|
49
|
+
const start = source.start === undefined ? text.indexOf(source.quote) : source.start;
|
|
50
|
+
if (!integer(start) || text.slice(start, start + source.quote.length) !== source.quote || (source.end !== undefined && source.end !== start + source.quote.length)) fail('Source quote or offsets do not match original');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function version(value, archive, records) {
|
|
55
|
+
if (!object(value) || value.schema !== 'talos.context.version.v1' || !id(value.id) || value.sessionId !== archive.session.sessionId || !integer(value.coveredThrough) || value.coveredThrough > archive.session.headSequence || !date(value.createdAt)) fail('Invalid context version');
|
|
56
|
+
const prefix = archive.records.filter(record => record.sequence <= value.coveredThrough);
|
|
57
|
+
if (JSON.stringify(value.sourceIds) !== JSON.stringify(prefix.map(record => record.id)) || value.sourceHash !== digest(JSON.stringify(prefix.map(({ id, sha256 }) => ({ id, sha256 }))))) fail('Version source provenance mismatch');
|
|
58
|
+
const summary = value.summary;
|
|
59
|
+
if (!object(summary) || summary.schema !== 'talos.context.summary.v1' || typeof summary.text !== 'string' || !summary.text.trim() || typeof summary.goal !== 'string' || !summary.goal.trim()) fail('Invalid summary');
|
|
60
|
+
for (const field of ['decisions', 'constraints', 'completed', 'pending', 'resources']) if (!Array.isArray(summary[field]) || summary[field].some(item => typeof item !== 'string')) fail('Invalid summary entries');
|
|
61
|
+
sources(summary.sources, new Map(prefix.map(record => [record.id, record])));
|
|
62
|
+
if (prefix.length && !summary.sources.length) fail('Summary must cite its originals');
|
|
63
|
+
if (!Array.isArray(value.activeMessages) || !value.activeMessages.length) fail('Invalid active messages');
|
|
64
|
+
for (const entry of value.activeMessages) message(entry);
|
|
65
|
+
if (!object(value.model) || !id(value.model.provider) || !id(value.model.model)) fail('Invalid version model');
|
|
66
|
+
const measurement = value.measurement;
|
|
67
|
+
if (!object(measurement) || measurement.schema !== 'talos.context.tokens.v1' || !integer(measurement.inputTokens) || !integer(measurement.windowTokens) || !integer(measurement.responseReserve) || !['runtime', 'provider', 'heuristic'].includes(measurement.method) || typeof measurement.exact !== 'boolean' || !hash(measurement.requestHash) || !id(measurement.provider) || !id(measurement.model)) fail('Invalid version measurement');
|
|
68
|
+
if (value.restoredFrom !== undefined && (!id(value.restoredFrom) || value.restoredFrom === value.id || !archive.versions.some(other => other.id === value.restoredFrom))) fail('Missing restored version provenance');
|
|
69
|
+
// A compacted prefix is closed: replay must not strand pending tools.
|
|
70
|
+
const pending = new Set();
|
|
71
|
+
for (const record of prefix) {
|
|
72
|
+
for (const call of record.message.tool_calls ?? []) { if (pending.has(call.id)) fail('Duplicate pending tool call'); pending.add(call.id); }
|
|
73
|
+
if (record.message.role === 'tool' && !pending.delete(record.message.tool_call_id)) fail('Orphan tool result in version');
|
|
74
|
+
}
|
|
75
|
+
if (pending.size) fail('Version covers an open tool prefix');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Verify before any import writes. The manifest is integrity evidence, not a
|
|
79
|
+
* signature or authorization: all nested ownership/provenance is checked too. */
|
|
80
|
+
export function verifyContextArchive(archive) {
|
|
81
|
+
try {
|
|
82
|
+
jsonData(archive);
|
|
83
|
+
if (!object(archive) || archive.schema !== 'talos.context.archive.v1') fail('Unsupported context archive');
|
|
84
|
+
const keys = ['schema', 'session', 'records', 'versions', 'facts', 'jobs', 'usage', 'blobs', 'manifest', ...(Object.hasOwn(archive, 'mutations') ? ['mutations'] : [])];
|
|
85
|
+
if (Object.keys(archive).length !== keys.length || keys.some(key => !Object.hasOwn(archive, key))) fail('Unexpected archive fields');
|
|
86
|
+
const { manifest, ...payload } = archive;
|
|
87
|
+
if (!object(manifest) || manifest.schema !== 'talos.context.manifest.v1' || manifest.algorithm !== 'sha256' || !hash(manifest.payloadSha256) || manifest.payloadSha256 !== digest(JSON.stringify(payload))) fail('Archive manifest mismatch');
|
|
88
|
+
const session = archive.session;
|
|
89
|
+
if (!object(session) || session.schema !== 'talos.context.snapshot.v1' || !id(session.sessionId) || !integer(session.revision) || !integer(session.stateRevision) || session.stateRevision > session.revision || !integer(session.headSequence) || !object(session.metadata)) fail('Invalid session snapshot');
|
|
90
|
+
settings(session.settings);
|
|
91
|
+
for (const field of ['records', 'versions', 'facts', 'jobs', 'usage', 'blobs']) if (!Array.isArray(archive[field])) fail(`Invalid archive ${field}`);
|
|
92
|
+
const unique = (items, key) => {
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
for (const item of items) { if (!object(item) || !id(item[key]) || seen.has(item[key])) fail(`Duplicate or invalid ${key}`); seen.add(item[key]); }
|
|
95
|
+
return seen;
|
|
96
|
+
};
|
|
97
|
+
const blobIds = unique(archive.blobs, 'id');
|
|
98
|
+
for (const blob of archive.blobs) {
|
|
99
|
+
if (!hash(blob.sha256) || typeof blob.mimeType !== 'string' || !blob.mimeType || blob.mimeType.length > 256 || typeof blob.base64 !== 'string') fail('Invalid blob manifest');
|
|
100
|
+
const bytes = Buffer.from(blob.base64, 'base64');
|
|
101
|
+
if (bytes.toString('base64') !== blob.base64 || digest(bytes) !== blob.sha256) fail('Blob bytes do not match manifest');
|
|
102
|
+
}
|
|
103
|
+
unique(archive.records, 'id');
|
|
104
|
+
if (archive.records.length !== session.headSequence) fail('Missing original sequence');
|
|
105
|
+
for (let i = 0; i < archive.records.length; i++) {
|
|
106
|
+
const record = archive.records[i];
|
|
107
|
+
if (record.schema !== 'talos.context.record.v1' || record.sessionId !== session.sessionId || record.sequence !== i + 1 || !date(record.createdAt)) fail('Invalid original provenance');
|
|
108
|
+
message(record.message);
|
|
109
|
+
if (record.sha256 !== digest(JSON.stringify(record.message))) fail('Original hash mismatch');
|
|
110
|
+
if (record.assetRefs !== undefined && (!Array.isArray(record.assetRefs) || record.assetRefs.some(ref => !blobIds.has(typeof ref === 'string' ? ref : ref?.id)))) fail('Original refers to absent asset');
|
|
111
|
+
}
|
|
112
|
+
const records = new Map(archive.records.map(record => [record.id, record]));
|
|
113
|
+
const versionIds = unique(archive.versions, 'id');
|
|
114
|
+
for (const entry of archive.versions) version(entry, archive, records);
|
|
115
|
+
if (session.activeVersion !== null && (!object(session.activeVersion) || !versionIds.has(session.activeVersion.id) || JSON.stringify(session.activeVersion) !== JSON.stringify(archive.versions.find(entry => entry.id === session.activeVersion.id)))) fail('Active version does not match immutable version');
|
|
116
|
+
unique(archive.facts, 'id');
|
|
117
|
+
for (const fact of archive.facts) {
|
|
118
|
+
if (typeof fact.text !== 'string' || !fact.text.trim() || !integer(fact.revision) || !['active', 'conflict', 'removed'].includes(fact.status)) fail('Invalid protected fact');
|
|
119
|
+
sources(fact.sources, records);
|
|
120
|
+
if (fact.status === 'conflict') {
|
|
121
|
+
if (!object(fact.conflict) || typeof fact.conflict.proposedText !== 'string' || !fact.conflict.proposedText.trim()) fail('Invalid fact conflict');
|
|
122
|
+
sources(fact.conflict.sources, records);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
unique(archive.jobs, 'id');
|
|
126
|
+
unique(archive.jobs, 'idempotencyKey');
|
|
127
|
+
let activeJobs = 0;
|
|
128
|
+
for (const job of archive.jobs) {
|
|
129
|
+
if (job.schema !== 'talos.context.job.v1' || job.sessionId !== session.sessionId || !id(job.requestFingerprint) || !['compact', 'regenerate', 'restore'].includes(job.kind) || !['queued', 'preparing', 'summarizing', 'validating', 'ready', 'committed', 'paused', 'cancelled', 'failed'].includes(job.state) || !integer(job.baseRevision) || !integer(job.baseStateRevision) || !integer(job.coveredThrough) || job.coveredThrough > session.headSequence || !date(job.createdAt) || !date(job.updatedAt) || !Array.isArray(job.completedSegments) || !object(job.progress) || !integer(job.progress.completed) || !integer(job.progress.total) || job.progress.completed > job.progress.total || typeof job.progress.phase !== 'string' || !object(job.model) || !id(job.model.provider) || !id(job.model.model)) fail('Invalid compaction job');
|
|
130
|
+
if (job.versionId !== undefined && !versionIds.has(job.versionId)) fail('Job refers to absent version');
|
|
131
|
+
if (job.state === 'committed' && !versionIds.has(job.versionId)) fail('Committed job lacks version');
|
|
132
|
+
if (['queued', 'preparing', 'summarizing', 'validating', 'ready'].includes(job.state)) activeJobs++;
|
|
133
|
+
}
|
|
134
|
+
if (activeJobs > 1 || JSON.stringify(session.jobs) !== JSON.stringify(archive.jobs) || JSON.stringify(session.facts) !== JSON.stringify(archive.facts)) fail('Snapshot derived state mismatch');
|
|
135
|
+
unique(archive.usage, 'operationId');
|
|
136
|
+
for (const item of archive.usage) {
|
|
137
|
+
if (item.sessionId !== session.sessionId || !object(item.usage) || (item.jobId != null && !archive.jobs.some(job => job.id === item.jobId))) fail('Invalid usage ownership');
|
|
138
|
+
for (const [key, value] of Object.entries(item.usage)) {
|
|
139
|
+
if (['inputTokens', 'outputTokens', 'totalTokens', 'cachedTokens'].includes(key) && !integer(value)) fail('Invalid usage token count');
|
|
140
|
+
if (key === 'cost' && (typeof value !== 'number' || !Number.isFinite(value) || value < 0)) fail('Invalid usage cost');
|
|
141
|
+
if (['currency', 'requestId'].includes(key) && typeof value !== 'string') fail('Invalid usage metadata');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (archive.mutations !== undefined) {
|
|
145
|
+
if (!Array.isArray(archive.mutations)) fail('Invalid mutation receipts');
|
|
146
|
+
unique(archive.mutations, 'idempotencyKey');
|
|
147
|
+
for (const receipt of archive.mutations) {
|
|
148
|
+
if (!hash(receipt.requestFingerprint) || !object(receipt.result) || (receipt.result.sessionId !== undefined && receipt.result.sessionId !== session.sessionId)) fail('Invalid mutation receipt ownership');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return JSON.parse(JSON.stringify(archive));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error?.code === 'CTX_ARCHIVE_INVALID') throw error;
|
|
154
|
+
throw new ContextStoreError('Malformed context archive', 'CTX_ARCHIVE_INVALID', { cause: error });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function exportContextArchive({ sessionId }, { store }) {
|
|
159
|
+
return verifyContextArchive(await store.exportSession({ sessionId }));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function importContextArchive(archive, { store }) {
|
|
163
|
+
return store.importSession({ archive: verifyContextArchive(archive) });
|
|
164
|
+
}
|