@snoai/memory 0.0.0-stage → 1.0.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/LICENSE +176 -0
- package/README.md +7 -2
- package/config/b-profile-section-registry.json +118 -0
- package/config/llm/compiled/2026-05-19T230932Z/manifest.json +25 -0
- package/config/llm/compiled/2026-05-19T230932Z/registry-root.json +67 -0
- package/config/llm/compiled/2026-05-19T230932Z/resolved/mem_claw/openai_gpt_5_nano.json +5 -0
- package/config/llm/compiled/2026-05-19T230932Z/resolved/mem_claw/openrouter_auto.json +5 -0
- package/config/llm/compiled/2026-05-19T230932Z/resolved/mem_claw/sno_ai_extract.json +5 -0
- package/config/llm/compiled/2026-05-19T230932Z/source/mem_claw/openai_gpt_5_nano.mda +24 -0
- package/config/llm/compiled/2026-05-19T230932Z/source/mem_claw/openrouter_auto.mda +24 -0
- package/config/llm/compiled/2026-05-19T230932Z/source/mem_claw/sno_ai_extract.mda +24 -0
- package/config/llm/compiled/2026-07-21T001311Z/manifest.json +43 -0
- package/config/llm/compiled/2026-07-21T001311Z/registry-root.json +97 -0
- package/config/llm/compiled/2026-07-21T001311Z/resolved/mem_claw/openai_gpt_5_nano.json +5 -0
- package/config/llm/compiled/2026-07-21T001311Z/resolved/mem_claw/openrouter_auto.json +5 -0
- package/config/llm/compiled/2026-07-21T001311Z/resolved/mem_claw/sno_ai_extract.json +5 -0
- package/config/llm/compiled/2026-07-21T001311Z/resolved/mem_claw/sno_conflict_verdict.json +10 -0
- package/config/llm/compiled/2026-07-21T001311Z/resolved/mem_claw/sno_extract_chat.json +10 -0
- package/config/llm/compiled/2026-07-21T001311Z/resolved/mem_claw/sno_extract_profile.json +10 -0
- package/config/llm/compiled/2026-07-21T001311Z/source/mem_claw/openai_gpt_5_nano.mda +24 -0
- package/config/llm/compiled/2026-07-21T001311Z/source/mem_claw/openrouter_auto.mda +24 -0
- package/config/llm/compiled/2026-07-21T001311Z/source/mem_claw/sno_ai_extract.mda +24 -0
- package/config/llm/compiled/2026-07-21T001311Z/source/mem_claw/sno_conflict_verdict.mda +27 -0
- package/config/llm/compiled/2026-07-21T001311Z/source/mem_claw/sno_extract_chat.mda +27 -0
- package/config/llm/compiled/2026-07-21T001311Z/source/mem_claw/sno_extract_profile.mda +27 -0
- package/config/llm/compiled/2026-08-29T060335Z/manifest.json +43 -0
- package/config/llm/compiled/2026-08-29T060335Z/registry-root.json +97 -0
- package/config/llm/compiled/2026-08-29T060335Z/resolved/mem_claw/openai_gpt_5_nano.json +5 -0
- package/config/llm/compiled/2026-08-29T060335Z/resolved/mem_claw/openrouter_auto.json +5 -0
- package/config/llm/compiled/2026-08-29T060335Z/resolved/mem_claw/sno_ai_extract.json +5 -0
- package/config/llm/compiled/2026-08-29T060335Z/resolved/mem_claw/sno_conflict_verdict.json +10 -0
- package/config/llm/compiled/2026-08-29T060335Z/resolved/mem_claw/sno_extract_chat.json +10 -0
- package/config/llm/compiled/2026-08-29T060335Z/resolved/mem_claw/sno_extract_profile.json +10 -0
- package/config/llm/compiled/2026-08-29T060335Z/source/mem_claw/openai_gpt_5_nano.mda +24 -0
- package/config/llm/compiled/2026-08-29T060335Z/source/mem_claw/openrouter_auto.mda +24 -0
- package/config/llm/compiled/2026-08-29T060335Z/source/mem_claw/sno_ai_extract.mda +24 -0
- package/config/llm/compiled/2026-08-29T060335Z/source/mem_claw/sno_conflict_verdict.mda +27 -0
- package/config/llm/compiled/2026-08-29T060335Z/source/mem_claw/sno_extract_chat.mda +27 -0
- package/config/llm/compiled/2026-08-29T060335Z/source/mem_claw/sno_extract_profile.mda +27 -0
- package/config/llm/compiled/2026-09-04T004828Z/manifest.json +43 -0
- package/config/llm/compiled/2026-09-04T004828Z/registry-root.json +97 -0
- package/config/llm/compiled/2026-09-04T004828Z/resolved/mem_claw/openai_gpt_5_nano.json +5 -0
- package/config/llm/compiled/2026-09-04T004828Z/resolved/mem_claw/openrouter_auto.json +5 -0
- package/config/llm/compiled/2026-09-04T004828Z/resolved/mem_claw/sno_ai_extract.json +5 -0
- package/config/llm/compiled/2026-09-04T004828Z/resolved/mem_claw/sno_conflict_verdict.json +10 -0
- package/config/llm/compiled/2026-09-04T004828Z/resolved/mem_claw/sno_extract_chat.json +10 -0
- package/config/llm/compiled/2026-09-04T004828Z/resolved/mem_claw/sno_extract_profile.json +10 -0
- package/config/llm/compiled/2026-09-04T004828Z/source/mem_claw/openai_gpt_5_nano.mda +24 -0
- package/config/llm/compiled/2026-09-04T004828Z/source/mem_claw/openrouter_auto.mda +24 -0
- package/config/llm/compiled/2026-09-04T004828Z/source/mem_claw/sno_ai_extract.mda +24 -0
- package/config/llm/compiled/2026-09-04T004828Z/source/mem_claw/sno_conflict_verdict.mda +27 -0
- package/config/llm/compiled/2026-09-04T004828Z/source/mem_claw/sno_extract_chat.mda +27 -0
- package/config/llm/compiled/2026-09-04T004828Z/source/mem_claw/sno_extract_profile.mda +27 -0
- package/config/llm/current.json +4 -0
- package/config/llm/source/mem_claw/openai_gpt_5_nano.mda +24 -0
- package/config/llm/source/mem_claw/openrouter_auto.mda +24 -0
- package/config/llm/source/mem_claw/sno_ai_extract.mda +24 -0
- package/config/llm/source/mem_claw/sno_conflict_verdict.mda +27 -0
- package/config/llm/source/mem_claw/sno_extract_chat.mda +27 -0
- package/config/llm/source/mem_claw/sno_extract_profile.mda +27 -0
- package/config/llm.trust.json +37 -0
- package/dist/_types-CUKHSRkS.d.ts +103 -0
- package/dist/access-tracker-CSc21GtV.d.ts +110 -0
- package/dist/adapter-DMEmCTKo.d.ts +69 -0
- package/dist/adapter-XaRHZWZ2.js +333 -0
- package/dist/agent-llm-port-5oXgBpzJ.d.ts +53 -0
- package/dist/all-resources-BQK2ugjB.js +6379 -0
- package/dist/all-resources-Dij6l5h0.d.ts +18 -0
- package/dist/ambient-learning-text-normalizer-CxjATmMp.js +170 -0
- package/dist/ambient-learning-text-normalizer-_fH3LeuK.d.ts +10 -0
- package/dist/atomic-extraction-reply-BJ9XV_9s.js +863 -0
- package/dist/atomic-extraction-skill-CcXmuCqL.js +56 -0
- package/dist/atomic-memory-extraction-BpOUWPJM.d.ts +327 -0
- package/dist/atomic-memory-extraction-sdNf1-B1.js +3605 -0
- package/dist/b-profile-section-dictionary-provider-BHkoNqgk.js +1812 -0
- package/dist/b-profile-section-dictionary-provider-BxbksLj3.d.ts +62 -0
- package/dist/best-effort-BPZSzKk0.d.ts +9 -0
- package/dist/calendar-instruction-CzqZbAPG.js +194 -0
- package/dist/capture-policy-detector-CHz5GOPA.d.ts +53 -0
- package/dist/capture-policy-detector-DaMfy9CZ.js +357 -0
- package/dist/classifier-C96uH9OF.d.ts +272 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.js +305 -0
- package/dist/coding-skin.d.ts +51 -0
- package/dist/coding-skin.js +55 -0
- package/dist/config-tSrvLXgV.js +2577 -0
- package/dist/connection-DAbzki8K.d.ts +391 -0
- package/dist/connection-DliCOBB2.js +297 -0
- package/dist/daily-log-generator-CH_KuWHs.js +469 -0
- package/dist/daily-log-generator-CsQPHl4A.d.ts +73 -0
- package/dist/daily-spend-estimator-DiwzBlo1.d.ts +8 -0
- package/dist/data-bootstrap-CYniFp8b.js +172 -0
- package/dist/data-bootstrap-CcJUwkqf.d.ts +30 -0
- package/dist/data-paths-C7sNdqvP.d.ts +22 -0
- package/dist/derived-suppression-cache-C7bGARc0.d.ts +36 -0
- package/dist/diagnostic-encoder.d.ts +6 -0
- package/dist/diagnostic-encoder.js +29 -0
- package/dist/embedding-provider-client-Q_hJonK9.js +294 -0
- package/dist/embedding-provider-client-vynZMuit.d.ts +94 -0
- package/dist/entry-metadata-parser-BIVSMCIk.js +20 -0
- package/dist/error-D-wb-rZw.js +23 -0
- package/dist/error-signals-D8_3vYu4.d.ts +28 -0
- package/dist/error-signals-lMH2uAdV.js +134 -0
- package/dist/errors-CeVRxf9V.d.ts +49 -0
- package/dist/extraction-text-sanitizer-B_AhIecT.d.ts +32 -0
- package/dist/i18n-text-DbD6Qify.d.ts +13 -0
- package/dist/index-BsMap4It.d.ts +650 -0
- package/dist/index-DL8AVj0Z.d.ts +571 -0
- package/dist/index-DQ7VL6d3.d.ts +350 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +1201 -0
- package/dist/intent-analyzer-B5XVrWwe.d.ts +28 -0
- package/dist/internal/config/index.d.ts +2 -0
- package/dist/internal/config/index.js +581 -0
- package/dist/internal/config/plugin-config-schema.d.ts +2 -0
- package/dist/internal/config/plugin-config-schema.js +2 -0
- package/dist/internal/config/product-mode.d.ts +2 -0
- package/dist/internal/config/product-mode.js +36 -0
- package/dist/internal/config/settings.d.ts +2 -0
- package/dist/internal/config/settings.js +265 -0
- package/dist/internal/config/skill-categories.d.ts +4 -0
- package/dist/internal/config/skill-categories.js +68 -0
- package/dist/internal/config/skin-defaults.d.ts +6 -0
- package/dist/internal/config/skin-defaults.js +6 -0
- package/dist/internal/contract/discovery.d.ts +10 -0
- package/dist/internal/contract/discovery.js +53 -0
- package/dist/internal/contract/profile.d.ts +2 -0
- package/dist/internal/contract/profile.js +44 -0
- package/dist/internal/contract/routes.d.ts +2 -0
- package/dist/internal/contract/routes.js +61 -0
- package/dist/internal/engine/bindings/memory-cli-shared.d.ts +47 -0
- package/dist/internal/engine/bindings/memory-cli-shared.js +84 -0
- package/dist/internal/engine/bindings/memory-forget-tool.d.ts +5 -0
- package/dist/internal/engine/bindings/memory-forget-tool.js +66 -0
- package/dist/internal/engine/bindings/memory-list-tool.d.ts +5 -0
- package/dist/internal/engine/bindings/memory-list-tool.js +57 -0
- package/dist/internal/engine/bindings/memory-recall-tool.d.ts +10 -0
- package/dist/internal/engine/bindings/memory-recall-tool.js +283 -0
- package/dist/internal/engine/bindings/memory-reflection-resolve-tool.d.ts +5 -0
- package/dist/internal/engine/bindings/memory-reflection-resolve-tool.js +150 -0
- package/dist/internal/engine/bindings/memory-stats-tool.d.ts +5 -0
- package/dist/internal/engine/bindings/memory-stats-tool.js +43 -0
- package/dist/internal/engine/bindings/memory-store-tool.d.ts +5 -0
- package/dist/internal/engine/bindings/memory-store-tool.js +165 -0
- package/dist/internal/engine/bindings/memory-tool-access.d.ts +16 -0
- package/dist/internal/engine/bindings/memory-tool-access.js +46 -0
- package/dist/internal/engine/bindings/memory-tool-formatting.d.ts +62 -0
- package/dist/internal/engine/bindings/memory-tool-formatting.js +106 -0
- package/dist/internal/engine/bindings/memory-tool-results.d.ts +8 -0
- package/dist/internal/engine/bindings/memory-tool-results.js +41 -0
- package/dist/internal/engine/bindings/memory-tool-schemas.d.ts +109 -0
- package/dist/internal/engine/bindings/memory-tool-schemas.js +75 -0
- package/dist/internal/engine/bindings/memory-update-tool.d.ts +5 -0
- package/dist/internal/engine/bindings/memory-update-tool.js +127 -0
- package/dist/internal/engine/bindings/sno-station-mem-ambient-learning-hook.d.ts +14 -0
- package/dist/internal/engine/bindings/sno-station-mem-ambient-learning-hook.js +248 -0
- package/dist/internal/engine/bindings/sno-station-mem-auto-recall-hook.d.ts +12 -0
- package/dist/internal/engine/bindings/sno-station-mem-auto-recall-hook.js +324 -0
- package/dist/internal/engine/bindings/sno-station-mem-hook-types.d.ts +56 -0
- package/dist/internal/engine/bindings/sno-station-mem-hook-types.js +1 -0
- package/dist/internal/engine/bindings/sno-station-mem-insight-distill-factory.d.ts +14 -0
- package/dist/internal/engine/bindings/sno-station-mem-insight-distill-factory.js +119 -0
- package/dist/internal/engine/bindings/sno-station-mem-message-transcript.d.ts +52 -0
- package/dist/internal/engine/bindings/sno-station-mem-message-transcript.js +145 -0
- package/dist/internal/engine/bindings/sno-station-mem-reset-hook.d.ts +10 -0
- package/dist/internal/engine/bindings/sno-station-mem-reset-hook.js +9 -0
- package/dist/internal/engine/bindings/sno-station-mem-runtime-mode.d.ts +37 -0
- package/dist/internal/engine/bindings/sno-station-mem-runtime-mode.js +105 -0
- package/dist/internal/engine/bindings/sno-station-mem-session-state.d.ts +10 -0
- package/dist/internal/engine/bindings/sno-station-mem-session-state.js +27 -0
- package/dist/internal/engine/eval/trace.d.ts +2 -0
- package/dist/internal/engine/eval/trace.js +151 -0
- package/dist/internal/engine/extraction/ambient-learning-text-normalizer.d.ts +2 -0
- package/dist/internal/engine/extraction/ambient-learning-text-normalizer.js +2 -0
- package/dist/internal/engine/extraction/atomic-memory-extraction.d.ts +2 -0
- package/dist/internal/engine/extraction/atomic-memory-extraction.js +2 -0
- package/dist/internal/engine/extraction/b-profile-section-dictionary-provider.d.ts +2 -0
- package/dist/internal/engine/extraction/b-profile-section-dictionary-provider.js +2 -0
- package/dist/internal/engine/extraction/b-profile-section-rekey.d.ts +49 -0
- package/dist/internal/engine/extraction/b-profile-section-rekey.js +730 -0
- package/dist/internal/engine/extraction/capture-policy-detector.d.ts +2 -0
- package/dist/internal/engine/extraction/capture-policy-detector.js +2 -0
- package/dist/internal/engine/extraction/embedding-provider-client.d.ts +2 -0
- package/dist/internal/engine/extraction/embedding-provider-client.js +2 -0
- package/dist/internal/engine/extraction/extraction-text-sanitizer.d.ts +2 -0
- package/dist/internal/engine/extraction/extraction-text-sanitizer.js +114 -0
- package/dist/internal/engine/extraction/memory-metadata-codec.d.ts +3 -0
- package/dist/internal/engine/extraction/memory-metadata-codec.js +2 -0
- package/dist/internal/engine/extraction/memory-temporality-classifier.d.ts +2 -0
- package/dist/internal/engine/extraction/memory-temporality-classifier.js +2 -0
- package/dist/internal/engine/i18n/all-resources.d.ts +2 -0
- package/dist/internal/engine/i18n/all-resources.js +2 -0
- package/dist/internal/engine/i18n/locales.d.ts +2 -0
- package/dist/internal/engine/i18n/locales.js +19 -0
- package/dist/internal/engine/i18n/res/_types.d.ts +2 -0
- package/dist/internal/engine/i18n/res/_types.js +11 -0
- package/dist/internal/engine/observability/adapter.d.ts +2 -0
- package/dist/internal/engine/observability/adapter.js +2 -0
- package/dist/internal/engine/observability/best-effort.d.ts +2 -0
- package/dist/internal/engine/observability/best-effort.js +41 -0
- package/dist/internal/engine/observability/early-diagnostics.js +2 -0
- package/dist/internal/engine/observability/memory-snapshot.d.ts +2 -0
- package/dist/internal/engine/observability/memory-snapshot.js +44 -0
- package/dist/internal/engine/observability/observable-embedder.d.ts +2 -0
- package/dist/internal/engine/observability/observable-embedder.js +43 -0
- package/dist/internal/engine/observability/observable-llm-client.d.ts +2 -0
- package/dist/internal/engine/observability/observable-llm-client.js +98 -0
- package/dist/internal/engine/observability/observable-memory-store.d.ts +2 -0
- package/dist/internal/engine/observability/observable-memory-store.js +701 -0
- package/dist/internal/engine/observability/observable-retriever.d.ts +2 -0
- package/dist/internal/engine/observability/observable-retriever.js +95 -0
- package/dist/internal/engine/observability/runtime-diagnostics.d.ts +13 -0
- package/dist/internal/engine/observability/runtime-diagnostics.js +96 -0
- package/dist/internal/engine/observability/session-registry.d.ts +2 -0
- package/dist/internal/engine/observability/session-registry.js +58 -0
- package/dist/internal/engine/observability/version-metadata.d.ts +7 -0
- package/dist/internal/engine/observability/version-metadata.js +59 -0
- package/dist/internal/engine/operations/daily-spend-estimator.d.ts +2 -0
- package/dist/internal/engine/operations/daily-spend-estimator.js +97 -0
- package/dist/internal/engine/operations/learning-file-maintenance.d.ts +2 -0
- package/dist/internal/engine/operations/learning-file-maintenance.js +255 -0
- package/dist/internal/engine/operations/memory-tier-promoter.d.ts +2 -0
- package/dist/internal/engine/operations/memory-tier-promoter.js +95 -0
- package/dist/internal/engine/operations/runtime-audit-log.d.ts +3 -0
- package/dist/internal/engine/operations/runtime-audit-log.js +3 -0
- package/dist/internal/engine/provider/canonical-memory-corpus.d.ts +38 -0
- package/dist/internal/engine/provider/canonical-memory-corpus.js +253 -0
- package/dist/internal/engine/provider/provider-registration.d.ts +39 -0
- package/dist/internal/engine/provider/provider-registration.js +2 -0
- package/dist/internal/engine/provider/provider-row-renderer.d.ts +7 -0
- package/dist/internal/engine/provider/provider-row-renderer.js +53 -0
- package/dist/internal/engine/provider/provider-types.d.ts +2 -0
- package/dist/internal/engine/provider/provider-types.js +1 -0
- package/dist/internal/engine/reflection/daily-log-generator.d.ts +2 -0
- package/dist/internal/engine/reflection/daily-log-generator.js +2 -0
- package/dist/internal/engine/reflection/reflection-command-hooks.d.ts +2 -0
- package/dist/internal/engine/reflection/reflection-command-hooks.js +2 -0
- package/dist/internal/engine/reflection/reflection-deps.d.ts +2 -0
- package/dist/internal/engine/reflection/reflection-deps.js +1 -0
- package/dist/internal/engine/reflection/reflection-embedded-generator.d.ts +2 -0
- package/dist/internal/engine/reflection/reflection-embedded-generator.js +2 -0
- package/dist/internal/engine/reflection/reflection-injection-hooks.d.ts +2 -0
- package/dist/internal/engine/reflection/reflection-injection-hooks.js +219 -0
- package/dist/internal/engine/reflection/reflection-lifecycle-hooks.d.ts +2 -0
- package/dist/internal/engine/reflection/reflection-lifecycle-hooks.js +63 -0
- package/dist/internal/engine/reflection/strategy-hook-runner.d.ts +4 -0
- package/dist/internal/engine/reflection/strategy-hook-runner.js +3 -0
- package/dist/internal/engine/rem/classifier.d.ts +2 -0
- package/dist/internal/engine/rem/classifier.js +180 -0
- package/dist/internal/engine/rem/index.d.ts +4 -0
- package/dist/internal/engine/rem/index.js +2551 -0
- package/dist/internal/engine/rem/types.d.ts +2 -0
- package/dist/internal/engine/rem/types.js +40 -0
- package/dist/internal/engine/retrieval/access-tracker.d.ts +2 -0
- package/dist/internal/engine/retrieval/access-tracker.js +314 -0
- package/dist/internal/engine/retrieval/intent-analyzer.d.ts +2 -0
- package/dist/internal/engine/retrieval/intent-analyzer.js +27 -0
- package/dist/internal/engine/retrieval/retrieval-gate.d.ts +2 -0
- package/dist/internal/engine/retrieval/retrieval-gate.js +47 -0
- package/dist/internal/engine/retrieval/retriever.d.ts +2 -0
- package/dist/internal/engine/retrieval/retriever.js +2 -0
- package/dist/internal/engine/security/memory-scope-policy.d.ts +2 -0
- package/dist/internal/engine/security/memory-scope-policy.js +2 -0
- package/dist/internal/engine/security/redact.d.ts +2 -0
- package/dist/internal/engine/security/redact.js +34 -0
- package/dist/internal/engine/security/scopes.d.ts +3 -0
- package/dist/internal/engine/security/scopes.js +3 -0
- package/dist/internal/engine/shared/errors.d.ts +2 -0
- package/dist/internal/engine/shared/errors.js +66 -0
- package/dist/internal/engine/shared/i18n-text.d.ts +2 -0
- package/dist/internal/engine/shared/i18n-text.js +58 -0
- package/dist/internal/engine/shared/iso-date-time.d.ts +16 -0
- package/dist/internal/engine/shared/iso-date-time.js +40 -0
- package/dist/internal/engine/shared/lru.d.ts +2 -0
- package/dist/internal/engine/shared/lru.js +30 -0
- package/dist/internal/engine/shared/memory-kind-policy.d.ts +2 -0
- package/dist/internal/engine/shared/memory-kind-policy.js +64 -0
- package/dist/internal/engine/shared/paths.d.ts +3 -0
- package/dist/internal/engine/shared/paths.js +2 -0
- package/dist/internal/engine/shared/types.d.ts +3 -0
- package/dist/internal/engine/shared/types.js +25 -0
- package/dist/internal/engine/shared/utils.d.ts +2 -0
- package/dist/internal/engine/shared/utils.js +175 -0
- package/dist/internal/engine/telemetry/memory-telemetry-api.d.ts +134 -0
- package/dist/internal/engine/telemetry/memory-telemetry-api.js +730 -0
- package/dist/internal/engine/telemetry/memory-telemetry-observability.d.ts +24 -0
- package/dist/internal/engine/telemetry/memory-telemetry-observability.js +133 -0
- package/dist/internal/engine/telemetry/memory-telemetry-outbox.d.ts +2 -0
- package/dist/internal/engine/telemetry/memory-telemetry-outbox.js +443 -0
- package/dist/internal/engine/telemetry/memory-telemetry-types.d.ts +2 -0
- package/dist/internal/engine/telemetry/memory-telemetry-types.js +21 -0
- package/dist/internal/engine/telemetry/observe-ledger.d.ts +30 -0
- package/dist/internal/engine/telemetry/observe-ledger.js +111 -0
- package/dist/internal/model/agent-llm-port.d.ts +2 -0
- package/dist/internal/model/agent-llm-port.js +1 -0
- package/dist/internal/model/llm-client.d.ts +3 -0
- package/dist/internal/model/llm-client.js +3 -0
- package/dist/internal/model/llm-failure.d.ts +26 -0
- package/dist/internal/model/llm-failure.js +63 -0
- package/dist/internal/model/llm-mode-routing.d.ts +22 -0
- package/dist/internal/model/llm-mode-routing.js +37 -0
- package/dist/internal/store/atomic-memory-cutover-sql.d.ts +13 -0
- package/dist/internal/store/atomic-memory-cutover-sql.js +237 -0
- package/dist/internal/store/connection.d.ts +2 -0
- package/dist/internal/store/connection.js +2 -0
- package/dist/internal/store/data-bootstrap.d.ts +2 -0
- package/dist/internal/store/data-bootstrap.js +2 -0
- package/dist/internal/store/data-paths.d.ts +2 -0
- package/dist/internal/store/data-paths.js +65 -0
- package/dist/internal/store/maintenance.d.ts +75 -0
- package/dist/internal/store/maintenance.js +1401 -0
- package/dist/internal/store/memory-kinds-cutover-migrator.d.ts +28 -0
- package/dist/internal/store/memory-kinds-cutover-migrator.js +415 -0
- package/dist/internal/store/migrations.d.ts +27 -0
- package/dist/internal/store/migrations.js +201 -0
- package/dist/internal/store/sqlite-runtime.d.ts +2 -0
- package/dist/internal/store/sqlite-runtime.js +109 -0
- package/dist/internal/store/store.d.ts +3 -0
- package/dist/internal/store/store.js +2 -0
- package/dist/learning-file-maintenance-9b-Ut6Kv.d.ts +30 -0
- package/dist/llm-client-B7Xoxqou.js +1378 -0
- package/dist/llm-client-DOTSGzhe.d.ts +51 -0
- package/dist/locales-JxGmkG6t.d.ts +7 -0
- package/dist/log-site-catalog.generated-B9WYdnAM.js +2321 -0
- package/dist/log-site-catalog.json +1 -0
- package/dist/lru-DyoaTyIs.d.ts +14 -0
- package/dist/memdump.d.ts +52 -0
- package/dist/memdump.js +163 -0
- package/dist/memory-entry-projector-DHo8jH10.js +1015 -0
- package/dist/memory-kind-policy-LKt0u3gy.d.ts +23 -0
- package/dist/memory-metadata-codec-C3oQd47V.d.ts +68 -0
- package/dist/memory-metadata-codec-DL1sbJiN.js +577 -0
- package/dist/memory-runtime-BZtREuwW.js +19459 -0
- package/dist/memory-scope-policy-1gg5QlWZ.d.ts +67 -0
- package/dist/memory-scope-policy-DICSCURA.js +283 -0
- package/dist/memory-snapshot-3e44ND2i.d.ts +16 -0
- package/dist/memory-source-order-BtrIxaIt.js +134 -0
- package/dist/memory-store-base-XRA4S9Xl.d.ts +1095 -0
- package/dist/memory-store-shared-DeIDkhLj.js +158 -0
- package/dist/memory-store-write-validation-B_hDG2zf.js +243 -0
- package/dist/memory-telemetry-incidents-BWA9kcZx.js +8 -0
- package/dist/memory-telemetry-metadata-CA7W8tK6.js +77 -0
- package/dist/memory-telemetry-outbox-ddTdImt7.d.ts +74 -0
- package/dist/memory-telemetry-receipts-MAJecvXQ.d.ts +34 -0
- package/dist/memory-telemetry-receipts-YsxbwOAf.js +91 -0
- package/dist/memory-telemetry-types-CufVmrCs.d.ts +14 -0
- package/dist/memory-temporality-classifier-CEdRTN5J.d.ts +43 -0
- package/dist/memory-temporality-classifier-CKiTAHXe.js +184 -0
- package/dist/memory-tier-promoter-DbTpahkP.d.ts +43 -0
- package/dist/memory-tool-dependencies-CLODycUw.js +19 -0
- package/dist/memory-tool-dependencies-N69chrQc.d.ts +14 -0
- package/dist/model-call-table-CspopkBg.js +563 -0
- package/dist/model-reply-text-CKopStP2.js +334 -0
- package/dist/observable-embedder-DQnNgf9O.d.ts +17 -0
- package/dist/observable-llm-client-uwqPADpN.d.ts +21 -0
- package/dist/observable-memory-store-BaZKbhgq.d.ts +73 -0
- package/dist/observable-retriever-Dt_NUxAA.d.ts +23 -0
- package/dist/operation-cancellation-CTKWwNHk.js +81 -0
- package/dist/paths-qVF-5uBq.d.ts +1 -0
- package/dist/plugin-config-schema-C0UUbOmG.d.ts +413 -0
- package/dist/plugin-config-schema-YJLyIIcJ.js +361 -0
- package/dist/profile-TWPJ_myr.d.ts +13 -0
- package/dist/profile-section-writer-hIphqpUY.js +2149 -0
- package/dist/provider-registration-CF1tg8Xy.js +721 -0
- package/dist/provider-runtime-types-DFhtdA6d.d.ts +134 -0
- package/dist/provider-types-CZGm6v88.d.ts +23 -0
- package/dist/redact-BTIZcHQL.d.ts +5 -0
- package/dist/reflection-command-hooks-BeQiJI9-.js +644 -0
- package/dist/reflection-command-hooks-C1Vxgb-O.d.ts +32 -0
- package/dist/reflection-deps-RpLPFrSc.d.ts +19 -0
- package/dist/reflection-embedded-generator-CDUhgV96.js +130 -0
- package/dist/reflection-embedded-generator-CxOex5xK.d.ts +14 -0
- package/dist/reflection-injection-hooks-DfUgqu8f.d.ts +43 -0
- package/dist/reflection-lifecycle-hooks-Ct5HBdSG.d.ts +27 -0
- package/dist/relation-dictionary-Cev1kyLe.js +4248 -0
- package/dist/release-download.d.ts +8 -0
- package/dist/release-download.js +32 -0
- package/dist/relevant-memories-context-m8nuFZcM.js +23 -0
- package/dist/rem-batch-executor-Cbm5gfSe.js +31644 -0
- package/dist/rem-batch-executor-bc0IFlp2.js +2 -0
- package/dist/rem-consumer-retrieval-CRpvG8KS.js +246 -0
- package/dist/rem-trigger-BTxkcjgT.js +729 -0
- package/dist/resolver-Dy-hnjCR.js +103 -0
- package/dist/results-C3ZLroMb.js +140 -0
- package/dist/retrieval-gate-Bx2Zg7jD.d.ts +31 -0
- package/dist/retriever-CygHBazG.js +1757 -0
- package/dist/retriever-Ruk3zp7_.d.ts +469 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/routes-Ce7SQKec.js +59 -0
- package/dist/runtime-audit-log-BCeujlEp.js +430 -0
- package/dist/runtime-audit-log-zpQiR9HT.d.ts +149 -0
- package/dist/scopes-CSOn6zdd.js +71 -0
- package/dist/scopes-CtrY-YQ3.d.ts +23 -0
- package/dist/server-BPAY4NTG.js +1410 -0
- package/dist/session-registry-DBoE7AvZ.d.ts +14 -0
- package/dist/sidecar/main.d.ts +1 -0
- package/dist/sidecar/main.js +56 -0
- package/dist/signed-registry-constants-Bv0fgZ8Q.d.ts +12 -0
- package/dist/signed-registry-constants-yq3DlG3B.js +82 -0
- package/dist/sno-station-mem-runtime-dependencies-Bu-MiqPX.d.ts +31 -0
- package/dist/sno-station-mem-runtime-dependencies-DjLOhB45.js +30 -0
- package/dist/sqlite-runtime-L_ywirTm.d.ts +58 -0
- package/dist/store-2pM0-dZg.d.ts +27 -0
- package/dist/store-DzpZBMEm.js +5830 -0
- package/dist/strategy-hook-runner-gBtU47tX.d.ts +16 -0
- package/dist/strategy-hook-runner-zmdIYOmQ.js +110 -0
- package/dist/token-counter-BD1f48Km.js +73 -0
- package/dist/trace-1t1n_wUm.d.ts +86 -0
- package/dist/types-DxKqU60b.d.ts +477 -0
- package/dist/types-pVomjzqR.d.ts +132 -0
- package/dist/unplaced-candidate-migration-CbQHzcEM.js +940 -0
- package/dist/utils-Ctb9aevJ.d.ts +34 -0
- package/drizzle/0000_initial.sql +10 -0
- package/drizzle/0001_virtual_tables.sql +34 -0
- package/drizzle/0002_add_indexes.sql +11 -0
- package/drizzle/0003_content_hash_lookup_index.sql +2 -0
- package/drizzle/0004_chunk_swap.sql +60 -0
- package/drizzle/0005_drop_legacy_vec_fts.sql +10 -0
- package/drizzle/0006_fts_simple.sql +45 -0
- package/drizzle/0007_restore_simple_fts.sql +40 -0
- package/drizzle/0008_widen_memory_hash_unique_index.sql +4 -0
- package/drizzle/0009_memory_telemetry.sql +109 -0
- package/drizzle/0010_provider_mappings.sql +27 -0
- package/drizzle/0011_db_optimization.sql +82 -0
- package/drizzle/0012_b_profile_lanes.sql +9 -0
- package/drizzle/0013_extraction_timestamps.sql +6 -0
- package/drizzle/0014_extraction_timestamp_source.sql +3 -0
- package/drizzle/0015_task_lifecycle_state.sql +101 -0
- package/drizzle/0016_task_lifecycle_legacy_migration.sql +144 -0
- package/drizzle/0017_unplaced_candidates.sql +15 -0
- package/drizzle/0018_rem_batch_chassis.sql +71 -0
- package/drizzle/0019_profile_recovery_entries.sql +17 -0
- package/drizzle/0020_rem_scan_retry_progress.down.sql +14 -0
- package/drizzle/0020_rem_scan_retry_progress.sql +24 -0
- package/drizzle/0021_rem_two_facet_storage.down.sql +14 -0
- package/drizzle/0021_rem_two_facet_storage.sql +42 -0
- package/drizzle/0022_rem_facet_chunk_versions.down.sql +4 -0
- package/drizzle/0022_rem_facet_chunk_versions.sql +4 -0
- package/drizzle/0023_rem_defect_ledger.sql +40 -0
- package/drizzle/0024_rem_unbuilt_foundations.down.sql +13 -0
- package/drizzle/0024_rem_unbuilt_foundations.sql +65 -0
- package/drizzle/0025_rem_journal_operation_constraint.sql +28 -0
- package/drizzle/0026_rem_journal_detail.sql +1 -0
- package/drizzle/0027_legacy_nodix_namespace.sql +3 -0
- package/drizzle/0028_memory_timezone.sql +23 -0
- package/drizzle/0029_rem_write_hash_symmetry.sql +17 -0
- package/drizzle/0030_atomic_memory_additive.sql +105 -0
- package/drizzle/0031_atomic_extraction_ledger.sql +23 -0
- package/drizzle/0032_atomic_memory_entities.sql +12 -0
- package/drizzle/0033_todo_own_store.sql +99 -0
- package/drizzle/meta/0000_snapshot.json +87 -0
- package/drizzle/meta/_journal.json +244 -0
- package/generated/rem-operations.json +1 -0
- package/package.json +126 -4
- package/scripts/download-simple-tokenizer.mjs +254 -0
- package/settings.default.json +243 -0
- package/skills/extract-atomic-memory/SKILL.md +298 -0
- package/skills/extract-atomic-memory/references/account-for-turns.md +9 -0
- package/skills/extract-atomic-memory/references/calendar-meaning.md +64 -0
- package/skills/extract-atomic-memory/references/capture.md +120 -0
- package/skills/extract-atomic-memory/references/enrichment.md +92 -0
- package/skills/extract-atomic-memory/references/missing-durable-half.md +6 -0
- package/skills/extract-atomic-memory/references/progress-classification.md +21 -0
- package/skills/extract-atomic-memory/references/resolve-subject.md +19 -0
- package/skills/extract-atomic-memory/references/surrounding-context.md +10 -0
- package/skills/extract-atomic-memory/references/user-subject-guard.md +29 -0
- package/skills/judge-group-closure/SKILL.md +25 -0
- package/skills/judge-rem-memory-updates/SKILL.md +56 -0
- package/skills/judge-rem-memory-updates/agents/openai.yaml +4 -0
- package/skills/judge-retired-position/SKILL.md +25 -0
- package/skills/judge-task-lifecycle/SKILL.md +45 -0
- package/skills/key-state-attribute/SKILL.md +16 -0
- package/skills/resolve-entity-identity/SKILL.md +24 -0
- package/skills/resolve-relative-time/SKILL.md +22 -0
- package/sqlite-extensions/LICENSE-simple-tokenizer +28 -0
- package/sqlite-extensions/LICENSE-sqlite-vec +21 -0
- package/sqlite-extensions/linux-arm64/vec0.so +0 -0
- package/sqlite-extensions/linux-x64/vec0.so +0 -0
|
@@ -0,0 +1,2551 @@
|
|
|
1
|
+
import { REM_ADDITIONAL_TRANSITION_PATTERN_SHA256, REM_AMBIGUOUS_PATTERN_SHA256, REM_CURRENT_PROGRESSIVE_PATTERN_SHA256, REM_HISTORICAL_FACT_PATTERN_SHA256, REM_PURE_NEGATION_PATTERN_SHA256, REM_TRANSITION_GRAMMAR_SHA256, classifyRemRow, runRestateClassifierEntry, runVerdictClassifierEntry } from "./classifier.js";
|
|
2
|
+
import { REM_BUILT_OPERATION_TYPES, REM_JOB_STATES, REM_OPERATION_TYPES, REM_ROW_STATES, assertJobIdentity, parseRemOperationType } from "./types.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { createLogger } from "@snoai/utils/logger";
|
|
5
|
+
import path, { dirname, extname, resolve } from "node:path";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
//#region src/engine/rem/adapter-a-conflict-adjudicator.ts
|
|
9
|
+
/** @file adapter-a-conflict-adjudicator.ts
|
|
10
|
+
* @purpose Renders and parses a fail-safe conflict verdict for an ordered memory pair.
|
|
11
|
+
* @boundary Accepts package-level memory views only; callers adapt their storage records.
|
|
12
|
+
*/
|
|
13
|
+
function adapterAViewFromRecord(record) {
|
|
14
|
+
if (record.kind !== "profile" && record.kind !== "episodic" && record.kind !== "state") throw new Error(`Adapter A does not accept ${record.kind} memories`);
|
|
15
|
+
return {
|
|
16
|
+
text: record.text,
|
|
17
|
+
...record.subject !== void 0 ? { subject: record.subject } : {},
|
|
18
|
+
kind: record.kind,
|
|
19
|
+
validFrom: isoUtcSeconds(record.validFrom),
|
|
20
|
+
assertedAt: isoUtcSeconds(record.assertedAt),
|
|
21
|
+
contentHash: record.contentHash
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function prospectiveProfileView(text, at) {
|
|
25
|
+
return {
|
|
26
|
+
text: text.trim(),
|
|
27
|
+
kind: "profile",
|
|
28
|
+
validFrom: isoUtcSeconds(at),
|
|
29
|
+
assertedAt: isoUtcSeconds(at),
|
|
30
|
+
contentHash: createHash("sha256").update(text.trim()).digest("hex")
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function orderAdapterAPair(existing, candidate) {
|
|
34
|
+
if (compareViews(existing, candidate) <= 0) return {
|
|
35
|
+
older: existing,
|
|
36
|
+
newer: candidate,
|
|
37
|
+
candidateRole: "newer"
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
older: candidate,
|
|
41
|
+
newer: existing,
|
|
42
|
+
candidateRole: "older"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function renderAdapterAPrompt(older, newer) {
|
|
46
|
+
return [
|
|
47
|
+
"Judge the relationship of the newer memory to the older memory.",
|
|
48
|
+
"",
|
|
49
|
+
"Older:",
|
|
50
|
+
`kind: ${older.kind}`,
|
|
51
|
+
`validFrom: ${older.validFrom}`,
|
|
52
|
+
`assertedAt: ${older.assertedAt}`,
|
|
53
|
+
`text: ${older.text}`,
|
|
54
|
+
"",
|
|
55
|
+
"Newer:",
|
|
56
|
+
`kind: ${newer.kind}`,
|
|
57
|
+
`validFrom: ${newer.validFrom}`,
|
|
58
|
+
`assertedAt: ${newer.assertedAt}`,
|
|
59
|
+
`text: ${newer.text}`,
|
|
60
|
+
"",
|
|
61
|
+
"Verdict:"
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
function renderAdapterAChatPrompt(pair) {
|
|
65
|
+
return [
|
|
66
|
+
"Adjudicate whether the newer memory replaces the older memory.",
|
|
67
|
+
"Return either one JSON object, {\"verdict\":\"replacement|keep|uncertain\"}, or exactly one verdict token.",
|
|
68
|
+
"replacement: the newer memory directly changes or corrects the same fact, state, or preference, so the older memory is no longer true.",
|
|
69
|
+
"keep: the older memory remains valid, is historical, concerns a different fact, coexists with the newer memory, or is only refined without being erased.",
|
|
70
|
+
"uncertain: the relationship is unclear or the evidence is insufficient. Prefer uncertain over guessing.",
|
|
71
|
+
"Apply the same semantics regardless of memory kind.",
|
|
72
|
+
`Older memory:\n${JSON.stringify(pair.older)}`,
|
|
73
|
+
`Newer memory:\n${JSON.stringify(pair.newer)}`
|
|
74
|
+
].join("\n\n");
|
|
75
|
+
}
|
|
76
|
+
function parseAdapterAChatVerdict(raw) {
|
|
77
|
+
const trimmed = raw.trim();
|
|
78
|
+
if (trimmed === "replacement" || trimmed === "keep" || trimmed === "uncertain") return trimmed;
|
|
79
|
+
const json = extractJsonFromResponse(trimmed);
|
|
80
|
+
if (!json) return "uncertain";
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(json);
|
|
83
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return "uncertain";
|
|
84
|
+
const verdict = parsed["verdict"];
|
|
85
|
+
return verdict === "replacement" || verdict === "keep" || verdict === "uncertain" ? verdict : "uncertain";
|
|
86
|
+
} catch {
|
|
87
|
+
return "uncertain";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether a reply is a verdict at all: a bare verdict token, or a JSON object carrying one, read
|
|
92
|
+
* the way `parseAdapterAChatVerdict` reads it (a fenced object counts).
|
|
93
|
+
*/
|
|
94
|
+
function isAdapterAVerdictReply(value) {
|
|
95
|
+
const trimmed = value.trim();
|
|
96
|
+
if (trimmed === "replacement" || trimmed === "keep" || trimmed === "uncertain") return true;
|
|
97
|
+
const json = extractJsonFromResponse(trimmed);
|
|
98
|
+
if (json === null) return false;
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(json);
|
|
101
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return false;
|
|
102
|
+
const verdict = parsed["verdict"];
|
|
103
|
+
return verdict === "replacement" || verdict === "keep" || verdict === "uncertain";
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function compareViews(a, b) {
|
|
109
|
+
const validFrom = Date.parse(a.validFrom) - Date.parse(b.validFrom);
|
|
110
|
+
if (validFrom !== 0) return validFrom;
|
|
111
|
+
return Date.parse(a.assertedAt) - Date.parse(b.assertedAt);
|
|
112
|
+
}
|
|
113
|
+
function isoUtcSeconds(value) {
|
|
114
|
+
if (!Number.isFinite(value)) throw new Error("Adapter A timestamp must be finite");
|
|
115
|
+
return new Date(value).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
116
|
+
}
|
|
117
|
+
function extractJsonFromResponse(text) {
|
|
118
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
119
|
+
if (fenceMatch) return (fenceMatch[1] ?? "").trim();
|
|
120
|
+
const firstBrace = text.indexOf("{");
|
|
121
|
+
if (firstBrace === -1) return null;
|
|
122
|
+
let depth = 0;
|
|
123
|
+
let lastBrace = -1;
|
|
124
|
+
let inString = false;
|
|
125
|
+
let escaped = false;
|
|
126
|
+
for (let i = firstBrace; i < text.length; i++) {
|
|
127
|
+
const ch = text[i];
|
|
128
|
+
if (ch === void 0) continue;
|
|
129
|
+
if (escaped) {
|
|
130
|
+
escaped = false;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === "\\") {
|
|
134
|
+
escaped = inString;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ch === "\"") {
|
|
138
|
+
inString = !inString;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (inString) continue;
|
|
142
|
+
if (ch === "{") depth++;
|
|
143
|
+
else if (ch === "}") {
|
|
144
|
+
depth--;
|
|
145
|
+
if (depth === 0) {
|
|
146
|
+
lastBrace = i;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (lastBrace === -1) return null;
|
|
152
|
+
return text.substring(firstBrace, lastBrace + 1);
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/engine/rem/operational-config.ts
|
|
156
|
+
const sha256Schema = z.string().regex(/^[0-9a-f]{64}$/u);
|
|
157
|
+
const positiveSafeInteger = z.number().int().safe().positive("value must be positive and non-zero");
|
|
158
|
+
const nullablePercentage = z.number().min(0).max(1).nullable();
|
|
159
|
+
const remOperationalConfigurationSchema = z.object({
|
|
160
|
+
profileId: z.string().min(1),
|
|
161
|
+
operations: z.object({
|
|
162
|
+
"rem-update": z.boolean(),
|
|
163
|
+
"rem-replace": z.boolean(),
|
|
164
|
+
"rem-distill": z.boolean(),
|
|
165
|
+
"rem-retire": z.boolean()
|
|
166
|
+
}).strict(),
|
|
167
|
+
budgets: z.object({ maxPairs: positiveSafeInteger }).strict(),
|
|
168
|
+
retrieval: z.object({
|
|
169
|
+
neighborLimit: positiveSafeInteger,
|
|
170
|
+
similarityThreshold: z.number().min(0).max(1)
|
|
171
|
+
}).strict(),
|
|
172
|
+
coverage: z.object({ accuracyFloor: nullablePercentage }).strict(),
|
|
173
|
+
retries: z.object({ liveContentionRetries: z.number().int().safe().nonnegative() }).strict(),
|
|
174
|
+
facetPolicy: z.object({
|
|
175
|
+
aggregationGrammar: z.string().min(1),
|
|
176
|
+
historyGrammar: z.string().min(1)
|
|
177
|
+
}).strict(),
|
|
178
|
+
calibration: z.object({
|
|
179
|
+
minimumPublishableScoreEffect: z.number().nonnegative().nullable(),
|
|
180
|
+
minimumTargetCount: positiveSafeInteger.nullable(),
|
|
181
|
+
minimumTargetPercent: nullablePercentage
|
|
182
|
+
}).strict(),
|
|
183
|
+
enableGateDigests: z.object({
|
|
184
|
+
"p5-production-config": sha256Schema,
|
|
185
|
+
"p6-monthly-non-regression": sha256Schema,
|
|
186
|
+
"p7-detector-gate-verdict": sha256Schema,
|
|
187
|
+
"population-routing": sha256Schema,
|
|
188
|
+
"rem-update": sha256Schema,
|
|
189
|
+
"rem-replace": sha256Schema,
|
|
190
|
+
"p5-immutable-profile": sha256Schema.optional()
|
|
191
|
+
}).strict()
|
|
192
|
+
}).strict();
|
|
193
|
+
function parseRemOperationalConfiguration(input) {
|
|
194
|
+
return remOperationalConfigurationSchema.parse(input);
|
|
195
|
+
}
|
|
196
|
+
function canonicalizeRemConfiguration(input) {
|
|
197
|
+
const { enableGateDigests: _excluded, ...operational } = parseRemOperationalConfiguration(input);
|
|
198
|
+
return canonicalJson(operational);
|
|
199
|
+
}
|
|
200
|
+
function deriveRemConfigurationSha256(input) {
|
|
201
|
+
return createHash("sha256").update(canonicalizeRemConfiguration(input), "utf8").digest("hex");
|
|
202
|
+
}
|
|
203
|
+
function validateRemEnableGateDigestKeys(input) {
|
|
204
|
+
if (parseRemOperationalConfiguration(input.configuration).enableGateDigests["p5-immutable-profile"] !== void 0 !== (input.p5ReadbackSource === "immutable-profile")) return {
|
|
205
|
+
decision: "refuse",
|
|
206
|
+
reasonCode: "enableGateDigests.p5-immutable-profile"
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
decision: "allow",
|
|
210
|
+
reasonCode: null
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function requireRemCoverageAccuracyFloor(input) {
|
|
214
|
+
return parseRemOperationalConfiguration(input.configuration).coverage.accuracyFloor === null ? {
|
|
215
|
+
decision: "refuse",
|
|
216
|
+
reasonCode: "coverage.accuracyFloor"
|
|
217
|
+
} : {
|
|
218
|
+
decision: "allow",
|
|
219
|
+
reasonCode: null
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function requireRemOwnerDecisions(input) {
|
|
223
|
+
const configuration = parseRemOperationalConfiguration(input.configuration);
|
|
224
|
+
if (configuration.coverage.accuracyFloor === null) return {
|
|
225
|
+
decision: "refuse",
|
|
226
|
+
reasonCode: "coverage.accuracyFloor"
|
|
227
|
+
};
|
|
228
|
+
for (const key of [
|
|
229
|
+
"minimumPublishableScoreEffect",
|
|
230
|
+
"minimumTargetCount",
|
|
231
|
+
"minimumTargetPercent"
|
|
232
|
+
]) if (configuration.calibration[key] === null) return {
|
|
233
|
+
decision: "refuse",
|
|
234
|
+
reasonCode: `calibration.${key}`
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
decision: "allow",
|
|
238
|
+
reasonCode: null
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function declareRemCalibrationThreshold(input) {
|
|
242
|
+
const configuration = parseRemOperationalConfiguration(input.configuration);
|
|
243
|
+
for (const key of [
|
|
244
|
+
"minimumPublishableScoreEffect",
|
|
245
|
+
"minimumTargetCount",
|
|
246
|
+
"minimumTargetPercent"
|
|
247
|
+
]) if (configuration.calibration[key] === null) return {
|
|
248
|
+
decision: "refuse",
|
|
249
|
+
reasonCode: `calibration.${key}`
|
|
250
|
+
};
|
|
251
|
+
if (input.derivationStatement?.trim().length === 0 || input.derivationStatement === void 0) return {
|
|
252
|
+
decision: "refuse",
|
|
253
|
+
reasonCode: "calibration.derivationStatement"
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
decision: "allow",
|
|
257
|
+
reasonCode: null
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function validateRemGrammarChange(input) {
|
|
261
|
+
const before = parseRemOperationalConfiguration(input.before);
|
|
262
|
+
const after = parseRemOperationalConfiguration(input.after);
|
|
263
|
+
if (deriveRemConfigurationSha256(before) !== input.beforeConfigurationSha256 || deriveRemConfigurationSha256(after) !== input.afterConfigurationSha256 || !sha256Schema.safeParse(input.beforeCorpusSha256).success) return {
|
|
264
|
+
decision: "refuse",
|
|
265
|
+
reasonCode: "configuration_digest_mismatch"
|
|
266
|
+
};
|
|
267
|
+
if (!(before.facetPolicy.aggregationGrammar !== after.facetPolicy.aggregationGrammar || before.facetPolicy.historyGrammar !== after.facetPolicy.historyGrammar)) return {
|
|
268
|
+
decision: "allow",
|
|
269
|
+
reasonCode: null
|
|
270
|
+
};
|
|
271
|
+
const artifact = input.abArtifact;
|
|
272
|
+
if (artifact === void 0 || artifact.beforeConfigurationSha256 !== input.beforeConfigurationSha256 || artifact.afterConfigurationSha256 !== input.afterConfigurationSha256 || artifact.corpusSha256 !== input.beforeCorpusSha256 || artifact.metricDefinitions.length === 0 || artifact.result !== "pass") return {
|
|
273
|
+
decision: "refuse",
|
|
274
|
+
reasonCode: "grammar_ab_missing_or_failed"
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
decision: "allow",
|
|
278
|
+
reasonCode: null
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function canonicalJson(value) {
|
|
282
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") return JSON.stringify(value);
|
|
283
|
+
if (typeof value === "number") {
|
|
284
|
+
if (!Number.isFinite(value)) throw new Error("canonical JSON does not accept non-finite numbers");
|
|
285
|
+
return JSON.stringify(value);
|
|
286
|
+
}
|
|
287
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
288
|
+
if (typeof value !== "object") throw new Error("canonical JSON accepts JSON values only");
|
|
289
|
+
const record = value;
|
|
290
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/engine/rem/census.ts
|
|
294
|
+
function sha256(value) {
|
|
295
|
+
return createHash("sha256").update(value).digest("hex");
|
|
296
|
+
}
|
|
297
|
+
function validateRemProspectiveIdentity(input) {
|
|
298
|
+
if (input.writeIdentitySha256.includes(":")) return {
|
|
299
|
+
decision: "refuse",
|
|
300
|
+
reasonCode: "scan_generation_identity"
|
|
301
|
+
};
|
|
302
|
+
if (!isSha256(input.writeIdentitySha256) || !isSha256(input.candidateSetSha256)) return {
|
|
303
|
+
decision: "refuse",
|
|
304
|
+
reasonCode: "identity_invalid"
|
|
305
|
+
};
|
|
306
|
+
return {
|
|
307
|
+
decision: "allow",
|
|
308
|
+
reasonCode: null
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function deriveRemWriteIdentity(input) {
|
|
312
|
+
return sha256(JSON.stringify([
|
|
313
|
+
input.rowId,
|
|
314
|
+
input.text,
|
|
315
|
+
input.contentHash,
|
|
316
|
+
input.timestamp
|
|
317
|
+
]));
|
|
318
|
+
}
|
|
319
|
+
function deriveRemReplayArtifactIdentity(input) {
|
|
320
|
+
const corpusSha256 = sha256(readFileSync(input.corpusPath));
|
|
321
|
+
const embedderSha256 = sha256(readFileSync(input.embedderPath));
|
|
322
|
+
const toolSha256 = sha256(readFileSync(input.toolPath));
|
|
323
|
+
const configurationSha256 = sha256(canonicalizeRemConfiguration(input.configuration));
|
|
324
|
+
return {
|
|
325
|
+
corpusSha256,
|
|
326
|
+
embedderSha256,
|
|
327
|
+
toolSha256,
|
|
328
|
+
configurationSha256,
|
|
329
|
+
replayIdentitySha256: sha256(JSON.stringify([
|
|
330
|
+
corpusSha256,
|
|
331
|
+
embedderSha256,
|
|
332
|
+
toolSha256,
|
|
333
|
+
configurationSha256
|
|
334
|
+
]))
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function validateRemReplayIdentityTuple(input) {
|
|
338
|
+
return Object.values(input).every(isSha256) ? {
|
|
339
|
+
decision: "allow",
|
|
340
|
+
reasonCode: null
|
|
341
|
+
} : {
|
|
342
|
+
decision: "refuse",
|
|
343
|
+
reasonCode: "replay_identity_invalid"
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function classifyRemCensusLabel(input) {
|
|
347
|
+
const observations = input.observations;
|
|
348
|
+
if (input.label === "reachability") {
|
|
349
|
+
if (observations["similarityMet"] !== true) return "unknown";
|
|
350
|
+
return observations["neighborEmitted"] === true ? "reachable" : "unreachable";
|
|
351
|
+
}
|
|
352
|
+
if (input.label === "contradiction") {
|
|
353
|
+
if (observations["sameFactKey"] !== true) return "undetermined";
|
|
354
|
+
return observations["valuesConflict"] === true ? "yes" : "no";
|
|
355
|
+
}
|
|
356
|
+
if (input.label === "coverageEligibility") {
|
|
357
|
+
if (observations["retrievabilityComputed"] !== true) return "undetermined";
|
|
358
|
+
return observations["referenceSetNonEmpty"] === true && observations["retiringAtomsMatched"] === true ? "eligible" : "ineligible";
|
|
359
|
+
}
|
|
360
|
+
throw new Error(`unknown REM census label: ${input.label}`);
|
|
361
|
+
}
|
|
362
|
+
function classifyAllRemCensusLabels(input) {
|
|
363
|
+
return {
|
|
364
|
+
reachability: classifyRemCensusLabel({
|
|
365
|
+
label: "reachability",
|
|
366
|
+
observations: input.reachability
|
|
367
|
+
}),
|
|
368
|
+
contradiction: classifyRemCensusLabel({
|
|
369
|
+
label: "contradiction",
|
|
370
|
+
observations: input.contradiction
|
|
371
|
+
}),
|
|
372
|
+
coverageEligibility: classifyRemCensusLabel({
|
|
373
|
+
label: "coverageEligibility",
|
|
374
|
+
observations: input.coverageEligibility
|
|
375
|
+
})
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function validateRemCensusOracleProvenance(input) {
|
|
379
|
+
return input.authoredBy === "product-classifier" ? {
|
|
380
|
+
decision: "refuse",
|
|
381
|
+
reasonCode: "self_authored_oracle"
|
|
382
|
+
} : {
|
|
383
|
+
decision: "allow",
|
|
384
|
+
reasonCode: null
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function validateRemCensusLabelTuple(input) {
|
|
388
|
+
return [
|
|
389
|
+
"reachable",
|
|
390
|
+
"unreachable",
|
|
391
|
+
"unknown"
|
|
392
|
+
].includes(input.reachability) && [
|
|
393
|
+
"yes",
|
|
394
|
+
"no",
|
|
395
|
+
"undetermined"
|
|
396
|
+
].includes(input.contradiction) && [
|
|
397
|
+
"eligible",
|
|
398
|
+
"ineligible",
|
|
399
|
+
"undetermined"
|
|
400
|
+
].includes(input.coverageEligibility) ? {
|
|
401
|
+
decision: "allow",
|
|
402
|
+
reasonCode: null
|
|
403
|
+
} : {
|
|
404
|
+
decision: "refuse",
|
|
405
|
+
reasonCode: "label_enum_mismatch"
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function readRetainedRemCensusArtifact(input) {
|
|
409
|
+
if (!existsSync(input.artifactPath)) return {
|
|
410
|
+
decision: "refuse",
|
|
411
|
+
reasonCode: "artifact_missing"
|
|
412
|
+
};
|
|
413
|
+
const bytes = readFileSync(input.artifactPath);
|
|
414
|
+
try {
|
|
415
|
+
JSON.parse(bytes.toString("utf8"));
|
|
416
|
+
} catch {
|
|
417
|
+
return {
|
|
418
|
+
decision: "refuse",
|
|
419
|
+
reasonCode: "artifact_invalid"
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
decision: "allow",
|
|
424
|
+
reasonCode: null,
|
|
425
|
+
artifactSha256: sha256(bytes)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function produceRemCensusArtifact(input) {
|
|
429
|
+
if (input.producer !== "capture" && input.producer !== "replay") throw new Error("REM census producer must be capture or replay");
|
|
430
|
+
writeFileSync(input.outputPath, `${JSON.stringify({ route: input.producer })}\n`);
|
|
431
|
+
}
|
|
432
|
+
function validateRemCensusRouteCoverage(input) {
|
|
433
|
+
const routes = new Set(input.map(({ route }) => route));
|
|
434
|
+
return routes.has("capture") && routes.has("replay") ? {
|
|
435
|
+
decision: "allow",
|
|
436
|
+
reasonCode: null
|
|
437
|
+
} : {
|
|
438
|
+
decision: "refuse",
|
|
439
|
+
reasonCode: "route_coverage_incomplete"
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function journalRemGenerationTransition(input) {
|
|
443
|
+
if (input.outgoingGenerationId === input.incomingGenerationId) return {
|
|
444
|
+
decision: "refuse",
|
|
445
|
+
reasonCode: "generation_identity_reused"
|
|
446
|
+
};
|
|
447
|
+
input.database.prepare(`INSERT INTO nodix_rem_generation_transitions(
|
|
448
|
+
outgoing_generation_id, incoming_generation_id, recorded_at
|
|
449
|
+
) VALUES (?, ?, ?)`).run(input.outgoingGenerationId, input.incomingGenerationId, (/* @__PURE__ */ new Date()).toISOString());
|
|
450
|
+
return {
|
|
451
|
+
decision: "allow",
|
|
452
|
+
reasonCode: null
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function summarizeRemBatchEvents(input) {
|
|
456
|
+
const distribution = {};
|
|
457
|
+
let calls = 0;
|
|
458
|
+
let tokens = 0;
|
|
459
|
+
let closeCount = 0;
|
|
460
|
+
let refusalCount = 0;
|
|
461
|
+
for (const event of input.events) {
|
|
462
|
+
calls += event.calls ?? 0;
|
|
463
|
+
tokens += event.tokens ?? 0;
|
|
464
|
+
closeCount += event.close ?? 0;
|
|
465
|
+
if (event.refusal !== void 0 && event.refusal !== null) refusalCount += 1;
|
|
466
|
+
if (event.verdict !== void 0) distribution[event.verdict] = (distribution[event.verdict] ?? 0) + 1;
|
|
467
|
+
}
|
|
468
|
+
input.database.prepare(`INSERT INTO nodix_rem_batch_summaries(
|
|
469
|
+
batch_id, generation_id, calls, tokens, pairCount, closeCount,
|
|
470
|
+
refusalCount, verdict_distribution_json
|
|
471
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(input.batchId, input.generationId ?? null, calls, tokens, input.events.length, closeCount, refusalCount, JSON.stringify(distribution));
|
|
472
|
+
}
|
|
473
|
+
function recordRemObservedVerdict(input) {
|
|
474
|
+
const isVerdict = (/* @__PURE__ */ new Set([
|
|
475
|
+
"replacement",
|
|
476
|
+
"keep",
|
|
477
|
+
"merge",
|
|
478
|
+
"unrelated"
|
|
479
|
+
])).has(input.observed);
|
|
480
|
+
input.database.prepare(`INSERT INTO nodix_rem_verdict_observations(
|
|
481
|
+
pair_id, audit_kind, persisted_verdict, raw_value
|
|
482
|
+
) VALUES (?, ?, ?, ?)`).run(input.pairId, isVerdict ? "VERDICT" : "SAMPLE", isVerdict ? input.observed : null, input.observed);
|
|
483
|
+
}
|
|
484
|
+
function recordRemCandidateCapCounts(input) {
|
|
485
|
+
input.database.prepare(`INSERT INTO nodix_rem_batch_summaries(
|
|
486
|
+
batch_id, generation_id, pre_truncation_count, emitted_count
|
|
487
|
+
) VALUES (?, ?, ?, ?)`).run(`candidate-cap:${input.generationId}`, input.generationId, input.total, Math.min(input.total, input.cap));
|
|
488
|
+
}
|
|
489
|
+
function reportRemSimilarityCalibration(input) {
|
|
490
|
+
const bytes = readFileSync(input.retainedArtifactPath);
|
|
491
|
+
JSON.parse(bytes.toString("utf8"));
|
|
492
|
+
const emitted = new Set(input.emittedPairIds);
|
|
493
|
+
const targets = new Set(input.targetPairIds);
|
|
494
|
+
const emittedTargetCount = [...targets].filter((pairId) => emitted.has(pairId)).length;
|
|
495
|
+
return {
|
|
496
|
+
emittedFraction: targets.size === 0 ? 0 : emittedTargetCount / targets.size,
|
|
497
|
+
targetRanks: input.targetPairIds.map((pairId) => input.rankedPairIds.indexOf(pairId) + 1),
|
|
498
|
+
adjudicationReachability: input.targetPairIds.map((pairId) => emitted.has(pairId)),
|
|
499
|
+
modelCallBudget: input.modelCallBudget,
|
|
500
|
+
consumedArtifactSha256: sha256(bytes)
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function validateRemCalibrationCrossCheck(input) {
|
|
504
|
+
return input.independent === null || input.independent === void 0 ? {
|
|
505
|
+
decision: "refuse",
|
|
506
|
+
reasonCode: "independent_count_missing"
|
|
507
|
+
} : {
|
|
508
|
+
decision: "allow",
|
|
509
|
+
reasonCode: null
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function isSha256(value) {
|
|
513
|
+
return /^[0-9a-f]{64}$/u.test(value);
|
|
514
|
+
}
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/engine/rem/clause-splitter.ts
|
|
517
|
+
/**
|
|
518
|
+
* Produces the fixed, lossless clause list used by REM judgments. The text itself
|
|
519
|
+
* remains the authority: clause values are exact source slices and separators are retained.
|
|
520
|
+
*/
|
|
521
|
+
function splitExactClauses(text) {
|
|
522
|
+
if (text.trim().length === 0) throw new Error("REM clause splitter requires non-empty text");
|
|
523
|
+
const clauses = [];
|
|
524
|
+
let cursor = 0;
|
|
525
|
+
for (const boundary of text.matchAll(/\n+|(?<=[.!?;])\s+|(?<=,)\s+(?=\p{Ll})/gu)) {
|
|
526
|
+
const end = boundary.index;
|
|
527
|
+
clauses.push({
|
|
528
|
+
value: text.slice(cursor, end),
|
|
529
|
+
start: cursor,
|
|
530
|
+
end,
|
|
531
|
+
separatorAfter: boundary[0]
|
|
532
|
+
});
|
|
533
|
+
cursor = end + boundary[0].length;
|
|
534
|
+
}
|
|
535
|
+
clauses.push({
|
|
536
|
+
value: text.slice(cursor),
|
|
537
|
+
start: cursor,
|
|
538
|
+
end: text.length,
|
|
539
|
+
separatorAfter: ""
|
|
540
|
+
});
|
|
541
|
+
if (clauses.some((clause) => clause.value.trim().length === 0)) throw new Error("REM clause splitter cannot delimit an empty clause");
|
|
542
|
+
if (clauses.map((clause) => `${clause.value}${clause.separatorAfter}`).join("") !== text) throw new Error("REM clause splitter must preserve source bytes");
|
|
543
|
+
return clauses;
|
|
544
|
+
}
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/engine/rem/update-locale-resources.ts
|
|
547
|
+
const REM_UPDATE_LOCALES = [
|
|
548
|
+
"en",
|
|
549
|
+
"de",
|
|
550
|
+
"es",
|
|
551
|
+
"fr",
|
|
552
|
+
"zh",
|
|
553
|
+
"zh-Hant",
|
|
554
|
+
"ja",
|
|
555
|
+
"ko",
|
|
556
|
+
"ru"
|
|
557
|
+
];
|
|
558
|
+
const REM_UPDATE_LOCALE_RESOURCES = {
|
|
559
|
+
en: {
|
|
560
|
+
valuePrefix: "Current preference: ",
|
|
561
|
+
listPrefix: "Current todos: ",
|
|
562
|
+
listSeparator: ", ",
|
|
563
|
+
spanSeparator: " ",
|
|
564
|
+
anchorMode: "capitalized-sequence"
|
|
565
|
+
},
|
|
566
|
+
de: {
|
|
567
|
+
valuePrefix: "Aktuelle Präferenz: ",
|
|
568
|
+
listPrefix: "Aktuelle Aufgaben: ",
|
|
569
|
+
listSeparator: ", ",
|
|
570
|
+
spanSeparator: " ",
|
|
571
|
+
anchorMode: "capitalized-sequence"
|
|
572
|
+
},
|
|
573
|
+
es: {
|
|
574
|
+
valuePrefix: "Preferencia actual: ",
|
|
575
|
+
listPrefix: "Tareas actuales: ",
|
|
576
|
+
listSeparator: ", ",
|
|
577
|
+
spanSeparator: " ",
|
|
578
|
+
anchorMode: "capitalized-sequence"
|
|
579
|
+
},
|
|
580
|
+
fr: {
|
|
581
|
+
valuePrefix: "Préférence actuelle : ",
|
|
582
|
+
listPrefix: "Tâches actuelles : ",
|
|
583
|
+
listSeparator: ", ",
|
|
584
|
+
spanSeparator: " ",
|
|
585
|
+
anchorMode: "capitalized-sequence"
|
|
586
|
+
},
|
|
587
|
+
zh: {
|
|
588
|
+
valuePrefix: "当前偏好:",
|
|
589
|
+
listPrefix: "当前任务:",
|
|
590
|
+
listSeparator: "、",
|
|
591
|
+
spanSeparator: "",
|
|
592
|
+
anchorMode: "explicit-script"
|
|
593
|
+
},
|
|
594
|
+
"zh-Hant": {
|
|
595
|
+
valuePrefix: "目前偏好:",
|
|
596
|
+
listPrefix: "目前任務:",
|
|
597
|
+
listSeparator: "、",
|
|
598
|
+
spanSeparator: "",
|
|
599
|
+
anchorMode: "explicit-script"
|
|
600
|
+
},
|
|
601
|
+
ja: {
|
|
602
|
+
valuePrefix: "現在の好み:",
|
|
603
|
+
listPrefix: "現在のタスク:",
|
|
604
|
+
listSeparator: "、",
|
|
605
|
+
spanSeparator: "",
|
|
606
|
+
anchorMode: "explicit-script"
|
|
607
|
+
},
|
|
608
|
+
ko: {
|
|
609
|
+
valuePrefix: "현재 선호: ",
|
|
610
|
+
listPrefix: "현재 작업: ",
|
|
611
|
+
listSeparator: ", ",
|
|
612
|
+
spanSeparator: " ",
|
|
613
|
+
anchorMode: "explicit-script"
|
|
614
|
+
},
|
|
615
|
+
ru: {
|
|
616
|
+
valuePrefix: "Текущее предпочтение: ",
|
|
617
|
+
listPrefix: "Текущие задачи: ",
|
|
618
|
+
listSeparator: ", ",
|
|
619
|
+
spanSeparator: " ",
|
|
620
|
+
anchorMode: "capitalized-sequence"
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
function getRemUpdateLocaleResource(locale) {
|
|
624
|
+
return REM_UPDATE_LOCALE_RESOURCES[locale];
|
|
625
|
+
}
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/engine/rem/update-rewrite.ts
|
|
628
|
+
function composeRemNegatedCurrent(input) {
|
|
629
|
+
const generatedRowId = `rem-negated-${createHash("sha256").update(`${input.topic}\u0000${input.retractionText}`).digest("hex").slice(0, 24)}`;
|
|
630
|
+
return {
|
|
631
|
+
shape: "negated-current",
|
|
632
|
+
topic: input.topic,
|
|
633
|
+
assertions: [{
|
|
634
|
+
polarity: "negative",
|
|
635
|
+
provenance: input.retractionText
|
|
636
|
+
}],
|
|
637
|
+
generatedRowId,
|
|
638
|
+
consumedRowIds: [input.priorRowId]
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function enumerateRemUpdateMembers(source) {
|
|
642
|
+
return extractListMemberSpans(source).map((span) => source.slice(span.start, span.end));
|
|
643
|
+
}
|
|
644
|
+
function buildRemUpdateReferenceSet(input) {
|
|
645
|
+
if (input.tier === "list") return buildMemberUnits(input.source, input.enumeratedMembers ?? []);
|
|
646
|
+
const clauses = splitExactClauses(input.source).map((clause, index) => toCoverageReference(input.source, `clause:${index}`, "clause", clause));
|
|
647
|
+
const anchors = extractAnchors(input.source, input.locale).map((anchor, index) => toCoverageReference(input.source, `anchor:${index}`, "anchor", anchor));
|
|
648
|
+
return [...clauses, ...anchors];
|
|
649
|
+
}
|
|
650
|
+
function buildMemberUnits(source, members) {
|
|
651
|
+
return members.map((value, index) => {
|
|
652
|
+
const start = source.indexOf(value);
|
|
653
|
+
return toCoverageReference(source, `member:${index}`, "structured-member", {
|
|
654
|
+
value,
|
|
655
|
+
start,
|
|
656
|
+
end: start < 0 ? -1 : start + value.length
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
function toCoverageReference(source, id, kind, span) {
|
|
661
|
+
const text = source.slice(span.start, span.end);
|
|
662
|
+
return {
|
|
663
|
+
id,
|
|
664
|
+
atomId: `atom:${id}`,
|
|
665
|
+
kind,
|
|
666
|
+
value: span.value,
|
|
667
|
+
text,
|
|
668
|
+
sourceTextSha256: createHash("sha256").update(source).digest("hex"),
|
|
669
|
+
start: span.start,
|
|
670
|
+
end: span.end,
|
|
671
|
+
startByte: Buffer.byteLength(source.slice(0, span.start)),
|
|
672
|
+
endByte: Buffer.byteLength(source.slice(0, span.end))
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function extractListMemberSpans(source) {
|
|
676
|
+
return [...source.matchAll(/^[ \t]*[-*•][ \t]+(.+)$/gmu)].flatMap((match) => {
|
|
677
|
+
const member = match[1];
|
|
678
|
+
if (member === void 0 || match.index === void 0) return [];
|
|
679
|
+
const start = match.index + match[0].indexOf(member);
|
|
680
|
+
return [{
|
|
681
|
+
start,
|
|
682
|
+
end: start + member.length
|
|
683
|
+
}];
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function extractAnchors(source, locale) {
|
|
687
|
+
const anchors = [...matches(source, /["“‘][^"”’]+["”’]/gu), ...matches(source, /\p{Sc}?\d[\d.,:/-]*/gu)];
|
|
688
|
+
if (getRemUpdateLocaleResource(locale).anchorMode === "capitalized-sequence") anchors.push(...matches(source, /\b\p{Lu}[\p{L}\p{M}]*(?:\s+\p{Lu}[\p{L}\p{M}]*)+\b/gu));
|
|
689
|
+
return uniqueSpans(anchors);
|
|
690
|
+
}
|
|
691
|
+
function matches(source, pattern) {
|
|
692
|
+
return [...source.matchAll(pattern)].flatMap((match) => {
|
|
693
|
+
if (match.index === void 0 || match[0].length === 0) return [];
|
|
694
|
+
return [{
|
|
695
|
+
value: match[0],
|
|
696
|
+
start: match.index,
|
|
697
|
+
end: match.index + match[0].length
|
|
698
|
+
}];
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
function uniqueSpans(spans) {
|
|
702
|
+
const seen = /* @__PURE__ */ new Set();
|
|
703
|
+
return spans.filter((span) => {
|
|
704
|
+
const key = `${span.start}:${span.end}`;
|
|
705
|
+
if (seen.has(key)) return false;
|
|
706
|
+
seen.add(key);
|
|
707
|
+
return true;
|
|
708
|
+
}).sort((left, right) => left.start - right.start || left.end - right.end);
|
|
709
|
+
}
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region src/engine/rem/coverage-contract.ts
|
|
712
|
+
function buildRemCoverageAtoms(input) {
|
|
713
|
+
if (input.source.trim().length === 0) return {
|
|
714
|
+
outcome: "refuse",
|
|
715
|
+
reason: "empty_reference_set"
|
|
716
|
+
};
|
|
717
|
+
if (input.format === "flattened-clauses") return {
|
|
718
|
+
outcome: "refuse",
|
|
719
|
+
reason: "unsegmentable_reference_set"
|
|
720
|
+
};
|
|
721
|
+
if (input.format === "prose") {
|
|
722
|
+
const atoms = buildRemUpdateReferenceSet({
|
|
723
|
+
source: input.source,
|
|
724
|
+
tier: "prose",
|
|
725
|
+
locale: "en"
|
|
726
|
+
});
|
|
727
|
+
return atoms.length === 0 ? {
|
|
728
|
+
outcome: "refuse",
|
|
729
|
+
reason: "empty_reference_set"
|
|
730
|
+
} : {
|
|
731
|
+
outcome: "accept",
|
|
732
|
+
atoms: atoms.map(selectCoverageFields)
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
return buildStructuredAtoms(input.source);
|
|
736
|
+
}
|
|
737
|
+
async function validateRemClauseCoverageArtifacts(input) {
|
|
738
|
+
const arms = {
|
|
739
|
+
goldSet: input.expectedGoldSetSha256 === input.measuredGoldSetSha256 ? "pass" : "fail",
|
|
740
|
+
confidence: input.confidenceSatisfied ? "pass" : "fail",
|
|
741
|
+
chronology: input.declarationSequence < input.firstScoredSequence ? "pass" : "fail",
|
|
742
|
+
measurement: input.measurementPresent ? "pass" : "fail",
|
|
743
|
+
artifactDigest: input.artifactDigestMatches ? "pass" : "fail",
|
|
744
|
+
accuracyFloor: input.accuracyFloor === null ? "owner-unset" : input.measuredAccuracy !== null && input.measuredAccuracy >= input.accuracyFloor ? "pass" : "fail"
|
|
745
|
+
};
|
|
746
|
+
if (input.accuracyFloor === null) return {
|
|
747
|
+
decision: "refuse",
|
|
748
|
+
reason: "coverage.accuracyFloor",
|
|
749
|
+
arms
|
|
750
|
+
};
|
|
751
|
+
if (input.measuredAccuracy === null) return {
|
|
752
|
+
decision: "refuse",
|
|
753
|
+
reason: "coverage.measurementMissing",
|
|
754
|
+
arms
|
|
755
|
+
};
|
|
756
|
+
for (const [arm, status] of Object.entries(arms)) if (status === "fail") return {
|
|
757
|
+
decision: "refuse",
|
|
758
|
+
reason: `coverage.${arm}`,
|
|
759
|
+
arms
|
|
760
|
+
};
|
|
761
|
+
return {
|
|
762
|
+
decision: "allow",
|
|
763
|
+
reason: null,
|
|
764
|
+
arms
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function buildStructuredAtoms(source) {
|
|
768
|
+
let decoded;
|
|
769
|
+
try {
|
|
770
|
+
decoded = JSON.parse(source);
|
|
771
|
+
} catch {
|
|
772
|
+
return {
|
|
773
|
+
outcome: "refuse",
|
|
774
|
+
reason: "unsegmentable_reference_set"
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (decoded === null || typeof decoded !== "object") return {
|
|
778
|
+
outcome: "refuse",
|
|
779
|
+
reason: "unsegmentable_reference_set"
|
|
780
|
+
};
|
|
781
|
+
const descriptors = [];
|
|
782
|
+
collectStructuredLeaves(decoded, "", void 0, descriptors);
|
|
783
|
+
let cursor = 0;
|
|
784
|
+
const digest = createHash("sha256").update(source).digest("hex");
|
|
785
|
+
const atoms = descriptors.flatMap((descriptor) => {
|
|
786
|
+
let start = source.indexOf(descriptor.needle, cursor);
|
|
787
|
+
if (start < 0) start = source.indexOf(descriptor.needle);
|
|
788
|
+
if (start < 0) return [];
|
|
789
|
+
const end = start + descriptor.needle.length;
|
|
790
|
+
cursor = end;
|
|
791
|
+
return [{
|
|
792
|
+
atomId: `atom:${descriptor.id}`,
|
|
793
|
+
kind: descriptor.kind,
|
|
794
|
+
sourceTextSha256: digest,
|
|
795
|
+
startByte: Buffer.byteLength(source.slice(0, start)),
|
|
796
|
+
endByte: Buffer.byteLength(source.slice(0, end)),
|
|
797
|
+
text: source.slice(start, end)
|
|
798
|
+
}];
|
|
799
|
+
});
|
|
800
|
+
return atoms.length === descriptors.length && atoms.length > 0 ? {
|
|
801
|
+
outcome: "accept",
|
|
802
|
+
atoms
|
|
803
|
+
} : {
|
|
804
|
+
outcome: "refuse",
|
|
805
|
+
reason: "unsegmentable_reference_set"
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function collectStructuredLeaves(value, pointer, objectKey, output) {
|
|
809
|
+
if (Array.isArray(value)) {
|
|
810
|
+
value.forEach((member, index) => {
|
|
811
|
+
const memberPointer = `${pointer}/${index}`;
|
|
812
|
+
if (member !== null && typeof member === "object") collectStructuredLeaves(member, memberPointer, void 0, output);
|
|
813
|
+
else output.push({
|
|
814
|
+
id: memberPointer,
|
|
815
|
+
kind: "structured-member",
|
|
816
|
+
needle: JSON.stringify(member)
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (value !== null && typeof value === "object") {
|
|
822
|
+
for (const [key, field] of Object.entries(value)) collectStructuredLeaves(field, `${pointer}/${escapeJsonPointer(key)}`, key, output);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (objectKey !== void 0) output.push({
|
|
826
|
+
id: pointer,
|
|
827
|
+
kind: "structured-field",
|
|
828
|
+
needle: `${JSON.stringify(objectKey)}:${JSON.stringify(value)}`
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
function selectCoverageFields(unit) {
|
|
832
|
+
return {
|
|
833
|
+
atomId: unit.atomId,
|
|
834
|
+
kind: unit.kind,
|
|
835
|
+
sourceTextSha256: unit.sourceTextSha256,
|
|
836
|
+
startByte: unit.startByte,
|
|
837
|
+
endByte: unit.endByte,
|
|
838
|
+
text: unit.text
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function escapeJsonPointer(value) {
|
|
842
|
+
return value.replace(/~/gu, "~0").replace(/\//gu, "~1");
|
|
843
|
+
}
|
|
844
|
+
//#endregion
|
|
845
|
+
//#region src/engine/rem/enable-gate.ts
|
|
846
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
847
|
+
const artifactSchema = z.object({
|
|
848
|
+
schema_version: z.number().int(),
|
|
849
|
+
job_type: z.enum(REM_BUILT_OPERATION_TYPES),
|
|
850
|
+
implementation_version: z.string().min(1),
|
|
851
|
+
corpus_sha256: z.string().regex(SHA256_PATTERN),
|
|
852
|
+
baseline_sha256: z.string().regex(SHA256_PATTERN),
|
|
853
|
+
result: z.enum(["pass", "fail"]),
|
|
854
|
+
evaluated_at: z.string().datetime(),
|
|
855
|
+
expires_at: z.string().datetime()
|
|
856
|
+
}).strict();
|
|
857
|
+
var RemEnableGateError = class extends Error {
|
|
858
|
+
code;
|
|
859
|
+
constructor(code) {
|
|
860
|
+
super(code);
|
|
861
|
+
this.code = code;
|
|
862
|
+
this.name = "RemEnableGateError";
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
const REM_ENABLE_GATE_BINDINGS = {
|
|
866
|
+
implementationVersion: "edge-rem-wave-20260806",
|
|
867
|
+
corpusSha256: "67d1acbe4451cccc0199a9b670b2b8df17b79ba7c3d6d431b4e0e303d73869e2",
|
|
868
|
+
baselineSha256: "87413c08883fddc81d479fe6dcb9d7a537ee156d2fb73716ab0603860caa64c1"
|
|
869
|
+
};
|
|
870
|
+
function loadRemEnableGate(input) {
|
|
871
|
+
const artifactPath = path.join(input.stateDir, "rem-gates", `${input.jobType}.json`);
|
|
872
|
+
let stat;
|
|
873
|
+
try {
|
|
874
|
+
stat = lstatSync(artifactPath);
|
|
875
|
+
} catch {
|
|
876
|
+
throw new RemEnableGateError("artifact_missing");
|
|
877
|
+
}
|
|
878
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new RemEnableGateError("artifact_not_regular");
|
|
879
|
+
if ((stat.mode & 18) !== 0) throw new RemEnableGateError("artifact_permissions");
|
|
880
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new RemEnableGateError("artifact_owner");
|
|
881
|
+
const bytes = readFileSync(artifactPath);
|
|
882
|
+
if (createHash("sha256").update(bytes).digest("hex") !== input.artifactSha256) throw new RemEnableGateError("artifact_digest_mismatch");
|
|
883
|
+
const artifact = parseArtifact(bytes);
|
|
884
|
+
assertArtifactBindings(artifact, input);
|
|
885
|
+
return artifact;
|
|
886
|
+
}
|
|
887
|
+
function parseArtifact(bytes) {
|
|
888
|
+
let decoded;
|
|
889
|
+
try {
|
|
890
|
+
decoded = JSON.parse(bytes.toString("utf8"));
|
|
891
|
+
} catch {
|
|
892
|
+
throw new RemEnableGateError("artifact_schema");
|
|
893
|
+
}
|
|
894
|
+
const parsed = artifactSchema.safeParse(decoded);
|
|
895
|
+
if (!parsed.success) throw new RemEnableGateError("artifact_schema");
|
|
896
|
+
return parsed.data;
|
|
897
|
+
}
|
|
898
|
+
function assertArtifactBindings(artifact, input) {
|
|
899
|
+
if (artifact.schema_version !== 1) throw new RemEnableGateError("schema_version_mismatch");
|
|
900
|
+
if (artifact.job_type !== input.jobType) throw new RemEnableGateError("job_type_mismatch");
|
|
901
|
+
if (artifact.implementation_version !== input.implementationVersion) throw new RemEnableGateError("implementation_version_mismatch");
|
|
902
|
+
if (artifact.corpus_sha256 !== input.corpusSha256) throw new RemEnableGateError("corpus_mismatch");
|
|
903
|
+
if (artifact.baseline_sha256 !== input.baselineSha256) throw new RemEnableGateError("baseline_mismatch");
|
|
904
|
+
if (artifact.result !== "pass") throw new RemEnableGateError("result_not_pass");
|
|
905
|
+
const now = Date.parse(input.now);
|
|
906
|
+
const evaluatedAt = Date.parse(artifact.evaluated_at);
|
|
907
|
+
const expiresAt = Date.parse(artifact.expires_at);
|
|
908
|
+
if (!Number.isFinite(now) || evaluatedAt > now) throw new RemEnableGateError("artifact_not_yet_valid");
|
|
909
|
+
if (expiresAt <= now) throw new RemEnableGateError("artifact_expired");
|
|
910
|
+
}
|
|
911
|
+
//#endregion
|
|
912
|
+
//#region src/engine/rem/entry-governance.ts
|
|
913
|
+
async function recordNonRefusePairDecision(input) {
|
|
914
|
+
if (input.decision === "allow" || input.throughRefusePair) return;
|
|
915
|
+
if (input.reason === null || input.reason.trim().length === 0) throw new Error("A refused pair decision requires a reason");
|
|
916
|
+
const existing = input.database.prepare(`SELECT job_id, job_type, stage, outcome, reason
|
|
917
|
+
FROM nodix_rem_journal WHERE attempt_id = ?`).get(input.attemptIdentity);
|
|
918
|
+
if (existing !== void 0) {
|
|
919
|
+
if (existing.job_id === input.waveId && existing.job_type === input.operation && existing.stage === input.source && existing.outcome === "refused" && existing.reason === input.reason) return;
|
|
920
|
+
throw new Error("attempt_decision_conflict");
|
|
921
|
+
}
|
|
922
|
+
input.database.prepare(`INSERT INTO nodix_rem_journal(
|
|
923
|
+
job_id, job_type, stage, attempt_id, outcome,
|
|
924
|
+
pairs_scanned, verdicts, actions_applied, reason
|
|
925
|
+
) VALUES (?, ?, ?, ?, 'refused', 0, 0, 0, ?)`).run(input.waveId, input.operation, input.source, input.attemptIdentity, input.reason);
|
|
926
|
+
}
|
|
927
|
+
function validateRemSubstantiveWaveEffects(input) {
|
|
928
|
+
const journalRows = input.database.prepare(`SELECT count(*) AS count FROM nodix_rem_journal
|
|
929
|
+
WHERE job_id = ? AND outcome NOT IN ('refused', 'disabled', 'no-action')`).get(input.waveId);
|
|
930
|
+
const effects = input.database.prepare("SELECT count(*) AS count FROM nodix_rem_write_attempts WHERE job_id = ? AND outcome = 'succeeded'").get(input.waveId);
|
|
931
|
+
const groupCloses = input.database.prepare(`SELECT count(*) AS count
|
|
932
|
+
FROM nodix_rem_journal AS journal
|
|
933
|
+
JOIN nodix_memories AS memory
|
|
934
|
+
ON memory.id = CASE WHEN json_valid(journal.detail)
|
|
935
|
+
THEN json_extract(journal.detail, '$.targetRowId') END
|
|
936
|
+
WHERE journal.job_id = ?
|
|
937
|
+
AND journal.outcome = 'done'
|
|
938
|
+
AND journal.stage LIKE 'update-retirement-target:%'
|
|
939
|
+
AND json_extract(memory.metadata, '$.superseded_by') = CASE
|
|
940
|
+
WHEN json_valid(journal.detail)
|
|
941
|
+
THEN json_extract(journal.detail, '$.nominatedRowId') END`).get(input.waveId);
|
|
942
|
+
if ((journalRows?.count ?? 0) > 0 && (effects?.count ?? 0) + (groupCloses?.count ?? 0) === 0) return {
|
|
943
|
+
decision: "refuse",
|
|
944
|
+
reasonCode: "journal_only"
|
|
945
|
+
};
|
|
946
|
+
return {
|
|
947
|
+
decision: "allow",
|
|
948
|
+
reasonCode: null
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function defineRemSafetyGuard(_metadata, guard) {
|
|
952
|
+
return guard;
|
|
953
|
+
}
|
|
954
|
+
function validateRemGuardCensus(input) {
|
|
955
|
+
const manifest = readJsonArray(input.manifestPath);
|
|
956
|
+
const required = readJsonArray(input.requiredGuardsPath);
|
|
957
|
+
const sourceFiles = input.sourceRoots.flatMap(listTypeScriptFiles);
|
|
958
|
+
const sources = new Map(sourceFiles.map((file) => [path.resolve(file), readFileSync(file, "utf8")]));
|
|
959
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
960
|
+
for (const source of sources.values()) for (const match of source.matchAll(/defineRemSafetyGuard\s*\(\s*\{\s*guardId:\s*["']([^"']+)["']/gu)) {
|
|
961
|
+
const guardId = match[1];
|
|
962
|
+
if (guardId !== void 0) discovered.add(guardId);
|
|
963
|
+
}
|
|
964
|
+
const manifestIds = new Set(manifest.map((row) => row.guardId));
|
|
965
|
+
if (manifestIds.size === 0 && discovered.size === 0) return {
|
|
966
|
+
decision: "refuse",
|
|
967
|
+
reasonCode: "guard_inventory_empty"
|
|
968
|
+
};
|
|
969
|
+
if (!sameSet(manifestIds, discovered)) return {
|
|
970
|
+
decision: "refuse",
|
|
971
|
+
reasonCode: [...sources.values()].some((source) => /(?:function\s+\w+\s*\([^)]*\)\s*:\s*RemRefusalCode|throw\s+new\s+\w*RemGateError)/u.test(source)) ? "unmarked_guard" : "manifest_source_inequality"
|
|
972
|
+
};
|
|
973
|
+
if (required.length === 0 || manifest.some((row) => !required.some((item) => item.guardId === row.guardId)) || required.some((item) => !manifestIds.has(item.guardId))) return {
|
|
974
|
+
decision: "refuse",
|
|
975
|
+
reasonCode: "required_protection_missing"
|
|
976
|
+
};
|
|
977
|
+
const reachable = collectReachableFiles(input.productionRoots, sources);
|
|
978
|
+
for (const row of manifest) {
|
|
979
|
+
const moduleFile = [...sources.keys()].find((file) => file.endsWith(row.module));
|
|
980
|
+
if (moduleFile === void 0 || !reachable.has(moduleFile)) return {
|
|
981
|
+
decision: "refuse",
|
|
982
|
+
reasonCode: "guard_unreachable"
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
decision: "allow",
|
|
987
|
+
reasonCode: null
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function readJsonArray(file) {
|
|
991
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
992
|
+
if (!Array.isArray(value)) throw new Error(`${file} must contain a JSON array`);
|
|
993
|
+
return value;
|
|
994
|
+
}
|
|
995
|
+
function listTypeScriptFiles(root) {
|
|
996
|
+
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
|
997
|
+
const entryPath = path.join(root, entry.name);
|
|
998
|
+
if (entry.isDirectory()) return listTypeScriptFiles(entryPath);
|
|
999
|
+
return entry.isFile() && /\.tsx?$/u.test(entry.name) ? [entryPath] : [];
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
function collectReachableFiles(roots, sources) {
|
|
1003
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
1004
|
+
const pending = roots.map((root) => path.resolve(root));
|
|
1005
|
+
while (pending.length > 0) {
|
|
1006
|
+
const file = pending.pop();
|
|
1007
|
+
if (file === void 0 || reachable.has(file)) continue;
|
|
1008
|
+
const source = sources.get(file);
|
|
1009
|
+
if (source === void 0) continue;
|
|
1010
|
+
reachable.add(file);
|
|
1011
|
+
for (const match of source.matchAll(/(?:from\s+|import\s*\()["'](\.[^"']+)["']/gu)) {
|
|
1012
|
+
const specifier = match[1];
|
|
1013
|
+
if (specifier === void 0) continue;
|
|
1014
|
+
for (const candidate of resolveSourceCandidates(file, specifier)) if (sources.has(candidate)) pending.push(candidate);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return reachable;
|
|
1018
|
+
}
|
|
1019
|
+
function resolveSourceCandidates(importer, specifier) {
|
|
1020
|
+
const base = path.resolve(path.dirname(importer), specifier);
|
|
1021
|
+
return [
|
|
1022
|
+
base,
|
|
1023
|
+
`${base}.ts`,
|
|
1024
|
+
`${base}.tsx`,
|
|
1025
|
+
path.join(base, "index.ts")
|
|
1026
|
+
].map((file) => path.resolve(file));
|
|
1027
|
+
}
|
|
1028
|
+
function sameSet(left, right) {
|
|
1029
|
+
return left.size === right.size && [...left].every((value) => right.has(value));
|
|
1030
|
+
}
|
|
1031
|
+
//#endregion
|
|
1032
|
+
//#region src/engine/rem/row-hash.ts
|
|
1033
|
+
/**
|
|
1034
|
+
* The one definition of a REM whole-row hash.
|
|
1035
|
+
*
|
|
1036
|
+
* A recovery record stores `expected_post_hash` before the write; the verified-write guard
|
|
1037
|
+
* recomputes it after the write and refuses the mutation when the two disagree. Those two
|
|
1038
|
+
* computations lived in different packages and drifted: this package hashed `SELECT *` while
|
|
1039
|
+
* `rem-sqlite-adapter` hashed a projected row, so adding the `timezone` column to the projection
|
|
1040
|
+
* made every text-version write refuse AFTER its transaction had already committed. One function,
|
|
1041
|
+
* imported by both sides, is what stops that happening again.
|
|
1042
|
+
*
|
|
1043
|
+
* `REM_ROW_HASH_FIELDS` IS the hash — reordering it, adding to it, or removing from it changes
|
|
1044
|
+
* every value this function produces. That is why it is versioned rather than edited in place.
|
|
1045
|
+
*/
|
|
1046
|
+
const REM_ROW_HASH_FIELDS = [
|
|
1047
|
+
"id",
|
|
1048
|
+
"text",
|
|
1049
|
+
"category",
|
|
1050
|
+
"project_id",
|
|
1051
|
+
"importance",
|
|
1052
|
+
"timestamp",
|
|
1053
|
+
"timezone",
|
|
1054
|
+
"metadata",
|
|
1055
|
+
"content_hash",
|
|
1056
|
+
"fact_id",
|
|
1057
|
+
"derived_from",
|
|
1058
|
+
"consolidation_epoch_id",
|
|
1059
|
+
"confidence_source",
|
|
1060
|
+
"lane",
|
|
1061
|
+
"raw_candidate_json",
|
|
1062
|
+
"disposition_reason",
|
|
1063
|
+
"dispositioned_at_ms"
|
|
1064
|
+
];
|
|
1065
|
+
/**
|
|
1066
|
+
* Stamped on every recovery record this build writes. Version 1 means "written before the field
|
|
1067
|
+
* list was pinned", and a version-1 hash cannot be reproduced — the two producers of that era
|
|
1068
|
+
* covered different columns. Readers must branch on the stored version rather than assume.
|
|
1069
|
+
*/
|
|
1070
|
+
const REM_ROW_HASH_VERSION = 2;
|
|
1071
|
+
/** Version 1 records predate the pinned field list and carry a hash nothing can recompute. */
|
|
1072
|
+
const REM_ROW_HASH_VERSION_LEGACY = 1;
|
|
1073
|
+
function hashRemMemoryRow(row) {
|
|
1074
|
+
const projected = {};
|
|
1075
|
+
for (const field of REM_ROW_HASH_FIELDS) projected[field] = row[field] ?? null;
|
|
1076
|
+
return createHash("sha256").update(JSON.stringify(projected)).digest("hex");
|
|
1077
|
+
}
|
|
1078
|
+
//#endregion
|
|
1079
|
+
//#region src/engine/rem/facet-persistence.ts
|
|
1080
|
+
async function writeRemTwoFacetTransaction(input) {
|
|
1081
|
+
const timestamp = input.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1082
|
+
const updatedAtMs = Date.parse(timestamp);
|
|
1083
|
+
if (!Number.isFinite(updatedAtMs)) throw new Error("REM facet timestamp must be valid");
|
|
1084
|
+
if (input.current.trim().length === 0 || input.history.trim().length === 0) throw new Error("REM facet text must be non-empty");
|
|
1085
|
+
if (input.currentChunks.length === 0) throw new Error("REM current facet requires prepared chunks");
|
|
1086
|
+
assertJobIdentity(input.jobId, input.jobType);
|
|
1087
|
+
return input.database.transaction(() => {
|
|
1088
|
+
const row = input.database.prepare("SELECT * FROM nodix_memories WHERE id = ?").get(input.memoryId);
|
|
1089
|
+
if (row === void 0) throw new Error("REM facet target row is missing");
|
|
1090
|
+
const nextHash = input.contentHash ?? createHash("sha256").update(input.current, "utf8").digest("hex");
|
|
1091
|
+
const recoveryHandle = randomUUID();
|
|
1092
|
+
const priorFacets = input.database.prepare("SELECT facet, text, updated_at_ms FROM nodix_rem_memory_facets WHERE memory_id = ? ORDER BY facet").all(input.memoryId);
|
|
1093
|
+
const priorChunkFacets = input.database.prepare("SELECT chunk_id, facet, chunk_index FROM nodix_memory_chunks WHERE memory_id = ? ORDER BY chunk_id").all(input.memoryId);
|
|
1094
|
+
if (input.metadata === void 0) input.database.prepare("UPDATE nodix_memories SET text = ?, content_hash = ? WHERE id = ?").run(input.current, nextHash, input.memoryId);
|
|
1095
|
+
else input.database.prepare("UPDATE nodix_memories SET text = ?, metadata = ?, content_hash = ? WHERE id = ?").run(input.current, input.metadata, nextHash, input.memoryId);
|
|
1096
|
+
if (process.env["SNO_STATION_MEM_REM_TEST_FAILPOINT"] === "after_primary_write") throw new Error("SNO_STATION_MEM_REM_TEST_FAILPOINT:after_primary_write");
|
|
1097
|
+
const postRow = input.database.prepare("SELECT * FROM nodix_memories WHERE id = ?").get(input.memoryId);
|
|
1098
|
+
if (postRow === void 0) throw new Error("REM facet target row disappeared after update");
|
|
1099
|
+
const expectedPostHash = hashRemMemoryRow(postRow);
|
|
1100
|
+
input.database.prepare(`INSERT INTO nodix_rem_recovery_history(
|
|
1101
|
+
recovery_handle, row_id, operation_kind, prior_row_image, prior_content_hash,
|
|
1102
|
+
expected_post_hash, row_hash_version, reason, mutation_ts
|
|
1103
|
+
) VALUES (?, ?, 'text-version', ?, ?, ?, ?, ?, ?)`).run(recoveryHandle, input.memoryId, JSON.stringify(row), row.content_hash, expectedPostHash, 2, input.reason ?? "REM two-facet update", timestamp);
|
|
1104
|
+
if (input.attemptId !== void 0) input.database.prepare(`UPDATE nodix_rem_write_attempts SET expected_post_content_sha256 = ?
|
|
1105
|
+
WHERE attempt_id = ? AND outcome = 'pending'`).run(nextHash, input.attemptId);
|
|
1106
|
+
const historyIndex = input.database.prepare("SELECT COALESCE(MAX(chunk_index), -1) AS value FROM nodix_memory_chunks WHERE memory_id = ? AND facet = 'history'").get(input.memoryId);
|
|
1107
|
+
input.database.prepare("UPDATE nodix_memory_chunks SET facet = 'history', chunk_index = chunk_index + ? WHERE memory_id = ? AND facet = 'current'").run(historyIndex.value + 1, input.memoryId);
|
|
1108
|
+
const insertChunk = input.database.prepare(`INSERT INTO nodix_memory_chunks(
|
|
1109
|
+
chunk_id, memory_id, chunk_index, chunk_text, dense_payload, summary,
|
|
1110
|
+
entities, tags, source, start_offset, end_offset, token_count, content_type,
|
|
1111
|
+
chunking_version, embedder_provider, embedder_model, embedder_dim,
|
|
1112
|
+
created_at, updated_at, facet
|
|
1113
|
+
) VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'current')`);
|
|
1114
|
+
const insertVector = input.database.prepare("INSERT INTO nodix_memory_chunk_vectors(id, project_id, embedding) VALUES (?, ?, vec_f32(?))");
|
|
1115
|
+
for (const chunk of input.currentChunks) {
|
|
1116
|
+
insertChunk.run(chunk.chunkId, input.memoryId, chunk.chunkIndex, chunk.chunkText, chunk.densePayload, chunk.summary ?? null, chunk.startOffset, chunk.endOffset, chunk.tokenCount, chunk.contentType, chunk.chunkingVersion, chunk.embedderProvider, chunk.embedderModel, chunk.embedderDim, updatedAtMs, updatedAtMs);
|
|
1117
|
+
insertVector.run(chunk.chunkId, row.project_id, chunk.vectorBytes);
|
|
1118
|
+
}
|
|
1119
|
+
input.database.prepare("DELETE FROM nodix_rem_memory_facets WHERE memory_id = ?").run(input.memoryId);
|
|
1120
|
+
const insertFacet = input.database.prepare("INSERT INTO nodix_rem_memory_facets(memory_id, facet, text, updated_at_ms) VALUES (?, ?, ?, ?)");
|
|
1121
|
+
insertFacet.run(input.memoryId, "current", input.current, updatedAtMs);
|
|
1122
|
+
insertFacet.run(input.memoryId, "history", input.history, updatedAtMs);
|
|
1123
|
+
const expectedFacets = input.database.prepare("SELECT facet, text, updated_at_ms FROM nodix_rem_memory_facets WHERE memory_id = ? ORDER BY facet").all(input.memoryId);
|
|
1124
|
+
const expectedChunkFacets = input.database.prepare("SELECT chunk_id, facet, chunk_index FROM nodix_memory_chunks WHERE memory_id = ? ORDER BY chunk_id").all(input.memoryId);
|
|
1125
|
+
input.database.prepare(`INSERT INTO nodix_rem_facet_recovery(
|
|
1126
|
+
recovery_handle, prior_facets_json, prior_chunk_facets_json,
|
|
1127
|
+
expected_facets_json, expected_chunk_facets_json
|
|
1128
|
+
) VALUES (?, ?, ?, ?, ?)`).run(recoveryHandle, JSON.stringify(priorFacets), JSON.stringify(priorChunkFacets), JSON.stringify(expectedFacets), JSON.stringify(expectedChunkFacets));
|
|
1129
|
+
const queueId = input.jobId;
|
|
1130
|
+
input.database.prepare(`INSERT INTO nodix_rem_journal(
|
|
1131
|
+
job_id, job_type, stage, outcome, row_id, pairs_scanned, verdicts, actions_applied
|
|
1132
|
+
) VALUES (?, ?, 'two-facet-write', 'done', ?, 0, 0, 1)`).run(queueId, input.jobType, input.memoryId);
|
|
1133
|
+
return {
|
|
1134
|
+
contentHash: nextHash,
|
|
1135
|
+
recoveryHandle
|
|
1136
|
+
};
|
|
1137
|
+
}).immediate();
|
|
1138
|
+
}
|
|
1139
|
+
//#endregion
|
|
1140
|
+
//#region src/engine/rem/repository.ts
|
|
1141
|
+
const bootIdPath = "/proc/sys/kernel/random/boot_id";
|
|
1142
|
+
function createRemRepository(database) {
|
|
1143
|
+
return {
|
|
1144
|
+
recordClassification: (input) => recordClassification(database, input),
|
|
1145
|
+
claimRow: (input) => claimRow(database, input),
|
|
1146
|
+
recoverRowClaim: (input) => recoverRowClaim(database, input),
|
|
1147
|
+
completeRowClaim: (input) => completeRowClaim(database, input),
|
|
1148
|
+
releaseRowClaim: (input) => releaseRowClaim(database, input),
|
|
1149
|
+
createVerdictGeneration: (input) => createVerdictGeneration(database, input),
|
|
1150
|
+
findOpenVerdictGeneration: (input) => findOpenVerdictGeneration(database, input),
|
|
1151
|
+
listVerdictPairs: (generationId) => listVerdictPairs(database, generationId),
|
|
1152
|
+
readClosedVerdictPairIds: (pairIds) => readClosedVerdictPairIds(database, pairIds),
|
|
1153
|
+
claimNextPair: (input) => claimNextPair(database, input),
|
|
1154
|
+
releasePairClaim: (input) => releasePairClaim(database, input),
|
|
1155
|
+
completePair: (input) => completePair(database, input),
|
|
1156
|
+
refusePair: (input) => refusePair(database, input),
|
|
1157
|
+
reserveLlmBudget: (input) => reserveLlmBudget(database, input),
|
|
1158
|
+
readGeneration: (generationId) => readGeneration(database, generationId),
|
|
1159
|
+
readInvocation: (input) => readInvocation(database, input),
|
|
1160
|
+
appendJournal: (jobId, jobType, entry) => appendJournal(database, jobId, jobType, entry),
|
|
1161
|
+
listJournal: (jobId, jobType) => listJournal(database, jobId, jobType),
|
|
1162
|
+
recordVerdictCheckpoint: (input) => recordVerdictCheckpoint(database, input),
|
|
1163
|
+
resumeVerdictPair: (input) => resumeVerdictPair(database, input),
|
|
1164
|
+
readVerdictPair: (generationId, pairId) => readVerdictPair(database, generationId, pairId)
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function recordClassification(database, input) {
|
|
1168
|
+
database.prepare(`INSERT INTO nodix_rem_relation_ledger(
|
|
1169
|
+
row_id, content_hash, state, owner, claim_ts, classified_at
|
|
1170
|
+
) VALUES (?, ?, ?, 'none', NULL, ?)
|
|
1171
|
+
ON CONFLICT(row_id) DO UPDATE SET
|
|
1172
|
+
content_hash = excluded.content_hash,
|
|
1173
|
+
state = excluded.state,
|
|
1174
|
+
classified_at = excluded.classified_at
|
|
1175
|
+
WHERE nodix_rem_relation_ledger.owner = 'none'`).run(input.rowId, input.contentHash, input.state, input.classifiedAt);
|
|
1176
|
+
}
|
|
1177
|
+
function claimRow(database, input) {
|
|
1178
|
+
const holderProcessStart = requireProcessStart(input.holderPid);
|
|
1179
|
+
const holderBootId = requireBootId();
|
|
1180
|
+
return database.transaction(() => {
|
|
1181
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_relation_ledger
|
|
1182
|
+
SET owner = ?, claim_ts = ?
|
|
1183
|
+
WHERE row_id = ? AND owner = 'none' AND content_hash = ?
|
|
1184
|
+
AND EXISTS (
|
|
1185
|
+
SELECT 1 FROM nodix_memories
|
|
1186
|
+
WHERE id = nodix_rem_relation_ledger.row_id
|
|
1187
|
+
AND content_hash = nodix_rem_relation_ledger.content_hash
|
|
1188
|
+
)`).run(input.owner, input.claimTs, input.rowId, input.contentHash)).changes === 1) {
|
|
1189
|
+
database.prepare(`INSERT INTO nodix_rem_row_claims(
|
|
1190
|
+
row_id, content_hash, owner, claim_token, holder_pid,
|
|
1191
|
+
holder_process_start, holder_boot_id, claimed_at
|
|
1192
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(input.rowId, input.contentHash, input.owner, input.claimToken, input.holderPid, holderProcessStart, holderBootId, input.claimTs);
|
|
1193
|
+
return { claimed: true };
|
|
1194
|
+
}
|
|
1195
|
+
const ledger = database.prepare("SELECT content_hash, owner FROM nodix_rem_relation_ledger WHERE row_id = ?").get(input.rowId);
|
|
1196
|
+
const memory = database.prepare("SELECT content_hash FROM nodix_memories WHERE id = ?").get(input.rowId);
|
|
1197
|
+
if (!ledger || !memory) return {
|
|
1198
|
+
claimed: false,
|
|
1199
|
+
reason: "missing"
|
|
1200
|
+
};
|
|
1201
|
+
if (ledger.content_hash !== input.contentHash || memory.content_hash !== input.contentHash) return {
|
|
1202
|
+
claimed: false,
|
|
1203
|
+
reason: "content_changed"
|
|
1204
|
+
};
|
|
1205
|
+
const claim = database.prepare("SELECT claim_token FROM nodix_rem_row_claims WHERE row_id = ?").get(input.rowId);
|
|
1206
|
+
if (!claim) throw new Error("row claim has inconsistent durable state");
|
|
1207
|
+
return {
|
|
1208
|
+
claimed: false,
|
|
1209
|
+
reason: "already_claimed",
|
|
1210
|
+
claimToken: claim.claim_token
|
|
1211
|
+
};
|
|
1212
|
+
}).immediate();
|
|
1213
|
+
}
|
|
1214
|
+
function recoverRowClaim(database, input) {
|
|
1215
|
+
const holderProcessStart = requireProcessStart(input.holderPid);
|
|
1216
|
+
const holderBootId = requireBootId();
|
|
1217
|
+
const refusal = inspectRowClaimOwnership(database, input);
|
|
1218
|
+
if (refusal) return {
|
|
1219
|
+
recovered: false,
|
|
1220
|
+
reason: refusal
|
|
1221
|
+
};
|
|
1222
|
+
const prior = getRowClaim(database, input.rowId);
|
|
1223
|
+
if (!(prior.holder_pid === input.holderPid && prior.holder_process_start === holderProcessStart && prior.holder_boot_id === holderBootId) && isExactProcessAlive(prior.holder_pid, prior.holder_process_start, prior.holder_boot_id, holderBootId)) return {
|
|
1224
|
+
recovered: false,
|
|
1225
|
+
reason: "holder_alive"
|
|
1226
|
+
};
|
|
1227
|
+
return database.transaction(() => {
|
|
1228
|
+
const currentRefusal = inspectRowClaimOwnership(database, input);
|
|
1229
|
+
if (currentRefusal) return {
|
|
1230
|
+
recovered: false,
|
|
1231
|
+
reason: currentRefusal
|
|
1232
|
+
};
|
|
1233
|
+
const memory = database.prepare("SELECT content_hash FROM nodix_memories WHERE id = ?").get(input.rowId);
|
|
1234
|
+
if (!memory) return {
|
|
1235
|
+
recovered: false,
|
|
1236
|
+
reason: "missing"
|
|
1237
|
+
};
|
|
1238
|
+
if (memory.content_hash !== input.contentHash) {
|
|
1239
|
+
if (asRunResult(database.prepare(`DELETE FROM nodix_rem_row_claims
|
|
1240
|
+
WHERE row_id = ? AND content_hash = ? AND owner = ? AND state = 'active'
|
|
1241
|
+
AND claim_token = ? AND holder_pid = ? AND holder_process_start = ?
|
|
1242
|
+
AND holder_boot_id IS ?`).run(input.rowId, input.contentHash, input.owner, input.expectedClaimToken, prior.holder_pid, prior.holder_process_start, prior.holder_boot_id)).changes !== 1) return {
|
|
1243
|
+
recovered: false,
|
|
1244
|
+
reason: "claim_changed"
|
|
1245
|
+
};
|
|
1246
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_relation_ledger SET owner = 'none', claim_ts = NULL
|
|
1247
|
+
WHERE row_id = ? AND content_hash = ? AND owner = ?`).run(input.rowId, input.contentHash, input.owner)).changes !== 1) throw new Error("row claim has inconsistent durable state");
|
|
1248
|
+
return {
|
|
1249
|
+
recovered: false,
|
|
1250
|
+
reason: "content_changed_released"
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_row_claims SET
|
|
1254
|
+
claim_token = ?, holder_pid = ?, holder_process_start = ?,
|
|
1255
|
+
holder_boot_id = ?, claimed_at = ?
|
|
1256
|
+
WHERE row_id = ? AND content_hash = ? AND owner = ? AND state = 'active'
|
|
1257
|
+
AND claim_token = ? AND holder_pid = ? AND holder_process_start = ?
|
|
1258
|
+
AND holder_boot_id IS ?`).run(input.claimToken, input.holderPid, holderProcessStart, holderBootId, input.claimTs, input.rowId, input.contentHash, input.owner, input.expectedClaimToken, prior.holder_pid, prior.holder_process_start, prior.holder_boot_id)).changes !== 1) return {
|
|
1259
|
+
recovered: false,
|
|
1260
|
+
reason: "claim_changed"
|
|
1261
|
+
};
|
|
1262
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_relation_ledger SET claim_ts = ?
|
|
1263
|
+
WHERE row_id = ? AND content_hash = ? AND owner = ?
|
|
1264
|
+
AND EXISTS (
|
|
1265
|
+
SELECT 1 FROM nodix_memories
|
|
1266
|
+
WHERE id = nodix_rem_relation_ledger.row_id
|
|
1267
|
+
AND content_hash = nodix_rem_relation_ledger.content_hash
|
|
1268
|
+
)`).run(input.claimTs, input.rowId, input.contentHash, input.owner)).changes !== 1) throw new Error("row claim has inconsistent durable state");
|
|
1269
|
+
return { recovered: true };
|
|
1270
|
+
}).immediate();
|
|
1271
|
+
}
|
|
1272
|
+
function completeRowClaim(database, input) {
|
|
1273
|
+
assertJobIdentity(input.jobId, input.jobType);
|
|
1274
|
+
return database.transaction(() => {
|
|
1275
|
+
const prior = getRowClaim(database, input.rowId);
|
|
1276
|
+
if (prior.state === "completed") {
|
|
1277
|
+
const ledger = database.prepare("SELECT content_hash, owner FROM nodix_rem_relation_ledger WHERE row_id = ?").get(input.rowId);
|
|
1278
|
+
const memory = database.prepare("SELECT content_hash FROM nodix_memories WHERE id = ?").get(input.rowId);
|
|
1279
|
+
if (prior.claim_token === input.claimToken && prior.content_hash === input.contentHash && prior.owner === input.owner && ledger?.content_hash === input.contentHash && ledger.owner === input.owner && memory?.content_hash === input.currentContentHash) return { completed: true };
|
|
1280
|
+
}
|
|
1281
|
+
const refusal = inspectRowClaim(database, {
|
|
1282
|
+
...input,
|
|
1283
|
+
expectedClaimToken: input.claimToken,
|
|
1284
|
+
expectedMemoryContentHash: input.currentContentHash
|
|
1285
|
+
});
|
|
1286
|
+
if (refusal) {
|
|
1287
|
+
if (refusal === "content_changed") appendJournal(database, input.jobId, input.jobType, {
|
|
1288
|
+
stage: `row-claim:${input.rowId}`,
|
|
1289
|
+
outcome: "refused",
|
|
1290
|
+
pairsScanned: 0,
|
|
1291
|
+
verdicts: 0,
|
|
1292
|
+
actionsApplied: 0,
|
|
1293
|
+
reason: refusal,
|
|
1294
|
+
rowId: input.rowId
|
|
1295
|
+
});
|
|
1296
|
+
return {
|
|
1297
|
+
completed: false,
|
|
1298
|
+
reason: refusal
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_row_claims SET state = 'completed', completed_at = ?
|
|
1302
|
+
WHERE row_id = ? AND content_hash = ? AND owner = ?
|
|
1303
|
+
AND claim_token = ? AND state = 'active'`).run(input.completedAt, input.rowId, input.contentHash, input.owner, input.claimToken)).changes !== 1) return {
|
|
1304
|
+
completed: false,
|
|
1305
|
+
reason: "claim_changed"
|
|
1306
|
+
};
|
|
1307
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_relation_ledger SET claim_ts = NULL
|
|
1308
|
+
WHERE row_id = ? AND content_hash = ? AND owner = ?`).run(input.rowId, input.contentHash, input.owner)).changes !== 1) throw new Error("row claim has inconsistent durable state");
|
|
1309
|
+
return { completed: true };
|
|
1310
|
+
}).immediate();
|
|
1311
|
+
}
|
|
1312
|
+
function releaseRowClaim(database, input) {
|
|
1313
|
+
return database.transaction(() => {
|
|
1314
|
+
const refusal = inspectRowClaimOwnership(database, {
|
|
1315
|
+
...input,
|
|
1316
|
+
expectedClaimToken: input.claimToken
|
|
1317
|
+
});
|
|
1318
|
+
if (refusal) return {
|
|
1319
|
+
released: false,
|
|
1320
|
+
reason: refusal
|
|
1321
|
+
};
|
|
1322
|
+
if (asRunResult(database.prepare(`DELETE FROM nodix_rem_row_claims
|
|
1323
|
+
WHERE row_id = ? AND content_hash = ? AND owner = ?
|
|
1324
|
+
AND claim_token = ? AND state = 'active'`).run(input.rowId, input.contentHash, input.owner, input.claimToken)).changes !== 1) return {
|
|
1325
|
+
released: false,
|
|
1326
|
+
reason: "claim_changed"
|
|
1327
|
+
};
|
|
1328
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_relation_ledger SET owner = 'none', claim_ts = NULL
|
|
1329
|
+
WHERE row_id = ? AND content_hash = ? AND owner = ?`).run(input.rowId, input.contentHash, input.owner)).changes !== 1) throw new Error("row claim has inconsistent durable state");
|
|
1330
|
+
return { released: true };
|
|
1331
|
+
}).immediate();
|
|
1332
|
+
}
|
|
1333
|
+
function createVerdictGeneration(database, input) {
|
|
1334
|
+
database.transaction(() => {
|
|
1335
|
+
database.prepare(`INSERT INTO nodix_rem_scan_generations(
|
|
1336
|
+
generation_id, corpus_snapshot_hash, pairing_config_hash, max_llm_calls, max_tokens
|
|
1337
|
+
) VALUES (?, ?, ?, ?, ?)`).run(input.generationId, input.corpusSnapshotHash, input.pairingConfigHash, input.maxLlmCalls, input.maxTokens);
|
|
1338
|
+
const insertPair = database.prepare(`INSERT INTO nodix_rem_scan_pairs(
|
|
1339
|
+
generation_id, pair_id, left_row_id, right_row_id, sort_key,
|
|
1340
|
+
claim_state, attempt_count, progress_state, refusal_reason, inherited_from_generation_id
|
|
1341
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
1342
|
+
const inheritedRefusal = input.inheritedRefusalsFromGenerationId ? database.prepare(`SELECT attempt_count, refusal_reason FROM nodix_rem_scan_pairs
|
|
1343
|
+
WHERE generation_id = ? AND pair_id = ? AND progress_state = 'refused'
|
|
1344
|
+
AND refusal_reason IN ('content_changed', 'target_changed')`) : void 0;
|
|
1345
|
+
for (const pair of input.pairs) {
|
|
1346
|
+
const inherited = inheritedRefusal?.get(input.inheritedRefusalsFromGenerationId, pair.pairId);
|
|
1347
|
+
insertPair.run(input.generationId, pair.pairId, pair.leftRowId, pair.rightRowId, pair.sortKey, "unvisited", inherited?.attempt_count ?? 0, inherited ? "refused" : "pending", inherited?.refusal_reason ?? null, inherited ? input.inheritedRefusalsFromGenerationId : null);
|
|
1348
|
+
}
|
|
1349
|
+
}).immediate();
|
|
1350
|
+
}
|
|
1351
|
+
function findOpenVerdictGeneration(database, input) {
|
|
1352
|
+
const row = database.prepare(`SELECT generation_id, corpus_snapshot_hash
|
|
1353
|
+
FROM nodix_rem_scan_generations AS generation
|
|
1354
|
+
WHERE corpus_snapshot_hash = ? AND pairing_config_hash = ?
|
|
1355
|
+
AND EXISTS (
|
|
1356
|
+
SELECT 1 FROM nodix_rem_scan_pairs AS pair
|
|
1357
|
+
WHERE pair.generation_id = generation.generation_id
|
|
1358
|
+
AND pair.claim_state != 'done'
|
|
1359
|
+
)
|
|
1360
|
+
ORDER BY rowid
|
|
1361
|
+
LIMIT 1`).get(input.corpusSnapshotHash, input.pairingConfigHash);
|
|
1362
|
+
return row === void 0 ? void 0 : {
|
|
1363
|
+
generationId: row.generation_id,
|
|
1364
|
+
corpusSnapshotHash: row.corpus_snapshot_hash
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function listVerdictPairs(database, generationId) {
|
|
1368
|
+
return database.prepare(`SELECT pair_id, left_row_id, right_row_id, sort_key, claim_state, invocation_id
|
|
1369
|
+
FROM nodix_rem_scan_pairs
|
|
1370
|
+
WHERE generation_id = ?
|
|
1371
|
+
ORDER BY sort_key, pair_id`).all(generationId).map((row) => ({
|
|
1372
|
+
pairId: row.pair_id,
|
|
1373
|
+
leftRowId: row.left_row_id,
|
|
1374
|
+
rightRowId: row.right_row_id,
|
|
1375
|
+
sortKey: row.sort_key,
|
|
1376
|
+
claimState: row.claim_state,
|
|
1377
|
+
invocationId: row.invocation_id
|
|
1378
|
+
}));
|
|
1379
|
+
}
|
|
1380
|
+
function readClosedVerdictPairIds(database, pairIds) {
|
|
1381
|
+
const rows = database.prepare(`SELECT DISTINCT pair_id FROM nodix_rem_scan_pairs
|
|
1382
|
+
WHERE progress_state = 'closed' AND pair_id IN (SELECT value FROM json_each(?))`).all(JSON.stringify(pairIds));
|
|
1383
|
+
return new Set(rows.map((row) => row.pair_id));
|
|
1384
|
+
}
|
|
1385
|
+
function claimNextPair(database, input) {
|
|
1386
|
+
const holderProcessStart = requireProcessStart(input.holderPid);
|
|
1387
|
+
const holderBootId = requireBootId();
|
|
1388
|
+
return database.transaction(() => {
|
|
1389
|
+
ensureInvocation(database, input.generationId, input.invocationId, input.claimedAt);
|
|
1390
|
+
const pair = database.prepare(`SELECT pair_id, left_row_id, right_row_id, sort_key, checkpoint, verdict,
|
|
1391
|
+
actions_applied, claim_state, invocation_id, budget_reserved, budget_invocation_id,
|
|
1392
|
+
reserved_tokens, attempt_count, progress_state, refusal_reason, inherited_from_generation_id
|
|
1393
|
+
FROM nodix_rem_scan_pairs
|
|
1394
|
+
WHERE generation_id = ? AND claim_state = 'unvisited'
|
|
1395
|
+
ORDER BY CASE progress_state WHEN 'refused' THEN 0 ELSE 1 END, sort_key, pair_id
|
|
1396
|
+
LIMIT 1`).get(input.generationId);
|
|
1397
|
+
if (!pair) return void 0;
|
|
1398
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_scan_pairs SET
|
|
1399
|
+
claim_state = 'claimed', invocation_id = ?, claimed_at = ?,
|
|
1400
|
+
attempt_count = attempt_count + 1, progress_state = 'pending', refusal_reason = NULL
|
|
1401
|
+
WHERE generation_id = ? AND pair_id = ? AND claim_state = 'unvisited'`).run(input.invocationId, input.claimedAt, input.generationId, pair.pair_id)).changes !== 1) return void 0;
|
|
1402
|
+
database.prepare(`INSERT INTO nodix_rem_pair_claims(
|
|
1403
|
+
generation_id, pair_id, claim_token, holder_pid, holder_process_start,
|
|
1404
|
+
holder_boot_id, claimed_at
|
|
1405
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(input.generationId, pair.pair_id, input.invocationId, input.holderPid, holderProcessStart, holderBootId, input.claimedAt);
|
|
1406
|
+
return {
|
|
1407
|
+
pairId: pair.pair_id,
|
|
1408
|
+
leftRowId: pair.left_row_id,
|
|
1409
|
+
rightRowId: pair.right_row_id
|
|
1410
|
+
};
|
|
1411
|
+
}).immediate();
|
|
1412
|
+
}
|
|
1413
|
+
function completePair(database, input) {
|
|
1414
|
+
database.transaction(() => {
|
|
1415
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_scan_pairs SET claim_state = 'done', progress_state = 'closed'
|
|
1416
|
+
WHERE generation_id = ? AND pair_id = ? AND claim_state = 'claimed'
|
|
1417
|
+
AND invocation_id = ?`).run(input.generationId, input.pairId, input.invocationId)).changes !== 1) throw new Error("pair is not owned by invocation");
|
|
1418
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_pair_claims SET state = 'completed', completed_at = CURRENT_TIMESTAMP
|
|
1419
|
+
WHERE generation_id = ? AND pair_id = ? AND claim_token = ? AND state = 'active'`).run(input.generationId, input.pairId, input.invocationId)).changes !== 1) throw new Error("pair claim has inconsistent durable state");
|
|
1420
|
+
}).immediate();
|
|
1421
|
+
}
|
|
1422
|
+
function releasePairClaim(database, input) {
|
|
1423
|
+
database.transaction(() => {
|
|
1424
|
+
if (asRunResult(database.prepare(`DELETE FROM nodix_rem_pair_claims WHERE generation_id = ? AND pair_id = ?
|
|
1425
|
+
AND claim_token = ? AND state = 'active'`).run(input.generationId, input.pairId, input.invocationId)).changes !== 1) throw new Error("pair claim is not owned by invocation");
|
|
1426
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_scan_pairs SET claim_state = 'unvisited', invocation_id = NULL,
|
|
1427
|
+
claimed_at = NULL, checkpoint = NULL, verdict = NULL,
|
|
1428
|
+
budget_reserved = 0, budget_invocation_id = NULL, reserved_tokens = 0,
|
|
1429
|
+
attempt_count = MAX(0, attempt_count - 1)
|
|
1430
|
+
WHERE generation_id = ? AND pair_id = ? AND claim_state = 'claimed' AND invocation_id = ?`).run(input.generationId, input.pairId, input.invocationId)).changes !== 1) throw new Error("pair claim has inconsistent durable state");
|
|
1431
|
+
database.prepare("DELETE FROM nodix_rem_pair_stage_budgets WHERE generation_id = ? AND pair_id = ?").run(input.generationId, input.pairId);
|
|
1432
|
+
}).immediate();
|
|
1433
|
+
}
|
|
1434
|
+
function refusePair(database, input) {
|
|
1435
|
+
if (input.reason.trim().length === 0) throw new Error("pair refusal requires a reason");
|
|
1436
|
+
assertJobIdentity(input.jobId, input.jobType);
|
|
1437
|
+
return database.transaction(() => {
|
|
1438
|
+
const pair = getPairRow(database, input.generationId, input.pairId);
|
|
1439
|
+
if (pair.claim_state !== "claimed" || pair.invocation_id !== input.invocationId) throw new Error("pair is not owned by invocation");
|
|
1440
|
+
const state = pair.attempt_count >= 3 ? "exhausted" : "refused";
|
|
1441
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_scan_pairs SET claim_state = 'done', progress_state = ?, refusal_reason = ?
|
|
1442
|
+
WHERE generation_id = ? AND pair_id = ? AND claim_state = 'claimed'
|
|
1443
|
+
AND invocation_id = ?`).run(state, input.reason, input.generationId, input.pairId, input.invocationId)).changes !== 1) throw new Error("pair is not owned by invocation");
|
|
1444
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_pair_claims SET state = 'completed', completed_at = CURRENT_TIMESTAMP
|
|
1445
|
+
WHERE generation_id = ? AND pair_id = ? AND claim_token = ? AND state = 'active'`).run(input.generationId, input.pairId, input.invocationId)).changes !== 1) throw new Error("pair claim has inconsistent durable state");
|
|
1446
|
+
appendJournal(database, input.jobId, input.jobType, {
|
|
1447
|
+
stage: `replace-pair:${input.pairId}`,
|
|
1448
|
+
outcome: "refused",
|
|
1449
|
+
pairsScanned: 1,
|
|
1450
|
+
verdicts: 1,
|
|
1451
|
+
actionsApplied: 0,
|
|
1452
|
+
reason: input.reason,
|
|
1453
|
+
pairId: input.pairId
|
|
1454
|
+
});
|
|
1455
|
+
return {
|
|
1456
|
+
state,
|
|
1457
|
+
attemptCount: pair.attempt_count
|
|
1458
|
+
};
|
|
1459
|
+
}).immediate();
|
|
1460
|
+
}
|
|
1461
|
+
function reserveLlmBudget(database, input) {
|
|
1462
|
+
return parseBudgetReservation(database.transaction(() => {
|
|
1463
|
+
const pair = getPairRow(database, input.generationId, input.pairId);
|
|
1464
|
+
if (pair.claim_state !== "claimed" || pair.invocation_id !== input.invocationId) throw new Error("budget pair is not owned by invocation");
|
|
1465
|
+
const holder = getPairClaim(database, input.generationId, input.pairId);
|
|
1466
|
+
if (holder.state !== "active" || holder.claim_token !== input.invocationId) throw new Error("budget pair has inconsistent durable claim");
|
|
1467
|
+
const existingStage = database.prepare(`SELECT reserved_tokens, idempotency_key FROM nodix_rem_pair_stage_budgets
|
|
1468
|
+
WHERE generation_id = ? AND pair_id = ? AND stage = ?`).get(input.generationId, input.pairId, input.stage);
|
|
1469
|
+
if (existingStage !== void 0) return {
|
|
1470
|
+
status: "reserved",
|
|
1471
|
+
tokens: existingStage.reserved_tokens,
|
|
1472
|
+
idempotencyKey: existingStage.idempotency_key
|
|
1473
|
+
};
|
|
1474
|
+
if (!Number.isSafeInteger(input.outputTokenCap) || input.outputTokenCap < 0) throw new Error("output token cap must be a non-negative safe integer");
|
|
1475
|
+
let tokens;
|
|
1476
|
+
if (input.providerUsage) tokens = input.providerUsage.inputTokens + input.providerUsage.outputTokens;
|
|
1477
|
+
else if (input.countTokens) tokens = input.countTokens(input.prompt) + input.outputTokenCap;
|
|
1478
|
+
else {
|
|
1479
|
+
persistPendingStage(database, input.generationId, input.stage);
|
|
1480
|
+
return {
|
|
1481
|
+
status: "pending",
|
|
1482
|
+
reason: "tokenizer_unavailable"
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_scan_invocations
|
|
1486
|
+
SET llm_calls_used = llm_calls_used + 1, tokens_used = tokens_used + ?
|
|
1487
|
+
WHERE generation_id = ? AND invocation_id = ?`).run(tokens, input.generationId, input.invocationId)).changes === 1) {
|
|
1488
|
+
database.prepare(`UPDATE nodix_rem_scan_generations
|
|
1489
|
+
SET llm_calls_used = llm_calls_used + 1, tokens_used = tokens_used + ?
|
|
1490
|
+
WHERE generation_id = ?`).run(tokens, input.generationId);
|
|
1491
|
+
const idempotencyKey = `${holder.claim_token}:${input.stage}`;
|
|
1492
|
+
database.prepare(`INSERT INTO nodix_rem_pair_stage_budgets(
|
|
1493
|
+
generation_id, pair_id, stage, invocation_id, reserved_tokens, idempotency_key
|
|
1494
|
+
) VALUES (?, ?, ?, ?, ?, ?)`).run(input.generationId, input.pairId, input.stage, input.invocationId, tokens, idempotencyKey);
|
|
1495
|
+
return {
|
|
1496
|
+
status: "reserved",
|
|
1497
|
+
tokens,
|
|
1498
|
+
idempotencyKey
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
persistPendingStage(database, input.generationId, input.stage);
|
|
1502
|
+
return {
|
|
1503
|
+
status: "pending",
|
|
1504
|
+
reason: "invocation_row_missing"
|
|
1505
|
+
};
|
|
1506
|
+
}).immediate());
|
|
1507
|
+
}
|
|
1508
|
+
function readGeneration(database, generationId) {
|
|
1509
|
+
const row = getGenerationRow(database, generationId);
|
|
1510
|
+
return {
|
|
1511
|
+
generationId: row.generation_id,
|
|
1512
|
+
pendingStages: parsePendingStages(row.pending_stages_json),
|
|
1513
|
+
llmCallsUsed: row.llm_calls_used,
|
|
1514
|
+
tokensUsed: row.tokens_used
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
function readInvocation(database, input) {
|
|
1518
|
+
const row = getInvocationRow(database, input.generationId, input.invocationId);
|
|
1519
|
+
return {
|
|
1520
|
+
llmCallsUsed: row.llm_calls_used,
|
|
1521
|
+
tokensUsed: row.tokens_used
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
function appendJournal(database, jobId, jobType, entry) {
|
|
1525
|
+
database.prepare(`INSERT INTO nodix_rem_journal(
|
|
1526
|
+
job_id, job_type, stage, outcome, pairs_scanned, verdicts, actions_applied, reason,
|
|
1527
|
+
detail, row_id, pair_id
|
|
1528
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(jobId, jobType, entry.stage, entry.outcome, entry.pairsScanned, entry.verdicts, entry.actionsApplied, entry.reason ?? null, entry.detail ?? null, entry.rowId ?? null, entry.pairId ?? null);
|
|
1529
|
+
}
|
|
1530
|
+
function listJournal(database, jobId, jobType) {
|
|
1531
|
+
const operationFilter = jobType === void 0 ? "" : " AND job_type = ?";
|
|
1532
|
+
const parameters = jobType === void 0 ? [jobId] : [jobId, jobType];
|
|
1533
|
+
return database.prepare(`SELECT stage, outcome, pairs_scanned, verdicts, actions_applied, reason, detail,
|
|
1534
|
+
row_id, pair_id
|
|
1535
|
+
FROM nodix_rem_journal WHERE job_id = ?${operationFilter} ORDER BY sequence`).all(...parameters).map((row) => ({
|
|
1536
|
+
stage: row.stage,
|
|
1537
|
+
outcome: row.outcome,
|
|
1538
|
+
pairsScanned: row.pairs_scanned,
|
|
1539
|
+
verdicts: row.verdicts,
|
|
1540
|
+
actionsApplied: row.actions_applied,
|
|
1541
|
+
...row.reason === null ? {} : { reason: row.reason },
|
|
1542
|
+
...row.detail === null ? {} : { detail: row.detail },
|
|
1543
|
+
...row.row_id === null ? {} : { rowId: row.row_id },
|
|
1544
|
+
...row.pair_id === null ? {} : { pairId: row.pair_id }
|
|
1545
|
+
}));
|
|
1546
|
+
}
|
|
1547
|
+
function recordVerdictCheckpoint(database, input) {
|
|
1548
|
+
if (input.verdict !== void 0 && input.verdict !== "keep" && input.verdict !== "replacement" && input.verdict !== "uncertain") throw new Error("verdict must be keep, replacement, or uncertain");
|
|
1549
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_scan_pairs SET
|
|
1550
|
+
claimed_at = ?,
|
|
1551
|
+
checkpoint = ?,
|
|
1552
|
+
verdict = COALESCE(?, verdict),
|
|
1553
|
+
actions_applied = CASE
|
|
1554
|
+
WHEN ? = 'action_applied' THEN 1
|
|
1555
|
+
ELSE actions_applied
|
|
1556
|
+
END
|
|
1557
|
+
WHERE generation_id = ? AND pair_id = ? AND claim_state = 'claimed'
|
|
1558
|
+
AND invocation_id = ?
|
|
1559
|
+
AND CASE checkpoint
|
|
1560
|
+
WHEN 'action_applied' THEN 3
|
|
1561
|
+
WHEN 'verdict_recorded' THEN 2
|
|
1562
|
+
WHEN 'before_llm' THEN 1
|
|
1563
|
+
ELSE 0
|
|
1564
|
+
END <= CASE ?
|
|
1565
|
+
WHEN 'action_applied' THEN 3
|
|
1566
|
+
WHEN 'verdict_recorded' THEN 2
|
|
1567
|
+
WHEN 'before_llm' THEN 1
|
|
1568
|
+
ELSE 0
|
|
1569
|
+
END`).run(input.recordedAt, input.checkpoint, input.verdict ?? null, input.checkpoint, input.generationId, input.pairId, input.invocationId, input.checkpoint)).changes !== 1) throw new Error("verdict pair is not owned by invocation");
|
|
1570
|
+
}
|
|
1571
|
+
function resumeVerdictPair(database, input) {
|
|
1572
|
+
const holderProcessStart = requireProcessStart(input.holderPid);
|
|
1573
|
+
const holderBootId = requireBootId();
|
|
1574
|
+
const pair = getPairRow(database, input.generationId, input.pairId);
|
|
1575
|
+
const holder = getPairClaim(database, input.generationId, input.pairId);
|
|
1576
|
+
if (pair.claim_state !== "claimed" || pair.invocation_id !== input.expectedInvocationId || holder.state !== "active" || holder.claim_token !== input.expectedInvocationId) throw new Error("verdict pair is not owned by invocation");
|
|
1577
|
+
if (!(holder.holder_pid === input.holderPid && holder.holder_process_start === holderProcessStart && holder.holder_boot_id === holderBootId) && isExactProcessAlive(holder.holder_pid, holder.holder_process_start, holder.holder_boot_id, holderBootId)) return {
|
|
1578
|
+
next: "refused",
|
|
1579
|
+
reason: "holder_alive"
|
|
1580
|
+
};
|
|
1581
|
+
const claimToken = input.invocationId;
|
|
1582
|
+
return database.transaction(() => {
|
|
1583
|
+
ensureInvocation(database, input.generationId, claimToken, input.resumedAt);
|
|
1584
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_scan_pairs SET invocation_id = ?, claimed_at = ?
|
|
1585
|
+
WHERE generation_id = ? AND pair_id = ? AND claim_state = 'claimed'
|
|
1586
|
+
AND invocation_id = ?`).run(claimToken, input.resumedAt, input.generationId, input.pairId, input.expectedInvocationId)).changes !== 1) throw new Error("verdict pair is not owned by invocation");
|
|
1587
|
+
if (asRunResult(database.prepare(`UPDATE nodix_rem_pair_claims SET
|
|
1588
|
+
claim_token = ?, holder_pid = ?, holder_process_start = ?,
|
|
1589
|
+
holder_boot_id = ?, claimed_at = ?
|
|
1590
|
+
WHERE generation_id = ? AND pair_id = ? AND state = 'active'
|
|
1591
|
+
AND claim_token = ? AND holder_pid = ? AND holder_process_start = ?
|
|
1592
|
+
AND holder_boot_id IS ?`).run(claimToken, input.holderPid, holderProcessStart, holderBootId, input.resumedAt, input.generationId, input.pairId, input.expectedInvocationId, holder.holder_pid, holder.holder_process_start, holder.holder_boot_id)).changes !== 1) throw new Error("verdict pair has inconsistent durable claim");
|
|
1593
|
+
const resumed = getPairRow(database, input.generationId, input.pairId);
|
|
1594
|
+
if (resumed.checkpoint === "action_applied") return {
|
|
1595
|
+
next: "complete",
|
|
1596
|
+
claimToken
|
|
1597
|
+
};
|
|
1598
|
+
if (resumed.checkpoint === "verdict_recorded") {
|
|
1599
|
+
if (!resumed.verdict) throw new Error("recorded verdict is missing");
|
|
1600
|
+
return {
|
|
1601
|
+
next: "apply_action",
|
|
1602
|
+
verdict: resumed.verdict,
|
|
1603
|
+
claimToken
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
return {
|
|
1607
|
+
next: "call_llm",
|
|
1608
|
+
claimToken
|
|
1609
|
+
};
|
|
1610
|
+
}).immediate();
|
|
1611
|
+
}
|
|
1612
|
+
function readVerdictPair(database, generationId, pairId) {
|
|
1613
|
+
const pair = getPairRow(database, generationId, pairId);
|
|
1614
|
+
return {
|
|
1615
|
+
checkpoint: pair.checkpoint,
|
|
1616
|
+
actionsApplied: pair.actions_applied
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
function getGenerationRow(database, generationId) {
|
|
1620
|
+
const row = database.prepare("SELECT * FROM nodix_rem_scan_generations WHERE generation_id = ?").get(generationId);
|
|
1621
|
+
if (!row) throw new Error("scan generation not found");
|
|
1622
|
+
return row;
|
|
1623
|
+
}
|
|
1624
|
+
function ensureInvocation(database, generationId, invocationId, createdAt) {
|
|
1625
|
+
database.prepare(`INSERT INTO nodix_rem_scan_invocations(generation_id, invocation_id, created_at)
|
|
1626
|
+
VALUES (?, ?, ?) ON CONFLICT(generation_id, invocation_id) DO NOTHING`).run(generationId, invocationId, createdAt);
|
|
1627
|
+
}
|
|
1628
|
+
function getInvocationRow(database, generationId, invocationId) {
|
|
1629
|
+
const row = database.prepare(`SELECT llm_calls_used, tokens_used FROM nodix_rem_scan_invocations
|
|
1630
|
+
WHERE generation_id = ? AND invocation_id = ?`).get(generationId, invocationId);
|
|
1631
|
+
if (!row) throw new Error("scan invocation not found");
|
|
1632
|
+
return row;
|
|
1633
|
+
}
|
|
1634
|
+
function getPairRow(database, generationId, pairId) {
|
|
1635
|
+
const row = database.prepare(`SELECT pair_id, left_row_id, right_row_id, sort_key, checkpoint, verdict,
|
|
1636
|
+
actions_applied, claim_state, invocation_id, budget_reserved, budget_invocation_id,
|
|
1637
|
+
reserved_tokens, attempt_count, progress_state, refusal_reason, inherited_from_generation_id
|
|
1638
|
+
FROM nodix_rem_scan_pairs WHERE generation_id = ? AND pair_id = ?`).get(generationId, pairId);
|
|
1639
|
+
if (!row) throw new Error("verdict pair not found");
|
|
1640
|
+
return row;
|
|
1641
|
+
}
|
|
1642
|
+
function getPairClaim(database, generationId, pairId) {
|
|
1643
|
+
const row = database.prepare(`SELECT claim_token, holder_pid, holder_process_start, holder_boot_id, state
|
|
1644
|
+
FROM nodix_rem_pair_claims WHERE generation_id = ? AND pair_id = ?`).get(generationId, pairId);
|
|
1645
|
+
if (!row) throw new Error("verdict pair has inconsistent durable claim");
|
|
1646
|
+
return row;
|
|
1647
|
+
}
|
|
1648
|
+
function getRowClaim(database, rowId) {
|
|
1649
|
+
const row = database.prepare(`SELECT content_hash, owner, claim_token, holder_pid, holder_process_start,
|
|
1650
|
+
holder_boot_id, state
|
|
1651
|
+
FROM nodix_rem_row_claims WHERE row_id = ?`).get(rowId);
|
|
1652
|
+
if (!row) throw new Error("row claim has inconsistent durable state");
|
|
1653
|
+
return row;
|
|
1654
|
+
}
|
|
1655
|
+
function inspectRowClaim(database, input) {
|
|
1656
|
+
const ownershipRefusal = inspectRowClaimOwnership(database, input);
|
|
1657
|
+
if (ownershipRefusal) return ownershipRefusal;
|
|
1658
|
+
const memory = database.prepare("SELECT content_hash FROM nodix_memories WHERE id = ?").get(input.rowId);
|
|
1659
|
+
if (!memory) return "missing";
|
|
1660
|
+
if (memory.content_hash !== (input.expectedMemoryContentHash ?? input.contentHash)) return "content_changed";
|
|
1661
|
+
}
|
|
1662
|
+
function inspectRowClaimOwnership(database, input) {
|
|
1663
|
+
const ledger = database.prepare("SELECT content_hash, owner FROM nodix_rem_relation_ledger WHERE row_id = ?").get(input.rowId);
|
|
1664
|
+
if (!ledger) return "missing";
|
|
1665
|
+
if (ledger.content_hash !== input.contentHash) return "inconsistent_state";
|
|
1666
|
+
const claim = database.prepare(`SELECT content_hash, owner, claim_token, holder_pid, holder_process_start,
|
|
1667
|
+
holder_boot_id, state
|
|
1668
|
+
FROM nodix_rem_row_claims WHERE row_id = ?`).get(input.rowId);
|
|
1669
|
+
if (!claim) return ledger.owner === "none" ? "claim_changed" : "inconsistent_state";
|
|
1670
|
+
if (claim.claim_token !== input.expectedClaimToken || claim.content_hash !== input.contentHash || claim.owner !== input.owner || claim.state !== "active") return "claim_changed";
|
|
1671
|
+
if (ledger.owner !== input.owner) return "inconsistent_state";
|
|
1672
|
+
}
|
|
1673
|
+
function requireProcessStart(pid) {
|
|
1674
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("holder pid must be positive");
|
|
1675
|
+
const marker = readProcessStart(pid);
|
|
1676
|
+
if (!marker) throw new Error("holder process is not running");
|
|
1677
|
+
return marker;
|
|
1678
|
+
}
|
|
1679
|
+
function isExactProcessAlive(pid, expectedStart, expectedBootId, currentBootId) {
|
|
1680
|
+
if (expectedBootId !== currentBootId) return false;
|
|
1681
|
+
const processStat = readProcessStat(pid);
|
|
1682
|
+
return processStat !== void 0 && processStat.start === expectedStart && processStat.state !== "Z" && processStat.state !== "X" && processStat.state !== "x";
|
|
1683
|
+
}
|
|
1684
|
+
function readProcessStart(pid) {
|
|
1685
|
+
return readProcessStat(pid)?.start;
|
|
1686
|
+
}
|
|
1687
|
+
function readProcessStat(pid) {
|
|
1688
|
+
try {
|
|
1689
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
1690
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/);
|
|
1691
|
+
const state = fields[0];
|
|
1692
|
+
const start = fields[19];
|
|
1693
|
+
if (!state) throw new Error(`process ${pid} stat is missing its state`);
|
|
1694
|
+
if (!start) throw new Error(`process ${pid} stat is missing its start marker`);
|
|
1695
|
+
return {
|
|
1696
|
+
state,
|
|
1697
|
+
start
|
|
1698
|
+
};
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
if (typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ESRCH")) return;
|
|
1701
|
+
throw error;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function requireBootId() {
|
|
1705
|
+
const bootId = readFileSync(bootIdPath, "utf8").trim();
|
|
1706
|
+
if (!bootId) throw new Error("kernel boot id is empty");
|
|
1707
|
+
return bootId;
|
|
1708
|
+
}
|
|
1709
|
+
function persistPendingStage(database, generationId, stage) {
|
|
1710
|
+
const row = getGenerationRow(database, generationId);
|
|
1711
|
+
const pending = new Set(parsePendingStages(row.pending_stages_json));
|
|
1712
|
+
pending.add(stage);
|
|
1713
|
+
database.prepare("UPDATE nodix_rem_scan_generations SET pending_stages_json = ? WHERE generation_id = ?").run(JSON.stringify(Array.from(pending)), generationId);
|
|
1714
|
+
}
|
|
1715
|
+
function parsePendingStages(value) {
|
|
1716
|
+
const parsed = JSON.parse(value);
|
|
1717
|
+
if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string")) throw new Error("invalid pending stage record");
|
|
1718
|
+
return parsed;
|
|
1719
|
+
}
|
|
1720
|
+
function asRunResult(value) {
|
|
1721
|
+
if (typeof value !== "object" || value === null || !("changes" in value) || typeof value.changes !== "number") throw new Error("database run result is missing changes");
|
|
1722
|
+
return { changes: value.changes };
|
|
1723
|
+
}
|
|
1724
|
+
function parseBudgetReservation(value) {
|
|
1725
|
+
if (typeof value !== "object" || value === null || !("status" in value)) throw new Error("budget transaction returned an invalid result");
|
|
1726
|
+
if (value.status === "reserved" && "tokens" in value && typeof value.tokens === "number" && "idempotencyKey" in value && typeof value.idempotencyKey === "string") return {
|
|
1727
|
+
status: "reserved",
|
|
1728
|
+
tokens: value.tokens,
|
|
1729
|
+
idempotencyKey: value.idempotencyKey
|
|
1730
|
+
};
|
|
1731
|
+
if (value.status === "pending" && "reason" in value && (value.reason === "invocation_row_missing" || value.reason === "tokenizer_unavailable")) return {
|
|
1732
|
+
status: "pending",
|
|
1733
|
+
reason: value.reason
|
|
1734
|
+
};
|
|
1735
|
+
throw new Error("budget transaction returned an invalid result");
|
|
1736
|
+
}
|
|
1737
|
+
//#endregion
|
|
1738
|
+
//#region src/engine/rem/replace-clause-verdict.ts
|
|
1739
|
+
function buildReplaceEligibleClauses(older, newer) {
|
|
1740
|
+
return [...splitExactClauses(older).map((clause) => ({
|
|
1741
|
+
origin: "older",
|
|
1742
|
+
...clause
|
|
1743
|
+
})), ...splitExactClauses(newer).map((clause) => ({
|
|
1744
|
+
origin: "newer",
|
|
1745
|
+
...clause
|
|
1746
|
+
}))];
|
|
1747
|
+
}
|
|
1748
|
+
function renderReplaceClauseVerdictPrompt(clauses) {
|
|
1749
|
+
return [
|
|
1750
|
+
"Judge whether the newer memory replaces the older memory.",
|
|
1751
|
+
"replacement: the newer memory supersedes the older memory, so one or more older clauses should retire.",
|
|
1752
|
+
"keep: the two memories should remain independently active because they are compatible, describe different facts, or the newer memory does not establish a replacement.",
|
|
1753
|
+
"uncertain: you cannot determine whether the newer memory replaces the older memory from the supplied content; take no action.",
|
|
1754
|
+
"Return JSON only: {\"verdict\":\"replacement|keep|uncertain\",\"retiring_clause_indices\":[0]}.",
|
|
1755
|
+
"Use only the zero-based indices in the supplied eligible clauses.",
|
|
1756
|
+
"A retiring clause must originate from the older memory. Do not produce text or explanations.",
|
|
1757
|
+
`Eligible clauses: ${JSON.stringify(clauses)}`
|
|
1758
|
+
].join("\n\n");
|
|
1759
|
+
}
|
|
1760
|
+
function parseReplaceClauseVerdict(response, clauses) {
|
|
1761
|
+
if (!isRecord(response)) return invalidVerdict();
|
|
1762
|
+
if (!hasOnlyKeys(response, ["verdict", "retiring_clause_indices"])) return invalidVerdict();
|
|
1763
|
+
const verdict = response["verdict"];
|
|
1764
|
+
const indices = response["retiring_clause_indices"];
|
|
1765
|
+
if (verdict !== "replacement" && verdict !== "keep" && verdict !== "uncertain" || !Array.isArray(indices) || !indices.every((index) => Number.isInteger(index) && index >= 0 && index < clauses.length) || new Set(indices).size !== indices.length || indices.some((index) => clauses[index]?.origin !== "older")) return invalidVerdict();
|
|
1766
|
+
return {
|
|
1767
|
+
parseState: "valid",
|
|
1768
|
+
verdict,
|
|
1769
|
+
retiringClauseIndices: indices
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
function arbitrateReplaceVerdicts(pairVerdict, clauseVerdict) {
|
|
1773
|
+
if (pairVerdict !== "replacement") return { outcome: "no-action" };
|
|
1774
|
+
if (clauseVerdict.parseState === "invalid") return {
|
|
1775
|
+
outcome: "refused",
|
|
1776
|
+
reason: "clause_parse_failed"
|
|
1777
|
+
};
|
|
1778
|
+
if (clauseVerdict.retiringClauseIndices.length === 0) return {
|
|
1779
|
+
outcome: "refused",
|
|
1780
|
+
reason: "empty_retiring_set"
|
|
1781
|
+
};
|
|
1782
|
+
return {
|
|
1783
|
+
outcome: "proceed",
|
|
1784
|
+
retiringClauseIndices: clauseVerdict.retiringClauseIndices
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
function invalidVerdict() {
|
|
1788
|
+
return {
|
|
1789
|
+
parseState: "invalid",
|
|
1790
|
+
verdict: "uncertain",
|
|
1791
|
+
retiringClauseIndices: []
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function isRecord(value) {
|
|
1795
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1796
|
+
}
|
|
1797
|
+
function hasOnlyKeys(value, expected) {
|
|
1798
|
+
return Object.keys(value).length === expected.length && expected.every((key) => key in value);
|
|
1799
|
+
}
|
|
1800
|
+
//#endregion
|
|
1801
|
+
//#region src/engine/rem/replace-coverage.ts
|
|
1802
|
+
const replaceCoverageSchema = z.object({ atoms: z.array(z.object({
|
|
1803
|
+
clause_index: z.number().int().nonnegative(),
|
|
1804
|
+
class: z.enum([
|
|
1805
|
+
"current-fact",
|
|
1806
|
+
"retired-fact",
|
|
1807
|
+
"event-fact"
|
|
1808
|
+
]),
|
|
1809
|
+
status: z.enum([
|
|
1810
|
+
"covered",
|
|
1811
|
+
"uncovered",
|
|
1812
|
+
"undetermined"
|
|
1813
|
+
])
|
|
1814
|
+
}).strict()) }).strict();
|
|
1815
|
+
function renderReplaceCoveragePrompt(input) {
|
|
1816
|
+
const olderClauses = buildReplaceEligibleClauses(input.older, input.newer).filter((clause) => clause.origin === "older");
|
|
1817
|
+
return [
|
|
1818
|
+
"Classify whether every older-memory clause remains covered after replacement.",
|
|
1819
|
+
"Return JSON only: {\"atoms\":[{\"clause_index\":0,\"class\":\"current-fact|retired-fact|event-fact\",\"status\":\"covered|uncovered|undetermined\"}]}.",
|
|
1820
|
+
"Return exactly one atom for each older clause. Prefer undetermined over guessing.",
|
|
1821
|
+
"Covered means the newer memory carries the older clause's information. A newer value for the same fact counts as covered: the older value was replaced, not lost.",
|
|
1822
|
+
"Uncovered means the older clause also holds a separate fact that the newer memory neither updates nor restates, so closing the older row would lose it.",
|
|
1823
|
+
"Class describes what kind of assertion the older clause makes, independently of Retiring older-clause indices. Those indices do not determine class or status and do not excuse information loss; evaluate every older clause.",
|
|
1824
|
+
`Older clauses: ${JSON.stringify(olderClauses)}`,
|
|
1825
|
+
`Newer memory: ${JSON.stringify(input.newer)}`,
|
|
1826
|
+
`Retiring older-clause indices: ${JSON.stringify(input.retiringClauseIndices)}`
|
|
1827
|
+
].join("\n\n");
|
|
1828
|
+
}
|
|
1829
|
+
function parseReplaceCoverageAtoms(value) {
|
|
1830
|
+
const parsed = replaceCoverageSchema.safeParse(value);
|
|
1831
|
+
if (!parsed.success) return void 0;
|
|
1832
|
+
return parsed.data.atoms.map((atom) => ({
|
|
1833
|
+
clauseIndex: atom.clause_index,
|
|
1834
|
+
class: atom.class,
|
|
1835
|
+
status: atom.status
|
|
1836
|
+
}));
|
|
1837
|
+
}
|
|
1838
|
+
/** Validates model-classified coverage against the code-owned older-clause reference set. */
|
|
1839
|
+
/**
|
|
1840
|
+
* Every refusal raised after the observation list exists carries what was seen up to that point.
|
|
1841
|
+
* Owner ruling 2026-08-13: the count is of conditions detected. Refusing on one condition while
|
|
1842
|
+
* silently dropping another the same pair exhibited is how a deletion stays unmeasurable.
|
|
1843
|
+
*/
|
|
1844
|
+
function refuseWith(reason, atoms, observations, detail) {
|
|
1845
|
+
return {
|
|
1846
|
+
decision: "refuse",
|
|
1847
|
+
reason,
|
|
1848
|
+
atoms,
|
|
1849
|
+
...detail === void 0 ? {} : { detail },
|
|
1850
|
+
...observations.length === 0 ? {} : { observations: [...observations] }
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
function decideReplaceCoverage(input) {
|
|
1854
|
+
const atoms = input.atoms.map((atom) => ({ ...atom }));
|
|
1855
|
+
if (input.older.trim().length === 0) return {
|
|
1856
|
+
decision: "refuse",
|
|
1857
|
+
reason: "empty_older_clause_set",
|
|
1858
|
+
atoms
|
|
1859
|
+
};
|
|
1860
|
+
const olderClauses = buildReplaceEligibleClauses(input.older, input.newer).filter((clause) => clause.origin === "older");
|
|
1861
|
+
if (olderClauses.length === 0) return {
|
|
1862
|
+
decision: "refuse",
|
|
1863
|
+
reason: "empty_older_clause_set",
|
|
1864
|
+
atoms
|
|
1865
|
+
};
|
|
1866
|
+
const observations = [];
|
|
1867
|
+
if (atoms.length !== olderClauses.length || new Set(atoms.map((atom) => atom.clauseIndex)).size !== atoms.length || atoms.some((atom) => atom.clauseIndex < 0 || atom.clauseIndex >= olderClauses.length)) observations.push({
|
|
1868
|
+
code: "incomplete_clause_accounting",
|
|
1869
|
+
detail: `atoms=${atoms.length} clauses=${olderClauses.length}`
|
|
1870
|
+
});
|
|
1871
|
+
const retiring = new Set(input.retiringClauseIndices);
|
|
1872
|
+
for (const atom of atoms) {
|
|
1873
|
+
if (atom.status !== "covered") return refuseWith(`atom_${atom.status}`, atoms, observations);
|
|
1874
|
+
if (atom.class === "retired-fact" && !retiring.has(atom.clauseIndex)) observations.push({
|
|
1875
|
+
code: "retiring_side_mismatch",
|
|
1876
|
+
detail: `clause_index=${atom.clauseIndex}`
|
|
1877
|
+
});
|
|
1878
|
+
if (atom.class === "retired-fact" && input.rawCandidateEvidence !== void 0) {
|
|
1879
|
+
const clause = olderClauses[atom.clauseIndex];
|
|
1880
|
+
if (clause !== void 0 && !input.rawCandidateEvidence.includes(clause.value)) {
|
|
1881
|
+
atom.status = "undetermined";
|
|
1882
|
+
return refuseWith("retired_fact_not_in_evidence", atoms, observations);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const certified = new Set(atoms.filter((atom) => atom.status === "covered").map((atom) => atom.clauseIndex));
|
|
1887
|
+
const uncertifiedClauses = olderClauses.map((clause, index) => certified.has(index) || retiring.has(index) ? void 0 : clause.value).filter((value) => value !== void 0);
|
|
1888
|
+
if (atoms.some((atom) => atom.class === "event-fact")) {
|
|
1889
|
+
if (input.carrier === void 0) return refuseWith("event_retrievability_port_missing", atoms, observations);
|
|
1890
|
+
return decideEventFacts(input.carrier, atoms, observations, uncertifiedClauses);
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
decision: "allow",
|
|
1894
|
+
atoms,
|
|
1895
|
+
...observations.length === 0 ? {} : { observations },
|
|
1896
|
+
...uncertifiedClauses.length === 0 ? {} : { uncertifiedClauses }
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
async function decideEventFacts(carrier, atoms, observations, uncertifiedClauses) {
|
|
1900
|
+
const state = await carrier.carrierState();
|
|
1901
|
+
if (!state.retained) return refuseWith("event_not_retrievable", atoms, observations, state.fault);
|
|
1902
|
+
return {
|
|
1903
|
+
decision: "allow",
|
|
1904
|
+
atoms,
|
|
1905
|
+
...observations.length === 0 ? {} : { observations: [...observations] },
|
|
1906
|
+
...uncertifiedClauses.length === 0 ? {} : { uncertifiedClauses: [...uncertifiedClauses] }
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
//#endregion
|
|
1910
|
+
//#region src/engine/rem/rem-update-judgment.ts
|
|
1911
|
+
const judgmentSchema = z.object({
|
|
1912
|
+
proposed_current: z.string(),
|
|
1913
|
+
retired_values: z.array(z.string())
|
|
1914
|
+
}).strict();
|
|
1915
|
+
const relationJudgmentSchema = z.object({
|
|
1916
|
+
supersedes: z.boolean(),
|
|
1917
|
+
retires_anything: z.boolean(),
|
|
1918
|
+
supersedes_everything: z.boolean()
|
|
1919
|
+
}).strict();
|
|
1920
|
+
const retirementTargetSchema = z.object({ target_row_ids: z.array(z.string().min(1)) }).strict();
|
|
1921
|
+
const verificationSchema = z.object({
|
|
1922
|
+
faithful: z.boolean(),
|
|
1923
|
+
retired_absent: z.boolean(),
|
|
1924
|
+
all_facts_accounted: z.boolean()
|
|
1925
|
+
}).strict();
|
|
1926
|
+
function renderRemUpdateJudgmentPrompt(input) {
|
|
1927
|
+
return [
|
|
1928
|
+
input.judgmentSkill,
|
|
1929
|
+
"Task: REM source rewrite.",
|
|
1930
|
+
"Return exactly one JSON object and no prose: {\"proposed_current\":\"\",\"retired_values\":[]}.",
|
|
1931
|
+
"Use exactly those two fields and no additional fields.",
|
|
1932
|
+
"proposed_current must be a string.",
|
|
1933
|
+
"retired_values must be an array of strings.",
|
|
1934
|
+
`Source row: ${JSON.stringify(input.source)}`,
|
|
1935
|
+
...input.enumeratedMembers === void 0 ? [] : [`Code-enumerated members: ${JSON.stringify(input.enumeratedMembers)}`],
|
|
1936
|
+
...input.supersedingText === void 0 ? [] : [`Newer record that supersedes part of this row: ${JSON.stringify(input.supersedingText)}`]
|
|
1937
|
+
].join("\n\n");
|
|
1938
|
+
}
|
|
1939
|
+
function renderRemUpdateRelationJudgmentPrompt(input) {
|
|
1940
|
+
return [
|
|
1941
|
+
input.judgmentSkill,
|
|
1942
|
+
"Task: REM relation judgment.",
|
|
1943
|
+
"Return exactly one JSON object and no prose: {\"supersedes\":false,\"retires_anything\":false,\"supersedes_everything\":false}.",
|
|
1944
|
+
"Use exactly those three fields and no additional fields.",
|
|
1945
|
+
"supersedes must be a boolean.",
|
|
1946
|
+
"retires_anything must be a boolean.",
|
|
1947
|
+
"supersedes_everything must be a boolean.",
|
|
1948
|
+
`Record under judgment: ${JSON.stringify(input.rowText)}`,
|
|
1949
|
+
`Older records: ${JSON.stringify(input.predecessorTexts)}`,
|
|
1950
|
+
`Other related records: ${JSON.stringify(input.successorTexts)}`
|
|
1951
|
+
].join("\n\n");
|
|
1952
|
+
}
|
|
1953
|
+
function renderRemRetirementTargetPrompt(input) {
|
|
1954
|
+
return [
|
|
1955
|
+
input.judgmentSkill,
|
|
1956
|
+
"Task: REM retirement target judgment.",
|
|
1957
|
+
"Return exactly one JSON object and no prose: {\"target_row_ids\":[]}.",
|
|
1958
|
+
"Use exactly that field and no additional fields.",
|
|
1959
|
+
"target_row_ids must contain only ids from Candidate rows and must not contain duplicates.",
|
|
1960
|
+
`Nominated row: ${JSON.stringify(input.nominatedRow)}`,
|
|
1961
|
+
`Candidate rows: ${JSON.stringify(input.candidateRows)}`
|
|
1962
|
+
].join("\n\n");
|
|
1963
|
+
}
|
|
1964
|
+
function renderRemUpdateVerificationPrompt(input) {
|
|
1965
|
+
return [
|
|
1966
|
+
input.judgmentSkill,
|
|
1967
|
+
"Task: REM rewrite verification.",
|
|
1968
|
+
"Return exactly one JSON object and no prose: {\"faithful\":false,\"retired_absent\":false,\"all_facts_accounted\":false}.",
|
|
1969
|
+
"Use exactly those three fields and no additional fields.",
|
|
1970
|
+
...input.retractionText === void 0 ? [] : ["Judge whether the proposed rewrite follows from the relation between the original row and the retraction row."],
|
|
1971
|
+
`Original row: ${JSON.stringify(input.source)}`,
|
|
1972
|
+
...input.supersedingText === void 0 ? [] : [`Newer record: ${JSON.stringify(input.supersedingText)}`],
|
|
1973
|
+
...input.retractionText === void 0 ? [] : [`Retraction row: ${JSON.stringify(input.retractionText)}`],
|
|
1974
|
+
`Proposed rewrite: ${JSON.stringify(input.proposedCurrent)}`,
|
|
1975
|
+
`Retired values: ${JSON.stringify(input.retiredValues)}`
|
|
1976
|
+
].join("\n\n");
|
|
1977
|
+
}
|
|
1978
|
+
function renderRemClauseCarryPrompt(input) {
|
|
1979
|
+
return [
|
|
1980
|
+
input.judgmentSkill,
|
|
1981
|
+
"Task: REM clause carry judgment.",
|
|
1982
|
+
"Return exactly one JSON object and no prose: {\"already_current\":[true,false]}.",
|
|
1983
|
+
"Use exactly that field and no additional fields.",
|
|
1984
|
+
"already_current must contain exactly one boolean per numbered clause, in the same order.",
|
|
1985
|
+
`Survivor text: ${JSON.stringify(input.survivorText)}`,
|
|
1986
|
+
...input.clauses.map((clause, index) => `Clause ${index + 1}: ${JSON.stringify(clause)}`)
|
|
1987
|
+
].join("\n\n");
|
|
1988
|
+
}
|
|
1989
|
+
function decideRemUpdateRelationFromReply(reply) {
|
|
1990
|
+
const json = extractJsonFromResponse(reply);
|
|
1991
|
+
if (json === null) return {
|
|
1992
|
+
outcome: "refuse",
|
|
1993
|
+
reason: "model_response_invalid"
|
|
1994
|
+
};
|
|
1995
|
+
let value;
|
|
1996
|
+
try {
|
|
1997
|
+
value = JSON.parse(json);
|
|
1998
|
+
} catch {
|
|
1999
|
+
return {
|
|
2000
|
+
outcome: "refuse",
|
|
2001
|
+
reason: "model_response_invalid"
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
const parsed = relationJudgmentSchema.safeParse(value);
|
|
2005
|
+
if (!parsed.success) return {
|
|
2006
|
+
outcome: "refuse",
|
|
2007
|
+
reason: "model_response_invalid"
|
|
2008
|
+
};
|
|
2009
|
+
if (!parsed.data.supersedes || !parsed.data.retires_anything) return {
|
|
2010
|
+
outcome: "refuse",
|
|
2011
|
+
reason: "no_retired_fact"
|
|
2012
|
+
};
|
|
2013
|
+
return {
|
|
2014
|
+
outcome: "allow",
|
|
2015
|
+
supersedesEverything: parsed.data.supersedes_everything
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
function decideRemRetirementTargetFromReply(reply, offeredRowIds) {
|
|
2019
|
+
const json = extractJsonFromResponse(reply);
|
|
2020
|
+
if (json === null) return {
|
|
2021
|
+
outcome: "refuse",
|
|
2022
|
+
reason: "model_response_invalid"
|
|
2023
|
+
};
|
|
2024
|
+
let value;
|
|
2025
|
+
try {
|
|
2026
|
+
value = JSON.parse(json);
|
|
2027
|
+
} catch {
|
|
2028
|
+
return {
|
|
2029
|
+
outcome: "refuse",
|
|
2030
|
+
reason: "model_response_invalid"
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
const parsed = retirementTargetSchema.safeParse(value);
|
|
2034
|
+
if (!parsed.success) return {
|
|
2035
|
+
outcome: "refuse",
|
|
2036
|
+
reason: "model_response_invalid"
|
|
2037
|
+
};
|
|
2038
|
+
const targetRowIds = new Set(parsed.data.target_row_ids);
|
|
2039
|
+
if (targetRowIds.size !== parsed.data.target_row_ids.length) return {
|
|
2040
|
+
outcome: "refuse",
|
|
2041
|
+
reason: "model_response_invalid"
|
|
2042
|
+
};
|
|
2043
|
+
if ([...targetRowIds].some((rowId) => !offeredRowIds.has(rowId))) return {
|
|
2044
|
+
outcome: "refuse",
|
|
2045
|
+
reason: "row_id_not_offered"
|
|
2046
|
+
};
|
|
2047
|
+
return {
|
|
2048
|
+
outcome: "allow",
|
|
2049
|
+
targetRowIds: [...targetRowIds]
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
function decideRemUpdateFromReply(reply) {
|
|
2053
|
+
const parsed = parseJudgment(reply);
|
|
2054
|
+
if (parsed === void 0) return {
|
|
2055
|
+
outcome: "refuse",
|
|
2056
|
+
reason: "model_response_invalid"
|
|
2057
|
+
};
|
|
2058
|
+
if (parsed.retiredValues.length === 0) return {
|
|
2059
|
+
outcome: "refuse",
|
|
2060
|
+
reason: "no_retired_fact"
|
|
2061
|
+
};
|
|
2062
|
+
if (parsed.proposedCurrent.trim().length === 0) return {
|
|
2063
|
+
outcome: "refuse",
|
|
2064
|
+
reason: "no_surviving_remainder"
|
|
2065
|
+
};
|
|
2066
|
+
return {
|
|
2067
|
+
outcome: "verify",
|
|
2068
|
+
...parsed
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
function decideRemUpdateVerification(reply) {
|
|
2072
|
+
const json = extractJsonFromResponse(reply);
|
|
2073
|
+
if (json === null) return {
|
|
2074
|
+
outcome: "refuse",
|
|
2075
|
+
reason: "model_response_invalid"
|
|
2076
|
+
};
|
|
2077
|
+
let value;
|
|
2078
|
+
try {
|
|
2079
|
+
value = JSON.parse(json);
|
|
2080
|
+
} catch {
|
|
2081
|
+
return {
|
|
2082
|
+
outcome: "refuse",
|
|
2083
|
+
reason: "model_response_invalid"
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
const parsed = verificationSchema.safeParse(value);
|
|
2087
|
+
if (!parsed.success) return {
|
|
2088
|
+
outcome: "refuse",
|
|
2089
|
+
reason: "model_response_invalid"
|
|
2090
|
+
};
|
|
2091
|
+
if (!parsed.data.faithful) return {
|
|
2092
|
+
outcome: "refuse",
|
|
2093
|
+
reason: "proposal_not_faithful"
|
|
2094
|
+
};
|
|
2095
|
+
if (!parsed.data.retired_absent) return {
|
|
2096
|
+
outcome: "refuse",
|
|
2097
|
+
reason: "retired_value_survives"
|
|
2098
|
+
};
|
|
2099
|
+
if (!parsed.data.all_facts_accounted) return {
|
|
2100
|
+
outcome: "refuse",
|
|
2101
|
+
reason: "surviving_fact_lost"
|
|
2102
|
+
};
|
|
2103
|
+
return { outcome: "apply" };
|
|
2104
|
+
}
|
|
2105
|
+
function decideRemClauseCarry(reply, clauseCount) {
|
|
2106
|
+
if (!Number.isSafeInteger(clauseCount) || clauseCount < 0) return {
|
|
2107
|
+
outcome: "refuse",
|
|
2108
|
+
reason: "model_response_invalid"
|
|
2109
|
+
};
|
|
2110
|
+
const json = extractJsonFromResponse(reply);
|
|
2111
|
+
if (json === null) return {
|
|
2112
|
+
outcome: "refuse",
|
|
2113
|
+
reason: "model_response_invalid"
|
|
2114
|
+
};
|
|
2115
|
+
let value;
|
|
2116
|
+
try {
|
|
2117
|
+
value = JSON.parse(json);
|
|
2118
|
+
} catch {
|
|
2119
|
+
return {
|
|
2120
|
+
outcome: "refuse",
|
|
2121
|
+
reason: "model_response_invalid"
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
const parsed = z.object({ already_current: z.array(z.boolean()).length(clauseCount) }).strict().safeParse(value);
|
|
2125
|
+
if (!parsed.success) return {
|
|
2126
|
+
outcome: "refuse",
|
|
2127
|
+
reason: "model_response_invalid"
|
|
2128
|
+
};
|
|
2129
|
+
return {
|
|
2130
|
+
outcome: "decided",
|
|
2131
|
+
alreadyCurrent: parsed.data.already_current
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
function parseJudgment(reply) {
|
|
2135
|
+
const json = extractJsonFromResponse(reply);
|
|
2136
|
+
if (json === null) return void 0;
|
|
2137
|
+
let value;
|
|
2138
|
+
try {
|
|
2139
|
+
value = JSON.parse(json);
|
|
2140
|
+
} catch {
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
const parsed = judgmentSchema.safeParse(value);
|
|
2144
|
+
if (!parsed.success) return void 0;
|
|
2145
|
+
return {
|
|
2146
|
+
proposedCurrent: parsed.data.proposed_current,
|
|
2147
|
+
retiredValues: parsed.data.retired_values
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
//#endregion
|
|
2151
|
+
//#region src/engine/rem/runner.ts
|
|
2152
|
+
async function runRemStages(input) {
|
|
2153
|
+
const results = {};
|
|
2154
|
+
for (const stage of input.stages) {
|
|
2155
|
+
let entry;
|
|
2156
|
+
try {
|
|
2157
|
+
const observed = await stage.run();
|
|
2158
|
+
results[stage.name] = "done";
|
|
2159
|
+
entry = {
|
|
2160
|
+
...journal(stage.name, "done"),
|
|
2161
|
+
...observed
|
|
2162
|
+
};
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2165
|
+
input.onStageError?.({
|
|
2166
|
+
stage: stage.name,
|
|
2167
|
+
error
|
|
2168
|
+
});
|
|
2169
|
+
results[stage.name] = "failed";
|
|
2170
|
+
entry = {
|
|
2171
|
+
...journal(stage.name, "failed"),
|
|
2172
|
+
reason
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
try {
|
|
2176
|
+
await input.repository.appendJournal(input.jobId, input.jobType, entry);
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
createLogger("sno-station-mem:rem-runner").error("rem.stage.journal.failed", {
|
|
2179
|
+
error,
|
|
2180
|
+
stage: stage.name
|
|
2181
|
+
}, {
|
|
2182
|
+
event_name: "rem.stage.journal.failed",
|
|
2183
|
+
file: "packages/memory/src/engine/rem/runner.ts",
|
|
2184
|
+
function: "runRemStages",
|
|
2185
|
+
site_id: "rem.stage.journal.failed"
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return results;
|
|
2190
|
+
}
|
|
2191
|
+
function journal(stage, outcome) {
|
|
2192
|
+
return {
|
|
2193
|
+
stage,
|
|
2194
|
+
outcome,
|
|
2195
|
+
pairsScanned: 0,
|
|
2196
|
+
verdicts: 0,
|
|
2197
|
+
actionsApplied: 0
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
//#endregion
|
|
2201
|
+
//#region src/engine/rem/schema.ts
|
|
2202
|
+
const REM_SCHEMA_SQL = `
|
|
2203
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_relation_ledger (
|
|
2204
|
+
row_id TEXT PRIMARY KEY,
|
|
2205
|
+
content_hash TEXT NOT NULL,
|
|
2206
|
+
state TEXT NOT NULL CHECK (state IN ('transition', 'stale-current', 'pure-negation', 'ambiguous')),
|
|
2207
|
+
owner TEXT NOT NULL DEFAULT 'none' CHECK (owner IN ('restate', 'verdict', 'none')),
|
|
2208
|
+
claim_ts TEXT,
|
|
2209
|
+
classified_at TEXT NOT NULL
|
|
2210
|
+
);
|
|
2211
|
+
|
|
2212
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_row_claims (
|
|
2213
|
+
row_id TEXT PRIMARY KEY,
|
|
2214
|
+
content_hash TEXT NOT NULL,
|
|
2215
|
+
owner TEXT NOT NULL CHECK (owner IN ('restate', 'verdict')),
|
|
2216
|
+
claim_token TEXT NOT NULL,
|
|
2217
|
+
holder_pid INTEGER NOT NULL CHECK (holder_pid > 0),
|
|
2218
|
+
holder_process_start TEXT NOT NULL,
|
|
2219
|
+
holder_boot_id TEXT NOT NULL,
|
|
2220
|
+
state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active', 'completed')),
|
|
2221
|
+
claimed_at TEXT NOT NULL,
|
|
2222
|
+
completed_at TEXT,
|
|
2223
|
+
FOREIGN KEY (row_id) REFERENCES nodix_rem_relation_ledger(row_id)
|
|
2224
|
+
);
|
|
2225
|
+
|
|
2226
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_scan_generations (
|
|
2227
|
+
generation_id TEXT PRIMARY KEY,
|
|
2228
|
+
corpus_snapshot_hash TEXT NOT NULL,
|
|
2229
|
+
pairing_config_hash TEXT NOT NULL,
|
|
2230
|
+
max_llm_calls INTEGER NOT NULL CHECK (max_llm_calls >= 0),
|
|
2231
|
+
max_tokens INTEGER NOT NULL CHECK (max_tokens >= 0),
|
|
2232
|
+
llm_calls_used INTEGER NOT NULL DEFAULT 0,
|
|
2233
|
+
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
2234
|
+
pending_stages_json TEXT NOT NULL DEFAULT '[]'
|
|
2235
|
+
);
|
|
2236
|
+
|
|
2237
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_scan_pairs (
|
|
2238
|
+
generation_id TEXT NOT NULL,
|
|
2239
|
+
pair_id TEXT NOT NULL,
|
|
2240
|
+
left_row_id TEXT NOT NULL,
|
|
2241
|
+
right_row_id TEXT NOT NULL,
|
|
2242
|
+
sort_key TEXT NOT NULL,
|
|
2243
|
+
claim_state TEXT NOT NULL DEFAULT 'unvisited'
|
|
2244
|
+
CHECK (claim_state IN ('unvisited', 'claimed', 'done')),
|
|
2245
|
+
invocation_id TEXT,
|
|
2246
|
+
claimed_at TEXT,
|
|
2247
|
+
checkpoint TEXT,
|
|
2248
|
+
verdict TEXT,
|
|
2249
|
+
actions_applied INTEGER NOT NULL DEFAULT 0 CHECK (actions_applied IN (0, 1)),
|
|
2250
|
+
budget_reserved INTEGER NOT NULL DEFAULT 0 CHECK (budget_reserved IN (0, 1)),
|
|
2251
|
+
budget_invocation_id TEXT,
|
|
2252
|
+
reserved_tokens INTEGER NOT NULL DEFAULT 0 CHECK (reserved_tokens >= 0),
|
|
2253
|
+
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
|
2254
|
+
progress_state TEXT NOT NULL DEFAULT 'pending'
|
|
2255
|
+
CHECK (progress_state IN ('pending', 'refused', 'closed', 'exhausted')),
|
|
2256
|
+
refusal_reason TEXT,
|
|
2257
|
+
inherited_from_generation_id TEXT,
|
|
2258
|
+
PRIMARY KEY (generation_id, pair_id),
|
|
2259
|
+
FOREIGN KEY (generation_id) REFERENCES nodix_rem_scan_generations(generation_id)
|
|
2260
|
+
);
|
|
2261
|
+
CREATE INDEX IF NOT EXISTS rem_scan_pairs_cursor
|
|
2262
|
+
ON nodix_rem_scan_pairs(generation_id, claim_state, sort_key, pair_id);
|
|
2263
|
+
|
|
2264
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_scan_invocations (
|
|
2265
|
+
generation_id TEXT NOT NULL,
|
|
2266
|
+
invocation_id TEXT NOT NULL,
|
|
2267
|
+
llm_calls_used INTEGER NOT NULL DEFAULT 0,
|
|
2268
|
+
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
2269
|
+
created_at TEXT NOT NULL,
|
|
2270
|
+
PRIMARY KEY (generation_id, invocation_id),
|
|
2271
|
+
FOREIGN KEY (generation_id) REFERENCES nodix_rem_scan_generations(generation_id)
|
|
2272
|
+
);
|
|
2273
|
+
|
|
2274
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_pair_stage_budgets (
|
|
2275
|
+
generation_id TEXT NOT NULL,
|
|
2276
|
+
pair_id TEXT NOT NULL,
|
|
2277
|
+
stage TEXT NOT NULL,
|
|
2278
|
+
invocation_id TEXT NOT NULL,
|
|
2279
|
+
reserved_tokens INTEGER NOT NULL CHECK (reserved_tokens >= 0),
|
|
2280
|
+
idempotency_key TEXT NOT NULL,
|
|
2281
|
+
PRIMARY KEY (generation_id, pair_id, stage),
|
|
2282
|
+
FOREIGN KEY (generation_id, pair_id) REFERENCES nodix_rem_scan_pairs(generation_id, pair_id),
|
|
2283
|
+
FOREIGN KEY (generation_id, invocation_id) REFERENCES nodix_rem_scan_invocations(generation_id, invocation_id)
|
|
2284
|
+
);
|
|
2285
|
+
|
|
2286
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_pair_claims (
|
|
2287
|
+
generation_id TEXT NOT NULL,
|
|
2288
|
+
pair_id TEXT NOT NULL,
|
|
2289
|
+
claim_token TEXT NOT NULL,
|
|
2290
|
+
holder_pid INTEGER NOT NULL CHECK (holder_pid > 0),
|
|
2291
|
+
holder_process_start TEXT NOT NULL,
|
|
2292
|
+
holder_boot_id TEXT NOT NULL,
|
|
2293
|
+
state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active', 'completed')),
|
|
2294
|
+
claimed_at TEXT NOT NULL,
|
|
2295
|
+
completed_at TEXT,
|
|
2296
|
+
PRIMARY KEY (generation_id, pair_id),
|
|
2297
|
+
FOREIGN KEY (generation_id, pair_id) REFERENCES nodix_rem_scan_pairs(generation_id, pair_id)
|
|
2298
|
+
);
|
|
2299
|
+
|
|
2300
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_journal (
|
|
2301
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2302
|
+
job_id TEXT NOT NULL,
|
|
2303
|
+
job_type TEXT NOT NULL,
|
|
2304
|
+
stage TEXT NOT NULL,
|
|
2305
|
+
attempt_id TEXT,
|
|
2306
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('done', 'failed', 'disabled', 'pending', 'refused', 'no-action')),
|
|
2307
|
+
row_id TEXT,
|
|
2308
|
+
pair_id TEXT,
|
|
2309
|
+
pairs_scanned INTEGER NOT NULL DEFAULT 0,
|
|
2310
|
+
verdicts INTEGER NOT NULL DEFAULT 0,
|
|
2311
|
+
actions_applied INTEGER NOT NULL DEFAULT 0,
|
|
2312
|
+
reason TEXT,
|
|
2313
|
+
detail TEXT,
|
|
2314
|
+
CHECK (outcome NOT IN ('failed', 'refused') OR length(trim(reason)) > 0)
|
|
2315
|
+
);
|
|
2316
|
+
CREATE INDEX IF NOT EXISTS rem_journal_job_sequence ON nodix_rem_journal(job_id, sequence);
|
|
2317
|
+
|
|
2318
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_recovery_history (
|
|
2319
|
+
recovery_handle TEXT PRIMARY KEY,
|
|
2320
|
+
row_id TEXT NOT NULL,
|
|
2321
|
+
operation_kind TEXT NOT NULL CHECK (operation_kind IN ('lane', 'text-version', 'mark')),
|
|
2322
|
+
prior_row_image TEXT NOT NULL,
|
|
2323
|
+
prior_content_hash TEXT NOT NULL,
|
|
2324
|
+
expected_post_hash TEXT NOT NULL,
|
|
2325
|
+
row_hash_version INTEGER NOT NULL DEFAULT 1,
|
|
2326
|
+
reason TEXT NOT NULL CHECK (length(trim(reason)) > 0),
|
|
2327
|
+
mutation_ts TEXT NOT NULL,
|
|
2328
|
+
restored_at TEXT
|
|
2329
|
+
);
|
|
2330
|
+
CREATE INDEX IF NOT EXISTS rem_recovery_history_row ON nodix_rem_recovery_history(row_id);
|
|
2331
|
+
|
|
2332
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_memory_facets (
|
|
2333
|
+
memory_id TEXT NOT NULL,
|
|
2334
|
+
facet TEXT NOT NULL CHECK (facet IN ('current', 'history')),
|
|
2335
|
+
text TEXT NOT NULL,
|
|
2336
|
+
updated_at_ms INTEGER NOT NULL,
|
|
2337
|
+
PRIMARY KEY (memory_id, facet),
|
|
2338
|
+
FOREIGN KEY (memory_id) REFERENCES nodix_memories(id) ON DELETE CASCADE
|
|
2339
|
+
);
|
|
2340
|
+
CREATE INDEX IF NOT EXISTS rem_memory_facets_by_facet
|
|
2341
|
+
ON nodix_rem_memory_facets(facet, memory_id);
|
|
2342
|
+
|
|
2343
|
+
CREATE TRIGGER IF NOT EXISTS rem_memory_facets_after_insert
|
|
2344
|
+
AFTER INSERT ON nodix_memories
|
|
2345
|
+
BEGIN
|
|
2346
|
+
INSERT INTO nodix_rem_memory_facets(memory_id, facet, text, updated_at_ms)
|
|
2347
|
+
VALUES (NEW.id, 'current', NEW.text, NEW.timestamp);
|
|
2348
|
+
END;
|
|
2349
|
+
|
|
2350
|
+
CREATE TRIGGER IF NOT EXISTS rem_memory_facets_after_text_update
|
|
2351
|
+
AFTER UPDATE OF text ON nodix_memories
|
|
2352
|
+
BEGIN
|
|
2353
|
+
INSERT INTO nodix_rem_memory_facets(memory_id, facet, text, updated_at_ms)
|
|
2354
|
+
VALUES (NEW.id, 'current', NEW.text, NEW.timestamp)
|
|
2355
|
+
ON CONFLICT(memory_id, facet) DO UPDATE SET
|
|
2356
|
+
text = excluded.text, updated_at_ms = excluded.updated_at_ms;
|
|
2357
|
+
END;
|
|
2358
|
+
|
|
2359
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_facet_recovery (
|
|
2360
|
+
recovery_handle TEXT PRIMARY KEY,
|
|
2361
|
+
prior_facets_json TEXT NOT NULL,
|
|
2362
|
+
prior_chunk_facets_json TEXT NOT NULL,
|
|
2363
|
+
expected_facets_json TEXT NOT NULL,
|
|
2364
|
+
expected_chunk_facets_json TEXT NOT NULL,
|
|
2365
|
+
FOREIGN KEY (recovery_handle) REFERENCES nodix_rem_recovery_history(recovery_handle)
|
|
2366
|
+
);
|
|
2367
|
+
|
|
2368
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_write_verdicts (
|
|
2369
|
+
evidence_id TEXT PRIMARY KEY,
|
|
2370
|
+
winner_row_id TEXT NOT NULL,
|
|
2371
|
+
loser_row_id TEXT NOT NULL,
|
|
2372
|
+
target_row_id TEXT NOT NULL,
|
|
2373
|
+
retired_fact_atoms_json TEXT NOT NULL,
|
|
2374
|
+
recorded_at TEXT NOT NULL
|
|
2375
|
+
);
|
|
2376
|
+
|
|
2377
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_write_attempts (
|
|
2378
|
+
attempt_id TEXT PRIMARY KEY,
|
|
2379
|
+
job_id TEXT NOT NULL,
|
|
2380
|
+
stage TEXT NOT NULL,
|
|
2381
|
+
row_id TEXT NOT NULL,
|
|
2382
|
+
writer TEXT NOT NULL,
|
|
2383
|
+
attempt_ordinal INTEGER NOT NULL CHECK (attempt_ordinal > 0),
|
|
2384
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('pending', 'succeeded', 'failed', 'refused', 'degraded')),
|
|
2385
|
+
reason_code TEXT,
|
|
2386
|
+
pre_write_content_sha256 TEXT NOT NULL,
|
|
2387
|
+
proposed_text_sha256 TEXT NOT NULL,
|
|
2388
|
+
evidence_id TEXT NOT NULL,
|
|
2389
|
+
configuration_sha256 TEXT NOT NULL,
|
|
2390
|
+
expected_post_content_sha256 TEXT,
|
|
2391
|
+
post_write_content_sha256 TEXT,
|
|
2392
|
+
opened_at TEXT NOT NULL,
|
|
2393
|
+
closed_at TEXT
|
|
2394
|
+
);
|
|
2395
|
+
CREATE INDEX IF NOT EXISTS rem_write_attempts_outcome ON nodix_rem_write_attempts(outcome, opened_at);
|
|
2396
|
+
|
|
2397
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_census_rows (
|
|
2398
|
+
row_id TEXT PRIMARY KEY,
|
|
2399
|
+
write_identity_sha256 TEXT,
|
|
2400
|
+
candidate_set_sha256 TEXT,
|
|
2401
|
+
generation_id TEXT,
|
|
2402
|
+
corpus_sha256 TEXT,
|
|
2403
|
+
embedder_sha256 TEXT,
|
|
2404
|
+
configuration_sha256 TEXT
|
|
2405
|
+
);
|
|
2406
|
+
|
|
2407
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_generation_transitions (
|
|
2408
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2409
|
+
outgoing_generation_id TEXT NOT NULL,
|
|
2410
|
+
incoming_generation_id TEXT NOT NULL,
|
|
2411
|
+
recorded_at TEXT NOT NULL
|
|
2412
|
+
);
|
|
2413
|
+
|
|
2414
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_batch_summaries (
|
|
2415
|
+
batch_id TEXT PRIMARY KEY,
|
|
2416
|
+
generation_id TEXT UNIQUE,
|
|
2417
|
+
calls INTEGER NOT NULL DEFAULT 0,
|
|
2418
|
+
tokens INTEGER NOT NULL DEFAULT 0,
|
|
2419
|
+
pairCount INTEGER NOT NULL DEFAULT 0,
|
|
2420
|
+
closeCount INTEGER NOT NULL DEFAULT 0,
|
|
2421
|
+
refusalCount INTEGER NOT NULL DEFAULT 0,
|
|
2422
|
+
verdict_distribution_json TEXT NOT NULL DEFAULT '{}',
|
|
2423
|
+
pre_truncation_count INTEGER,
|
|
2424
|
+
emitted_count INTEGER
|
|
2425
|
+
);
|
|
2426
|
+
|
|
2427
|
+
CREATE TABLE IF NOT EXISTS nodix_rem_verdict_observations (
|
|
2428
|
+
pair_id TEXT PRIMARY KEY,
|
|
2429
|
+
audit_kind TEXT NOT NULL CHECK (audit_kind IN ('VERDICT', 'SAMPLE')),
|
|
2430
|
+
persisted_verdict TEXT,
|
|
2431
|
+
raw_value TEXT NOT NULL
|
|
2432
|
+
);
|
|
2433
|
+
`;
|
|
2434
|
+
function installRemSchema(database) {
|
|
2435
|
+
database.exec(REM_SCHEMA_SQL);
|
|
2436
|
+
ensureAmbiguousRowState(database);
|
|
2437
|
+
ensureHolderBootIdColumn(database, "nodix_rem_row_claims");
|
|
2438
|
+
ensureHolderBootIdColumn(database, "nodix_rem_pair_claims");
|
|
2439
|
+
ensureJournalNullableColumn(database, "row_id");
|
|
2440
|
+
ensureJournalNullableColumn(database, "pair_id");
|
|
2441
|
+
ensureJournalNullableColumn(database, "attempt_id");
|
|
2442
|
+
ensureJournalNullableColumn(database, "detail");
|
|
2443
|
+
database.exec(`
|
|
2444
|
+
CREATE UNIQUE INDEX IF NOT EXISTS rem_journal_external_decision_attempt
|
|
2445
|
+
ON nodix_rem_journal(attempt_id)
|
|
2446
|
+
WHERE attempt_id IS NOT NULL AND stage IN ('arbitration', 'coverage');
|
|
2447
|
+
`);
|
|
2448
|
+
ensureRemChunkFacet(database);
|
|
2449
|
+
backfillRemCurrentFacets(database);
|
|
2450
|
+
}
|
|
2451
|
+
function ensureAmbiguousRowState(database) {
|
|
2452
|
+
if (database.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'nodix_rem_relation_ledger'").get()?.sql?.includes("'ambiguous'")) return;
|
|
2453
|
+
database.exec("PRAGMA foreign_keys = OFF");
|
|
2454
|
+
try {
|
|
2455
|
+
database.transaction(() => {
|
|
2456
|
+
database.exec(`
|
|
2457
|
+
CREATE TABLE nodix_rem_relation_ledger_next (
|
|
2458
|
+
row_id TEXT PRIMARY KEY,
|
|
2459
|
+
content_hash TEXT NOT NULL,
|
|
2460
|
+
state TEXT NOT NULL CHECK (state IN ('transition', 'stale-current', 'pure-negation', 'ambiguous')),
|
|
2461
|
+
owner TEXT NOT NULL DEFAULT 'none' CHECK (owner IN ('restate', 'verdict', 'none')),
|
|
2462
|
+
claim_ts TEXT,
|
|
2463
|
+
classified_at TEXT NOT NULL
|
|
2464
|
+
);
|
|
2465
|
+
INSERT INTO nodix_rem_relation_ledger_next(
|
|
2466
|
+
row_id, content_hash, state, owner, claim_ts, classified_at
|
|
2467
|
+
) SELECT row_id, content_hash, state, owner, claim_ts, classified_at
|
|
2468
|
+
FROM nodix_rem_relation_ledger;
|
|
2469
|
+
DROP TABLE nodix_rem_relation_ledger;
|
|
2470
|
+
ALTER TABLE nodix_rem_relation_ledger_next RENAME TO nodix_rem_relation_ledger;
|
|
2471
|
+
`);
|
|
2472
|
+
}).immediate();
|
|
2473
|
+
} finally {
|
|
2474
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
function ensureRemChunkFacet(database) {
|
|
2478
|
+
if (!tableExists(database, "nodix_memory_chunks")) return;
|
|
2479
|
+
if (!database.prepare("PRAGMA table_info(nodix_memory_chunks)").all().some(({ name }) => name === "facet")) database.exec("ALTER TABLE nodix_memory_chunks ADD COLUMN facet TEXT NOT NULL DEFAULT 'current' CHECK (facet IN ('current', 'history'))");
|
|
2480
|
+
database.exec(`
|
|
2481
|
+
DROP INDEX IF EXISTS nodix_idx_memory_chunks_memory;
|
|
2482
|
+
CREATE UNIQUE INDEX IF NOT EXISTS nodix_idx_memory_chunks_memory_facet
|
|
2483
|
+
ON nodix_memory_chunks(memory_id, facet, chunk_index);
|
|
2484
|
+
`);
|
|
2485
|
+
}
|
|
2486
|
+
function backfillRemCurrentFacets(database) {
|
|
2487
|
+
if (!tableExists(database, "nodix_memories")) return;
|
|
2488
|
+
database.exec(`
|
|
2489
|
+
INSERT OR IGNORE INTO nodix_rem_memory_facets(memory_id, facet, text, updated_at_ms)
|
|
2490
|
+
SELECT id, 'current', text, timestamp FROM nodix_memories;
|
|
2491
|
+
`);
|
|
2492
|
+
}
|
|
2493
|
+
function tableExists(database, tableName) {
|
|
2494
|
+
return database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName) !== void 0;
|
|
2495
|
+
}
|
|
2496
|
+
function ensureJournalNullableColumn(database, column) {
|
|
2497
|
+
if (database.prepare("PRAGMA table_info(nodix_rem_journal)").all().some((candidate) => candidate.name === column)) return;
|
|
2498
|
+
database.exec(`ALTER TABLE nodix_rem_journal ADD COLUMN ${column} TEXT`);
|
|
2499
|
+
}
|
|
2500
|
+
function ensureHolderBootIdColumn(database, tableName) {
|
|
2501
|
+
if (database.prepare(`PRAGMA table_info(${tableName})`).all().some((column) => column.name === "holder_boot_id")) return;
|
|
2502
|
+
database.exec(`ALTER TABLE ${tableName} ADD COLUMN holder_boot_id TEXT`);
|
|
2503
|
+
}
|
|
2504
|
+
//#endregion
|
|
2505
|
+
//#region src/engine/rem/writer-census.ts
|
|
2506
|
+
function discoverRemWritersFromCallGraph(input) {
|
|
2507
|
+
const executorSource = [...readImportClosure(input.entryPoints).values()].find((source) => /^(?:async\s+)?function applyWriterOperation\s*\(/mu.test(source));
|
|
2508
|
+
if (executorSource === void 0) throw new Error("REM production mutation executor is not reachable from the entry points");
|
|
2509
|
+
const start = executorSource.search(/^(?:async\s+)?function applyWriterOperation\s*\(/mu);
|
|
2510
|
+
const end = executorSource.indexOf("\n}\n\nfunction ", start);
|
|
2511
|
+
if (end < 0) throw new Error("REM production mutation executor boundary is incomplete");
|
|
2512
|
+
const writers = /* @__PURE__ */ new Set();
|
|
2513
|
+
for (const match of executorSource.slice(start, end + 2).matchAll(/case\s+"([A-Za-z]+)"\s*:/g)) {
|
|
2514
|
+
const writer = match[1];
|
|
2515
|
+
if (writer !== void 0) writers.add(writer);
|
|
2516
|
+
}
|
|
2517
|
+
return { writers: [...writers].map((writer) => ({ writer })) };
|
|
2518
|
+
}
|
|
2519
|
+
function readImportClosure(entryPoints) {
|
|
2520
|
+
const pending = entryPoints.map((entryPoint) => resolve(entryPoint));
|
|
2521
|
+
const sources = /* @__PURE__ */ new Map();
|
|
2522
|
+
while (pending.length > 0) {
|
|
2523
|
+
const path = pending.shift();
|
|
2524
|
+
if (path === void 0 || sources.has(path)) continue;
|
|
2525
|
+
if (!existsSync(path)) throw new Error(`REM call-graph entry does not exist: ${path}`);
|
|
2526
|
+
const source = readFileSync(path, "utf8");
|
|
2527
|
+
sources.set(path, source);
|
|
2528
|
+
for (const specifier of readModuleSpecifiers(source)) {
|
|
2529
|
+
const resolved = resolveSourceImport(path, specifier);
|
|
2530
|
+
if (resolved !== void 0 && !sources.has(resolved)) pending.push(resolved);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
return sources;
|
|
2534
|
+
}
|
|
2535
|
+
function readModuleSpecifiers(source) {
|
|
2536
|
+
const specifiers = [];
|
|
2537
|
+
for (const match of source.matchAll(/(?:from\s+|import\s*\()(["'])([^"']+)\1/g)) if (match[2] !== void 0) specifiers.push(match[2]);
|
|
2538
|
+
return specifiers;
|
|
2539
|
+
}
|
|
2540
|
+
function resolveSourceImport(importer, specifier) {
|
|
2541
|
+
let candidate;
|
|
2542
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) candidate = resolve(dirname(importer), specifier);
|
|
2543
|
+
else if (specifier.startsWith("@/")) {
|
|
2544
|
+
const markerIndex = importer.indexOf("/packages/memory/src/");
|
|
2545
|
+
if (markerIndex < 0) return void 0;
|
|
2546
|
+
candidate = resolve(importer.slice(0, markerIndex + 21), specifier.slice(2));
|
|
2547
|
+
} else return;
|
|
2548
|
+
return (extname(candidate).length > 0 ? [candidate, candidate.replace(/\.js$/, ".ts")] : [`${candidate}.ts`, resolve(candidate, "index.ts")]).find((path) => existsSync(path));
|
|
2549
|
+
}
|
|
2550
|
+
//#endregion
|
|
2551
|
+
export { REM_ADDITIONAL_TRANSITION_PATTERN_SHA256, REM_AMBIGUOUS_PATTERN_SHA256, REM_BUILT_OPERATION_TYPES, REM_CURRENT_PROGRESSIVE_PATTERN_SHA256, REM_ENABLE_GATE_BINDINGS, REM_HISTORICAL_FACT_PATTERN_SHA256, REM_JOB_STATES, REM_OPERATION_TYPES, REM_PURE_NEGATION_PATTERN_SHA256, REM_ROW_HASH_FIELDS, REM_ROW_HASH_VERSION, REM_ROW_HASH_VERSION_LEGACY, REM_ROW_STATES, REM_TRANSITION_GRAMMAR_SHA256, REM_UPDATE_LOCALES, RemEnableGateError, adapterAViewFromRecord, arbitrateReplaceVerdicts, assertJobIdentity, buildRemCoverageAtoms, buildRemUpdateReferenceSet, buildReplaceEligibleClauses, canonicalizeRemConfiguration, classifyAllRemCensusLabels, classifyRemCensusLabel, classifyRemRow, composeRemNegatedCurrent, createRemRepository, decideRemClauseCarry, decideRemRetirementTargetFromReply, decideRemUpdateFromReply, decideRemUpdateRelationFromReply, decideRemUpdateVerification, decideReplaceCoverage, declareRemCalibrationThreshold, defineRemSafetyGuard, deriveRemConfigurationSha256, deriveRemReplayArtifactIdentity, deriveRemWriteIdentity, discoverRemWritersFromCallGraph, enumerateRemUpdateMembers, extractJsonFromResponse, getRemUpdateLocaleResource, hashRemMemoryRow, installRemSchema, isAdapterAVerdictReply, journalRemGenerationTransition, loadRemEnableGate, orderAdapterAPair, parseAdapterAChatVerdict, parseRemOperationType, parseRemOperationalConfiguration, parseReplaceClauseVerdict, parseReplaceCoverageAtoms, produceRemCensusArtifact, prospectiveProfileView, readRetainedRemCensusArtifact, recordNonRefusePairDecision, recordRemCandidateCapCounts, recordRemObservedVerdict, renderAdapterAChatPrompt, renderAdapterAPrompt, renderRemClauseCarryPrompt, renderRemRetirementTargetPrompt, renderRemUpdateJudgmentPrompt, renderRemUpdateRelationJudgmentPrompt, renderRemUpdateVerificationPrompt, renderReplaceClauseVerdictPrompt, renderReplaceCoveragePrompt, reportRemSimilarityCalibration, requireRemCoverageAccuracyFloor, requireRemOwnerDecisions, runRemStages, runRestateClassifierEntry, runVerdictClassifierEntry, splitExactClauses, summarizeRemBatchEvents, validateRemCalibrationCrossCheck, validateRemCensusLabelTuple, validateRemCensusOracleProvenance, validateRemCensusRouteCoverage, validateRemClauseCoverageArtifacts, validateRemEnableGateDigestKeys, validateRemGrammarChange, validateRemGuardCensus, validateRemProspectiveIdentity, validateRemReplayIdentityTuple, validateRemSubstantiveWaveEffects, writeRemTwoFacetTransaction };
|