@snoai/memory 0.0.0-stage → 1.0.1
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 +33 -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-BbEvEocb.d.ts +110 -0
- package/dist/adapter-B7Jox9-6.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-C5rmogoY.js +3605 -0
- package/dist/atomic-memory-extraction-d9EG-4ti.d.ts +327 -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-CxbHPkMX.d.ts +53 -0
- package/dist/capture-policy-detector-DaMfy9CZ.js +357 -0
- package/dist/classifier-DnbbtwTS.d.ts +272 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.js +306 -0
- package/dist/coding-skin.d.ts +51 -0
- package/dist/coding-skin.js +55 -0
- package/dist/config-CL90qYva.js +2576 -0
- package/dist/connection-3IVXpuPE.d.ts +391 -0
- package/dist/connection-C9YD5sgm.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-Bl3lthzE.d.ts +30 -0
- package/dist/data-bootstrap-P8sie9WA.js +172 -0
- package/dist/data-paths-C7sNdqvP.d.ts +22 -0
- package/dist/derived-suppression-cache-CI0durYx.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-0aqS8pVF.d.ts +351 -0
- package/dist/index-BO4RU_mc.d.ts +650 -0
- package/dist/index-DL8AVj0Z.d.ts +571 -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 +62 -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 +36 -0
- package/dist/internal/engine/bindings/sno-station-mem-runtime-mode.js +104 -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-BSYUAcUb.js +1374 -0
- package/dist/llm-client-DOTSGzhe.d.ts +51 -0
- package/dist/locales-JxGmkG6t.d.ts +7 -0
- package/dist/log-site-catalog.generated-BMKWj-Nx.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-BiuRjSJc.d.ts +23 -0
- package/dist/memory-metadata-codec-DL1sbJiN.js +577 -0
- package/dist/memory-metadata-codec-DUvsYe_X.d.ts +68 -0
- package/dist/memory-runtime-DqUS4cWi.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-CqY1BUUA.d.ts +16 -0
- package/dist/memory-source-order-YMRoeDLd.js +134 -0
- package/dist/memory-store-base-CQ_bHlYA.d.ts +1094 -0
- package/dist/memory-store-shared-DqpWJXZZ.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-BUjXG5o7.js +184 -0
- package/dist/memory-temporality-classifier-Brht5jyK.d.ts +43 -0
- package/dist/memory-tier-promoter-CqGxCkue.d.ts +43 -0
- package/dist/memory-tool-dependencies-CatGzI9R.d.ts +14 -0
- package/dist/memory-tool-dependencies-DjUoelnm.js +19 -0
- package/dist/model-call-table-CspopkBg.js +563 -0
- package/dist/model-reply-text-CKopStP2.js +334 -0
- package/dist/observable-embedder-CNNe9gkg.d.ts +17 -0
- package/dist/observable-llm-client-CRuKlefQ.d.ts +21 -0
- package/dist/observable-memory-store-CnGDpOY5.d.ts +73 -0
- package/dist/observable-retriever-Cooc62Qw.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-BhGKPBjw.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-ClUl-iv6.js +644 -0
- package/dist/reflection-command-hooks-G1-dI2Av.d.ts +32 -0
- package/dist/reflection-deps-Ccm2Jf5W.d.ts +19 -0
- package/dist/reflection-embedded-generator-BRvqV5N-.js +130 -0
- package/dist/reflection-embedded-generator-kfQfKEYV.d.ts +14 -0
- package/dist/reflection-injection-hooks-CS_uhjoD.d.ts +43 -0
- package/dist/reflection-lifecycle-hooks-CRSpUdLi.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-Dg2HgROM.js +2 -0
- package/dist/rem-batch-executor-DuC4nYyf.js +31640 -0
- package/dist/rem-consumer-retrieval-CR0dtcHR.js +246 -0
- package/dist/rem-trigger-Hn6od8IV.js +730 -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-Dxss5fRv.d.ts +469 -0
- package/dist/retriever-ZoLvPRHq.js +1757 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/routes-DLHT__Ay.js +60 -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-CSvxchqg.js +1412 -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-CoWqUMbK.js +30 -0
- package/dist/sno-station-mem-runtime-dependencies-DHESIuPn.d.ts +31 -0
- package/dist/sqlite-runtime-L_ywirTm.d.ts +58 -0
- package/dist/store-C8sUTssE.d.ts +27 -0
- package/dist/store-nPql26YM.js +5830 -0
- package/dist/strategy-hook-runner-Bxy851Xx.d.ts +16 -0
- package/dist/strategy-hook-runner-Djify5Rw.js +110 -0
- package/dist/token-counter-BD1f48Km.js +73 -0
- package/dist/trace-C1JxMfs-.d.ts +86 -0
- package/dist/types-CWdhXLxr.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,1401 @@
|
|
|
1
|
+
import { BACKUP_INTERVAL_MS, BACKUP_MIN_FREE_DISK_RATIO, FTS_MERGE_INTERVAL_MS, INTEGRITY_CHECK_INTERVAL_MS, MAINTENANCE_TICK_MS, PLANNER_STATISTICS_INTERVAL_MS, REM_TRIGGER_CHECK_INTERVAL_MS, USAGE_EVENT_RETENTION_INTERVAL_MS, USAGE_OUTBOX_INTERVAL_MS } from "../config/index.js";
|
|
2
|
+
import { REM_SIDECAR_TOKEN_HEADER } from "../contract/routes.js";
|
|
3
|
+
import "../engine/shared/utils.js";
|
|
4
|
+
import "../../coding-skin.js";
|
|
5
|
+
import "../config/settings.js";
|
|
6
|
+
import "../../llm-client-BSYUAcUb.js";
|
|
7
|
+
import "../model/llm-mode-routing.js";
|
|
8
|
+
import { n as appendAuditEntryStrict, o as getAuditPath, t as appendAuditEntry } from "../../runtime-audit-log-BCeujlEp.js";
|
|
9
|
+
import { getDiscoveryPath, getSnoStationMemStateDir } from "../contract/profile.js";
|
|
10
|
+
import "../engine/shared/paths.js";
|
|
11
|
+
import { defineRemSafetyGuard, requireRemOwnerDecisions } from "../engine/rem/index.js";
|
|
12
|
+
import { n as state_vocabulary_default, r as attribute_dictionary_default } from "../../relation-dictionary-Cev1kyLe.js";
|
|
13
|
+
import "../../memory-store-shared-DqpWJXZZ.js";
|
|
14
|
+
import { n as loadSqliteVecExtension } from "../../unplaced-candidate-migration-CbQHzcEM.js";
|
|
15
|
+
import { openSqliteDatabase } from "./sqlite-runtime.js";
|
|
16
|
+
import "../../connection-C9YD5sgm.js";
|
|
17
|
+
import "../../store-nPql26YM.js";
|
|
18
|
+
import "../../embedding-provider-client-Q_hJonK9.js";
|
|
19
|
+
import "../../retriever-ZoLvPRHq.js";
|
|
20
|
+
import { t as recordMemoryTelemetryIncident } from "../../memory-telemetry-incidents-BWA9kcZx.js";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import "@snoai/chunking";
|
|
23
|
+
import { createLogger } from "@snoai/utils/logger";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
26
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
27
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, statfsSync } from "node:fs";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { runIntegrityCheck } from "@snoai/sqlite-crypto";
|
|
31
|
+
import { withLogContext as withLogContext$1 } from "@snoai/utils/log-context";
|
|
32
|
+
//#region src/store/backup.ts
|
|
33
|
+
/** @file backup.ts
|
|
34
|
+
* @purpose Exports, imports, and validates persisted memory data for backup workflows.
|
|
35
|
+
* @boundary Memory store serialization, schema compatibility, and CLI operations.
|
|
36
|
+
* @see store.ts, memory-management-cli.ts, schema.ts.
|
|
37
|
+
*/
|
|
38
|
+
const log$4 = createLogger("sno-station-mem:backup");
|
|
39
|
+
const BACKUP_PREFIX = "sno-station-mem-";
|
|
40
|
+
const BACKUP_SUFFIX = ".sqlite";
|
|
41
|
+
const BACKUP_FILE_REGEX = /^sno-station-mem-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}-p\d+-[a-z0-9]+\.sqlite$/;
|
|
42
|
+
/** Format `Date` as a unique UTC timestamp (colon-free for FS-safety). */
|
|
43
|
+
function formatBackupTimestamp(now) {
|
|
44
|
+
return `${now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "")}-p${process.pid}-${process.hrtime.bigint().toString(36)}`;
|
|
45
|
+
}
|
|
46
|
+
/** Escapes a value for SQLite string-literal use in backup statements. */
|
|
47
|
+
function quoteSqliteStringLiteral(value) {
|
|
48
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validates backup file placement before SQLite is allowed to write outside the state directory.
|
|
52
|
+
*/
|
|
53
|
+
function assertSafeBackupPath(stateDir, filePath) {
|
|
54
|
+
const resolvedStateDir = realpathSync(stateDir);
|
|
55
|
+
const resolvedBackupPath = path.resolve(filePath);
|
|
56
|
+
const relativePath = path.relative(resolvedStateDir, resolvedBackupPath);
|
|
57
|
+
if (relativePath === "" || relativePath.startsWith("..") || path.isAbsolute(relativePath)) throw new Error(`Backup path escapes configured state directory: ${filePath}`);
|
|
58
|
+
if (!BACKUP_FILE_REGEX.test(path.basename(resolvedBackupPath))) throw new Error(`Unexpected backup filename: ${path.basename(resolvedBackupPath)}`);
|
|
59
|
+
return resolvedBackupPath;
|
|
60
|
+
}
|
|
61
|
+
/** Thrown when the volume cannot hold a backup even after old backups are removed. */
|
|
62
|
+
var BackupSkippedLowDiskError = class extends Error {
|
|
63
|
+
neededBytes;
|
|
64
|
+
freeBytes;
|
|
65
|
+
reserveBytes;
|
|
66
|
+
constructor(neededBytes, freeBytes, reserveBytes) {
|
|
67
|
+
super(`backup skipped: needs ${neededBytes} bytes above a ${reserveBytes}-byte reserve, ${freeBytes} free`);
|
|
68
|
+
this.neededBytes = neededBytes;
|
|
69
|
+
this.freeBytes = freeBytes;
|
|
70
|
+
this.reserveBytes = reserveBytes;
|
|
71
|
+
this.name = "BackupSkippedLowDiskError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
function readVolumeSpace(dir) {
|
|
75
|
+
const info = statfsSync(dir);
|
|
76
|
+
return {
|
|
77
|
+
freeBytes: info.bavail * info.bsize,
|
|
78
|
+
totalBytes: info.blocks * info.bsize
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Backup file names in the directory, oldest first (names sort by their UTC timestamp). */
|
|
82
|
+
function listBackups(stateDir) {
|
|
83
|
+
return readdirSync(stateDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.startsWith(BACKUP_PREFIX) && entry.name.endsWith(BACKUP_SUFFIX)).map((entry) => entry.name).sort();
|
|
84
|
+
}
|
|
85
|
+
function removeBackups(stateDir, names, reason) {
|
|
86
|
+
for (const name of names) rmSync(path.join(stateDir, name), { force: true });
|
|
87
|
+
if (names.length > 0) log$4.debug("pruned old backups", {
|
|
88
|
+
removed: names.length,
|
|
89
|
+
reason
|
|
90
|
+
}, {
|
|
91
|
+
event_name: "sno_station_mem.backup.pruned.old.backups",
|
|
92
|
+
file: "packages/memory/src/store/backup.ts",
|
|
93
|
+
function: "removeBackups",
|
|
94
|
+
site_id: "backup.pruneOldBackups.1dca69b51b"
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Deletes the oldest backups until `neededBytes` fits above the volume's reserve. The newest backup
|
|
99
|
+
* is never deleted here: a full disk must not cost the user their last good copy. Returns whether
|
|
100
|
+
* the space now fits.
|
|
101
|
+
*/
|
|
102
|
+
function freeSpaceForBackup(stateDir, neededBytes, readDiskSpace) {
|
|
103
|
+
const backups = listBackups(stateDir);
|
|
104
|
+
let space = readDiskSpace(stateDir);
|
|
105
|
+
const reserve = space.totalBytes * BACKUP_MIN_FREE_DISK_RATIO;
|
|
106
|
+
while (space.freeBytes - neededBytes < reserve && backups.length > 1) {
|
|
107
|
+
const oldest = backups.shift();
|
|
108
|
+
if (oldest === void 0) break;
|
|
109
|
+
removeBackups(stateDir, [oldest], "low-disk");
|
|
110
|
+
space = readDiskSpace(stateDir);
|
|
111
|
+
}
|
|
112
|
+
return space.freeBytes - neededBytes >= reserve;
|
|
113
|
+
}
|
|
114
|
+
/** Size a fresh copy of the store can reach: the main file plus its unmerged write-ahead log. */
|
|
115
|
+
function storeBytes(dbPath) {
|
|
116
|
+
const walPath = `${dbPath}-wal`;
|
|
117
|
+
return statSync(dbPath).size + (existsSync(walPath) ? statSync(walPath).size : 0);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Whether a backup is due: none exists yet, or the newest is at least BACKUP_INTERVAL_MS old.
|
|
121
|
+
* Read from the files so a restart never takes an extra backup.
|
|
122
|
+
*/
|
|
123
|
+
function isBackupDue(stateDir, now, intervalMs = BACKUP_INTERVAL_MS) {
|
|
124
|
+
if (!existsSync(stateDir)) return true;
|
|
125
|
+
const newest = listBackups(stateDir).at(-1);
|
|
126
|
+
if (newest === void 0) return true;
|
|
127
|
+
return now - statSync(path.join(stateDir, newest)).mtimeMs >= intervalMs;
|
|
128
|
+
}
|
|
129
|
+
/** Implements run backup as the local SQLite backup scheduling operation. */
|
|
130
|
+
function runBackup(dbPath, stateDir, options = {}) {
|
|
131
|
+
const normalizedStateDir = stateDir.replaceAll("\\", path.sep);
|
|
132
|
+
mkdirSync(normalizedStateDir, { recursive: true });
|
|
133
|
+
const resolvedStateDir = realpathSync(normalizedStateDir);
|
|
134
|
+
const fileName = `${BACKUP_PREFIX}${formatBackupTimestamp(/* @__PURE__ */ new Date())}${BACKUP_SUFFIX}`;
|
|
135
|
+
const safeBackupPath = assertSafeBackupPath(resolvedStateDir, path.join(resolvedStateDir, fileName));
|
|
136
|
+
const tempBackupPath = `${safeBackupPath}.tmp`;
|
|
137
|
+
const readDiskSpace = options.readDiskSpace ?? readVolumeSpace;
|
|
138
|
+
const neededBytes = storeBytes(dbPath);
|
|
139
|
+
if (!freeSpaceForBackup(resolvedStateDir, neededBytes, readDiskSpace)) {
|
|
140
|
+
const space = readDiskSpace(resolvedStateDir);
|
|
141
|
+
throw new BackupSkippedLowDiskError(neededBytes, space.freeBytes, space.totalBytes * BACKUP_MIN_FREE_DISK_RATIO);
|
|
142
|
+
}
|
|
143
|
+
const sqlite = openSqliteDatabase(dbPath, {
|
|
144
|
+
readonly: true,
|
|
145
|
+
fileMustExist: true
|
|
146
|
+
});
|
|
147
|
+
loadSqliteVecExtension(sqlite.raw);
|
|
148
|
+
try {
|
|
149
|
+
sqlite.db.exec(`VACUUM INTO ${quoteSqliteStringLiteral(tempBackupPath)}`);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
rmSync(tempBackupPath, { force: true });
|
|
152
|
+
throw error;
|
|
153
|
+
} finally {
|
|
154
|
+
sqlite.db.close();
|
|
155
|
+
}
|
|
156
|
+
renameSync(tempBackupPath, safeBackupPath);
|
|
157
|
+
const backups = listBackups(resolvedStateDir);
|
|
158
|
+
removeBackups(resolvedStateDir, backups.slice(0, Math.max(0, backups.length - 6)), "retention");
|
|
159
|
+
freeSpaceForBackup(resolvedStateDir, 0, readDiskSpace);
|
|
160
|
+
log$4.info("backup created", { backupPath: safeBackupPath }, {
|
|
161
|
+
event_name: "sno_station_mem.backup.backup.created",
|
|
162
|
+
file: "packages/memory/src/store/backup.ts",
|
|
163
|
+
function: "runBackup",
|
|
164
|
+
site_id: "backup.runBackup.ca114973f1"
|
|
165
|
+
});
|
|
166
|
+
return safeBackupPath;
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/sidecar/config.ts
|
|
170
|
+
const REM_SIDECAR_HOST = "127.0.0.1";
|
|
171
|
+
const REM_CORRELATION_ID_HEADER = "x-rem-correlation-id";
|
|
172
|
+
const REM_RUN_PATH = "/rem/run";
|
|
173
|
+
function getRemDiscoveryPath() {
|
|
174
|
+
return getDiscoveryPath();
|
|
175
|
+
}
|
|
176
|
+
"0".repeat(64), "0".repeat(64), "0".repeat(64), "0".repeat(64), "0".repeat(64), "0".repeat(64);
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/sidecar/rem-entry-foundations.ts
|
|
179
|
+
const sha256Schema = z.string().regex(/^[0-9a-f]{64}$/u);
|
|
180
|
+
const finiteRecordSchema = z.record(z.string(), z.number().finite()).refine((value) => Object.keys(value).length > 0);
|
|
181
|
+
const commonArtifactShape = {
|
|
182
|
+
schemaVersion: z.literal(1),
|
|
183
|
+
artifactId: z.string().min(1),
|
|
184
|
+
profileId: z.string().min(1),
|
|
185
|
+
configurationSha256: sha256Schema,
|
|
186
|
+
createdAt: z.string().datetime()
|
|
187
|
+
};
|
|
188
|
+
z.object({
|
|
189
|
+
...commonArtifactShape,
|
|
190
|
+
artifactId: z.literal("p5-production-config"),
|
|
191
|
+
result: z.enum(["pass", "fail"]),
|
|
192
|
+
servedModel: z.string().min(1),
|
|
193
|
+
resolvedParameters: z.record(z.string(), z.unknown()).refine((value) => Object.keys(value).length > 0),
|
|
194
|
+
readbackSource: z.enum(["wrapper", "immutable-profile"]),
|
|
195
|
+
immutableProfileSha256: sha256Schema.nullable(),
|
|
196
|
+
wrapperInvocationId: z.string().min(1).optional(),
|
|
197
|
+
wrapperReadbackSha256: sha256Schema.optional()
|
|
198
|
+
}).strict();
|
|
199
|
+
z.object({
|
|
200
|
+
...commonArtifactShape,
|
|
201
|
+
artifactId: z.literal("p6-monthly-non-regression"),
|
|
202
|
+
period: z.string().regex(/^\d{4}-\d{2}$/u),
|
|
203
|
+
minima: finiteRecordSchema,
|
|
204
|
+
measured: finiteRecordSchema,
|
|
205
|
+
result: z.enum(["pass", "fail"])
|
|
206
|
+
}).strict();
|
|
207
|
+
z.object({
|
|
208
|
+
...commonArtifactShape,
|
|
209
|
+
artifactId: z.literal("p6-monthly-non-regression"),
|
|
210
|
+
kind: z.literal("owner-waiver"),
|
|
211
|
+
result: z.literal("pass"),
|
|
212
|
+
waivedBy: z.string().min(1),
|
|
213
|
+
waiverReason: z.string().min(1)
|
|
214
|
+
}).strict();
|
|
215
|
+
z.object({
|
|
216
|
+
...commonArtifactShape,
|
|
217
|
+
artifactId: z.literal("p7-detector-gate-verdict"),
|
|
218
|
+
kind: z.literal("measured-pass"),
|
|
219
|
+
minima: finiteRecordSchema,
|
|
220
|
+
measured: finiteRecordSchema,
|
|
221
|
+
result: z.enum(["pass", "fail"])
|
|
222
|
+
}).strict();
|
|
223
|
+
z.object({
|
|
224
|
+
...commonArtifactShape,
|
|
225
|
+
artifactId: z.literal("p7-detector-gate-verdict"),
|
|
226
|
+
kind: z.literal("owner-waiver"),
|
|
227
|
+
result: z.literal("pass"),
|
|
228
|
+
waivedBy: z.string().min(1),
|
|
229
|
+
waiverReason: z.string().min(1)
|
|
230
|
+
}).strict();
|
|
231
|
+
z.object({
|
|
232
|
+
...commonArtifactShape,
|
|
233
|
+
artifactId: z.literal("population-routing"),
|
|
234
|
+
producedAtHead: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
235
|
+
classifierSha256: sha256Schema,
|
|
236
|
+
fixtureSetSha256: sha256Schema,
|
|
237
|
+
derivationCommand: z.string().min(1),
|
|
238
|
+
invocationId: z.string().min(1),
|
|
239
|
+
observations: z.array(z.object({
|
|
240
|
+
fixtureId: z.string().min(1),
|
|
241
|
+
inputSha256: sha256Schema,
|
|
242
|
+
observedRoute: z.string().min(1),
|
|
243
|
+
observedVerdict: z.string().min(1)
|
|
244
|
+
}).strict())
|
|
245
|
+
}).strict();
|
|
246
|
+
z.object({
|
|
247
|
+
...commonArtifactShape,
|
|
248
|
+
artifactId: z.literal("population-routing"),
|
|
249
|
+
kind: z.literal("owner-waiver"),
|
|
250
|
+
result: z.literal("pass"),
|
|
251
|
+
waivedBy: z.string().min(1),
|
|
252
|
+
waiverReason: z.string().min(1)
|
|
253
|
+
}).strict();
|
|
254
|
+
defineRemSafetyGuard({ guardId: "guard.rem.ordered-wave-entry" }, requireRemOwnerDecisions);
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/sidecar/rem-update-judgment-skill.ts
|
|
257
|
+
/** @file rem-update-judgment-skill.ts
|
|
258
|
+
* @purpose Loads the model-readable REM update judgment method shipped with the plugin.
|
|
259
|
+
* @boundary Package-relative read of one trusted bundled skill.
|
|
260
|
+
*/
|
|
261
|
+
const REM_UPDATE_SKILL_PATH = path.join("skills", "judge-rem-memory-updates", "SKILL.md");
|
|
262
|
+
const GROUP_CLOSURE_SKILL_PATH = path.join("skills", "judge-group-closure", "SKILL.md");
|
|
263
|
+
function resolveSkillPath(skillPath) {
|
|
264
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
265
|
+
for (const root of [
|
|
266
|
+
path.resolve(moduleDir, ".."),
|
|
267
|
+
path.resolve(moduleDir, "../.."),
|
|
268
|
+
path.resolve(moduleDir, "../../..")
|
|
269
|
+
]) {
|
|
270
|
+
const candidate = path.join(root, skillPath);
|
|
271
|
+
if (existsSync(candidate)) return candidate;
|
|
272
|
+
}
|
|
273
|
+
throw new Error(`sno-station-mem REM update judgment skill is missing: ${skillPath}`);
|
|
274
|
+
}
|
|
275
|
+
const skillText = readFileSync(resolveSkillPath(REM_UPDATE_SKILL_PATH), "utf8");
|
|
276
|
+
readFileSync(resolveSkillPath(GROUP_CLOSURE_SKILL_PATH), "utf8");
|
|
277
|
+
function readSection(heading) {
|
|
278
|
+
const marker = `## ${heading}\n`;
|
|
279
|
+
const start = skillText.indexOf(marker);
|
|
280
|
+
if (start === -1) throw new Error(`REM update judgment skill is missing section: ${heading}`);
|
|
281
|
+
const bodyStart = start + marker.length;
|
|
282
|
+
const nextSection = skillText.indexOf("\n## ", bodyStart);
|
|
283
|
+
return skillText.slice(bodyStart, nextSection === -1 ? void 0 : nextSection).trim();
|
|
284
|
+
}
|
|
285
|
+
readSection("Relation Judgment"), readSection("Source Rewrite"), readSection("Rewrite Verification"), readSection("Clause Carry");
|
|
286
|
+
createLogger("sno-station-mem:rem-sqlite-adapter");
|
|
287
|
+
new AsyncLocalStorage();
|
|
288
|
+
createLogger("sno-station-mem:rem-sidecar:batch");
|
|
289
|
+
const retirementAttributeConfigSchema = z.object({ slugs: z.array(z.object({
|
|
290
|
+
slug: z.string(),
|
|
291
|
+
family: z.string().optional(),
|
|
292
|
+
cardinality: z.enum(["one", "many"]).optional()
|
|
293
|
+
})) });
|
|
294
|
+
const personRetirementAttributes = retirementAttributeConfigSchema.parse(attribute_dictionary_default).slugs;
|
|
295
|
+
const stateRetirementAttributes = retirementAttributeConfigSchema.parse(state_vocabulary_default).slugs;
|
|
296
|
+
const attributeFamilyBySlug = new Map(personRetirementAttributes.map((entry) => [entry.slug, entry.family]));
|
|
297
|
+
for (const entry of stateRetirementAttributes) attributeFamilyBySlug.set(entry.slug, entry.slug.split(".", 1)[0]);
|
|
298
|
+
new Set([...personRetirementAttributes, ...stateRetirementAttributes].filter((entry) => entry.cardinality === "one").map((entry) => entry.slug));
|
|
299
|
+
const REM_CANDIDATE_PREDICATE_SQL = `lane = 'active'
|
|
300
|
+
AND length(trim(text)) > 0
|
|
301
|
+
AND category IN ('profile', 'episodic', 'state')
|
|
302
|
+
AND json_valid(metadata)
|
|
303
|
+
AND json_extract(metadata, '$.superseded_by') IS NULL`;
|
|
304
|
+
function enumerateRemCandidateScopes(database) {
|
|
305
|
+
return database.prepare(`SELECT project_id AS scope, count(*) AS candidate_count
|
|
306
|
+
FROM nodix_memories
|
|
307
|
+
WHERE ${REM_CANDIDATE_PREDICATE_SQL}
|
|
308
|
+
GROUP BY project_id
|
|
309
|
+
ORDER BY project_id`).all().map((row) => ({
|
|
310
|
+
scope: row.scope,
|
|
311
|
+
candidateCount: row.candidate_count
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/sidecar/rem-trigger-state.ts
|
|
316
|
+
/** @file rem-trigger-state.ts
|
|
317
|
+
* @purpose Persists the automatic REM scheduler's strict per-scope state.
|
|
318
|
+
* @boundary One atomically replaced JSON file under the sno-station-mem state directory.
|
|
319
|
+
*/
|
|
320
|
+
const remTriggerScopeStateSchema = z.object({
|
|
321
|
+
last_pass_at: z.string().datetime(),
|
|
322
|
+
schedule_zone: z.string().min(1).refine(isValidScheduleZone, "invalid IANA time zone"),
|
|
323
|
+
last_covered_count: z.number().int().nonnegative(),
|
|
324
|
+
last_volume_pass_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/u).nullable(),
|
|
325
|
+
missed_window: z.object({
|
|
326
|
+
due_at: z.string().datetime(),
|
|
327
|
+
trigger: z.enum(["daily", "volume"]),
|
|
328
|
+
recorded_at: z.string().datetime()
|
|
329
|
+
}).strict().nullable().default(null),
|
|
330
|
+
attempts: z.object({
|
|
331
|
+
identity: z.string().min(1).nullable(),
|
|
332
|
+
count: z.number().int().min(0).max(3)
|
|
333
|
+
}).strict()
|
|
334
|
+
}).strict();
|
|
335
|
+
const remTriggerStateDocumentSchema = z.object({
|
|
336
|
+
version: z.literal(1),
|
|
337
|
+
scopes: z.record(z.string().min(1), remTriggerScopeStateSchema)
|
|
338
|
+
}).strict();
|
|
339
|
+
var RemTriggerStateError = class extends Error {
|
|
340
|
+
statePath;
|
|
341
|
+
constructor(statePath, cause) {
|
|
342
|
+
super(`REM trigger state at ${statePath} is invalid or unreadable: ${errorMessage$1(cause)}`, { cause: cause instanceof Error ? cause : void 0 });
|
|
343
|
+
this.statePath = statePath;
|
|
344
|
+
this.name = "RemTriggerStateError";
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
function remTriggerStatePath(stateDir) {
|
|
348
|
+
return path.join(stateDir, "rem-trigger-state.json");
|
|
349
|
+
}
|
|
350
|
+
async function loadRemTriggerState(stateDir) {
|
|
351
|
+
const statePath = remTriggerStatePath(stateDir);
|
|
352
|
+
let raw;
|
|
353
|
+
try {
|
|
354
|
+
raw = await readFile(statePath, "utf8");
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (isNodeError$1(error) && error.code === "ENOENT") return {
|
|
357
|
+
version: 1,
|
|
358
|
+
scopes: {}
|
|
359
|
+
};
|
|
360
|
+
throw new RemTriggerStateError(statePath, error);
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
return remTriggerStateDocumentSchema.parse(JSON.parse(raw));
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw new RemTriggerStateError(statePath, error);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function ensureRemTriggerScope(state, input) {
|
|
369
|
+
const existing = state.scopes[input.scope];
|
|
370
|
+
if (existing !== void 0) return {
|
|
371
|
+
state,
|
|
372
|
+
scopeState: existing,
|
|
373
|
+
initialized: false
|
|
374
|
+
};
|
|
375
|
+
if (input.scope.trim().length === 0) throw new Error("REM trigger scope is required");
|
|
376
|
+
if (!Number.isInteger(input.candidateCount) || input.candidateCount < 0) throw new Error("REM trigger candidate count must be a nonnegative integer");
|
|
377
|
+
if (Number.isNaN(input.now.getTime())) throw new Error("REM trigger initialization time is invalid");
|
|
378
|
+
const scheduleZone = (input.resolveScheduleZone ?? resolveHostScheduleZone)();
|
|
379
|
+
if (!isValidScheduleZone(scheduleZone)) throw new Error(`invalid REM schedule zone: ${scheduleZone}`);
|
|
380
|
+
const scopeState = {
|
|
381
|
+
last_pass_at: input.now.toISOString(),
|
|
382
|
+
schedule_zone: scheduleZone,
|
|
383
|
+
last_covered_count: input.candidateCount,
|
|
384
|
+
last_volume_pass_date: null,
|
|
385
|
+
missed_window: null,
|
|
386
|
+
attempts: {
|
|
387
|
+
identity: null,
|
|
388
|
+
count: 0
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
return {
|
|
392
|
+
state: {
|
|
393
|
+
version: 1,
|
|
394
|
+
scopes: {
|
|
395
|
+
...state.scopes,
|
|
396
|
+
[input.scope]: scopeState
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
scopeState,
|
|
400
|
+
initialized: true
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
async function writeRemTriggerStateAtomic$1(stateDir, state) {
|
|
404
|
+
const validated = remTriggerStateDocumentSchema.parse(state);
|
|
405
|
+
await mkdir(stateDir, {
|
|
406
|
+
recursive: true,
|
|
407
|
+
mode: 448
|
|
408
|
+
});
|
|
409
|
+
const statePath = remTriggerStatePath(stateDir);
|
|
410
|
+
const temporaryPath = `${statePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
411
|
+
try {
|
|
412
|
+
const handle = await open(temporaryPath, "wx", 384);
|
|
413
|
+
try {
|
|
414
|
+
await handle.writeFile(JSON.stringify(validated), "utf8");
|
|
415
|
+
await handle.sync();
|
|
416
|
+
} finally {
|
|
417
|
+
await handle.close();
|
|
418
|
+
}
|
|
419
|
+
await rename(temporaryPath, statePath);
|
|
420
|
+
await syncDirectory(stateDir);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
await rm(temporaryPath, { force: true });
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function resolveHostScheduleZone() {
|
|
427
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
428
|
+
}
|
|
429
|
+
function isValidScheduleZone(value) {
|
|
430
|
+
if (value.trim().length === 0) return false;
|
|
431
|
+
try {
|
|
432
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(0);
|
|
433
|
+
return true;
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function syncDirectory(directory) {
|
|
439
|
+
if (process.platform === "win32") return;
|
|
440
|
+
const handle = await open(directory, "r");
|
|
441
|
+
try {
|
|
442
|
+
await handle.sync();
|
|
443
|
+
} finally {
|
|
444
|
+
await handle.close();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function isNodeError$1(error) {
|
|
448
|
+
return error instanceof Error && "code" in error;
|
|
449
|
+
}
|
|
450
|
+
function errorMessage$1(error) {
|
|
451
|
+
return error instanceof Error ? error.message : String(error);
|
|
452
|
+
}
|
|
453
|
+
const REM_COMPLETION_AUDIT_TAIL_BYTES = 16777216;
|
|
454
|
+
const log$1 = createLogger("sno-station-mem:rem-trigger");
|
|
455
|
+
function remNeedsHost(mode, modelCalls) {
|
|
456
|
+
return Object.keys(modelCalls).some((id) => id.startsWith("REM") && modelCalls[id][mode] === "host");
|
|
457
|
+
}
|
|
458
|
+
const discoverySchema = z.object({
|
|
459
|
+
pid: z.number().int().positive(),
|
|
460
|
+
port: z.number().int().min(1).max(65535),
|
|
461
|
+
token: z.string().min(1)
|
|
462
|
+
}).strict();
|
|
463
|
+
const idleEvaluations = /* @__PURE__ */ new Map();
|
|
464
|
+
async function evaluateRemAutomaticTriggers(input) {
|
|
465
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
466
|
+
if (!Number.isInteger(input.volumeThreshold ?? 100) || (input.volumeThreshold ?? 100) < 1) throw new Error("REM volume threshold must be a positive integer");
|
|
467
|
+
const auditStateDir = input.auditStateDir ?? input.stateDir;
|
|
468
|
+
if (Number.isNaN(now.getTime())) throw new Error("REM trigger evaluation time is invalid");
|
|
469
|
+
if (input.requestedOperations.length === 0) {
|
|
470
|
+
await recordDecision(auditStateDir, void 0, "skipped", {
|
|
471
|
+
row: "automatic-trigger-skipped",
|
|
472
|
+
reason: "product-mode-disabled"
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
evaluations: 0,
|
|
476
|
+
dispatches: 0
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (input.mode && remNeedsHost(input.mode, input.modelCalls) && !input.hasConnectedHost?.()) {
|
|
480
|
+
await recordDecision(auditStateDir, void 0, "skipped", {
|
|
481
|
+
row: "automatic-trigger-skipped",
|
|
482
|
+
reason: "skipped: no host model connected"
|
|
483
|
+
});
|
|
484
|
+
return {
|
|
485
|
+
evaluations: 0,
|
|
486
|
+
dispatches: 0
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
let scopes;
|
|
490
|
+
try {
|
|
491
|
+
scopes = enumerateRemCandidateScopes(input.database);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
await recordDecision(auditStateDir, void 0, "error", {
|
|
494
|
+
row: "enumeration-failed",
|
|
495
|
+
cause: errorMessage(error)
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
evaluations: 1,
|
|
499
|
+
dispatches: 0
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (scopes.length === 0) {
|
|
503
|
+
await recordDecision(auditStateDir, void 0, "ok", { row: "no-scopes" });
|
|
504
|
+
return {
|
|
505
|
+
evaluations: 1,
|
|
506
|
+
dispatches: 0
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
let state;
|
|
510
|
+
let lostState = false;
|
|
511
|
+
try {
|
|
512
|
+
state = await loadRemTriggerState(input.stateDir);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
for (const { scope, candidateCount } of scopes) {
|
|
515
|
+
const consecutiveIdle = (idleEvaluations.get(scope) ?? 0) + 1;
|
|
516
|
+
idleEvaluations.set(scope, consecutiveIdle);
|
|
517
|
+
await recordDecision(auditStateDir, scope, "error", {
|
|
518
|
+
row: "state-unreadable",
|
|
519
|
+
candidate_count: candidateCount,
|
|
520
|
+
cause: errorMessage(error),
|
|
521
|
+
consecutive_idle: consecutiveIdle
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
log$1.error("REM trigger state unavailable; continuing with due work", { cause: errorMessage(error) }, {
|
|
525
|
+
event_name: "memory.rem.trigger.state.unavailable",
|
|
526
|
+
file: "packages/memory/src/sidecar/rem-trigger.ts",
|
|
527
|
+
function: "evaluateRemAutomaticTriggers",
|
|
528
|
+
site_id: "memory.rem.trigger.state.unavailable"
|
|
529
|
+
});
|
|
530
|
+
state = {
|
|
531
|
+
version: 1,
|
|
532
|
+
scopes: {}
|
|
533
|
+
};
|
|
534
|
+
lostState = true;
|
|
535
|
+
}
|
|
536
|
+
state = await applyCompletedBaselines(auditStateDir, input.stateDir, state);
|
|
537
|
+
let dispatches = 0;
|
|
538
|
+
for (const { scope, candidateCount } of scopes) try {
|
|
539
|
+
const ensured = ensureRemTriggerScope(state, {
|
|
540
|
+
scope,
|
|
541
|
+
now: lostState ? previousDay(now) : now,
|
|
542
|
+
candidateCount,
|
|
543
|
+
resolveScheduleZone: input.resolveScheduleZone
|
|
544
|
+
});
|
|
545
|
+
state = ensured.state;
|
|
546
|
+
if (ensured.initialized) await writeRemTriggerStateAtomic(input.stateDir, state);
|
|
547
|
+
const scopeResult = await evaluateScope(input, auditStateDir, state, scope, candidateCount, now);
|
|
548
|
+
state = scopeResult.state;
|
|
549
|
+
dispatches += scopeResult.dispatched ? 1 : 0;
|
|
550
|
+
} catch (error) {
|
|
551
|
+
log$1.error("REM automatic scope evaluation failed", {
|
|
552
|
+
scope,
|
|
553
|
+
error
|
|
554
|
+
}, {
|
|
555
|
+
event_name: "sno_station_mem.rem-trigger.rem.automatic.scope.evaluation.failed",
|
|
556
|
+
file: "packages/memory/src/sidecar/rem-trigger.ts",
|
|
557
|
+
function: "evaluateRemAutomaticTriggers",
|
|
558
|
+
site_id: "rem-trigger.evaluateRemAutomaticTriggers.1d59077a5c"
|
|
559
|
+
});
|
|
560
|
+
try {
|
|
561
|
+
await recordDecision(auditStateDir, scope, "error", {
|
|
562
|
+
row: "scope-evaluation-failed",
|
|
563
|
+
cause: errorMessage(error)
|
|
564
|
+
});
|
|
565
|
+
} catch (auditError) {
|
|
566
|
+
log$1.warn("REM automatic scope failure audit failed", {
|
|
567
|
+
scope,
|
|
568
|
+
error: auditError
|
|
569
|
+
}, {
|
|
570
|
+
event_name: "sno_station_mem.rem-trigger.rem.automatic.scope.failure.audit.failed",
|
|
571
|
+
file: "packages/memory/src/sidecar/rem-trigger.ts",
|
|
572
|
+
function: "evaluateRemAutomaticTriggers",
|
|
573
|
+
site_id: "rem-trigger.evaluateRemAutomaticTriggers.ebe1cdd204"
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
state = await loadRemTriggerState(input.stateDir);
|
|
578
|
+
} catch (stateError) {
|
|
579
|
+
log$1.warn("REM automatic trigger state reload failed", {
|
|
580
|
+
scope,
|
|
581
|
+
error: stateError
|
|
582
|
+
}, {
|
|
583
|
+
event_name: "sno_station_mem.rem-trigger.rem.automatic.trigger.state.reload.failed",
|
|
584
|
+
file: "packages/memory/src/sidecar/rem-trigger.ts",
|
|
585
|
+
function: "evaluateRemAutomaticTriggers",
|
|
586
|
+
site_id: "rem-trigger.evaluateRemAutomaticTriggers.46669e66ff"
|
|
587
|
+
});
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
evaluations: scopes.length,
|
|
593
|
+
dispatches
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function previousDay(now) {
|
|
597
|
+
const day = new Date(now);
|
|
598
|
+
day.setUTCDate(day.getUTCDate() - 1);
|
|
599
|
+
return day;
|
|
600
|
+
}
|
|
601
|
+
async function writeRemTriggerStateAtomic(stateDir, state) {
|
|
602
|
+
try {
|
|
603
|
+
await writeRemTriggerStateAtomic$1(stateDir, state);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
log$1.error("REM trigger state write failed; continuing dispatch", { cause: errorMessage(error) }, {
|
|
606
|
+
event_name: "memory.rem.trigger.state.write.failed",
|
|
607
|
+
file: "packages/memory/src/sidecar/rem-trigger.ts",
|
|
608
|
+
function: "evaluateRemAutomaticTriggers",
|
|
609
|
+
site_id: "memory.rem.trigger.state.write.failed"
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function evaluateScope(input, auditStateDir, state, scope, candidateCount, now) {
|
|
614
|
+
const scopeState = requiredScopeState(state, scope);
|
|
615
|
+
const zone = scopeState.schedule_zone;
|
|
616
|
+
const nextDue = nextDailyDue(scopeState);
|
|
617
|
+
const localDate = localDateAt(now, zone);
|
|
618
|
+
const growth = candidateCount - scopeState.last_covered_count;
|
|
619
|
+
const scheduledDue = computeRemDailyDue(scopeState.last_pass_at, zone);
|
|
620
|
+
const pendingVolumeToday = scopeState.attempts.identity === remAutomaticCorrelationId("volume", scope, localDate);
|
|
621
|
+
const pendingDailyToday = localDateAt(scheduledDue, zone) === localDate && scopeState.attempts.identity === remAutomaticCorrelationId("daily", scope, scheduledDue.toISOString());
|
|
622
|
+
const dailyDue = now.getTime() >= nextDue.getTime() && !pendingVolumeToday;
|
|
623
|
+
const volumeDue = growth >= (input.volumeThreshold ?? 100) && scopeState.last_volume_pass_date !== localDate && !pendingDailyToday;
|
|
624
|
+
if (!dailyDue && !volumeDue) {
|
|
625
|
+
const consecutiveIdle = (idleEvaluations.get(scope) ?? 0) + 1;
|
|
626
|
+
idleEvaluations.set(scope, consecutiveIdle);
|
|
627
|
+
await recordDecision(auditStateDir, scope, "ok", {
|
|
628
|
+
row: "waiting-for-schedule",
|
|
629
|
+
next_due: nextDue.toISOString(),
|
|
630
|
+
milliseconds_until_due: Math.max(0, nextDue.getTime() - now.getTime()),
|
|
631
|
+
growth: growthDetails(candidateCount, scopeState.last_covered_count, input.volumeThreshold ?? 100),
|
|
632
|
+
consecutive_idle: consecutiveIdle
|
|
633
|
+
});
|
|
634
|
+
return {
|
|
635
|
+
state,
|
|
636
|
+
dispatched: false
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const trigger = dailyDue ? "daily" : "volume";
|
|
640
|
+
if (input.tickEnabled === false) {
|
|
641
|
+
const nextState = replaceScopeState(state, scope, {
|
|
642
|
+
...scopeState,
|
|
643
|
+
missed_window: {
|
|
644
|
+
due_at: dailyDue ? nextDue.toISOString() : now.toISOString(),
|
|
645
|
+
trigger,
|
|
646
|
+
recorded_at: now.toISOString()
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
await writeRemTriggerStateAtomic(input.stateDir, nextState);
|
|
650
|
+
return {
|
|
651
|
+
state: nextState,
|
|
652
|
+
dispatched: false
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const correlationId = remAutomaticCorrelationId(trigger, scope, trigger === "daily" ? nextDue.toISOString() : localDate);
|
|
656
|
+
const priorAttempts = scopeState.attempts.identity === correlationId ? scopeState.attempts.count : 0;
|
|
657
|
+
await recordDecision(auditStateDir, scope, "ok", {
|
|
658
|
+
row: "dispatch",
|
|
659
|
+
trigger,
|
|
660
|
+
next_due: nextDue.toISOString(),
|
|
661
|
+
pass_at: now.toISOString(),
|
|
662
|
+
local_date: localDate,
|
|
663
|
+
growth: growthDetails(candidateCount, scopeState.last_covered_count, input.volumeThreshold ?? 100),
|
|
664
|
+
correlation_id: correlationId,
|
|
665
|
+
consecutive_idle: 0
|
|
666
|
+
});
|
|
667
|
+
idleEvaluations.set(scope, 0);
|
|
668
|
+
let nextState = replaceScopeState(state, scope, {
|
|
669
|
+
...scopeState,
|
|
670
|
+
attempts: {
|
|
671
|
+
identity: correlationId,
|
|
672
|
+
count: Math.min(priorAttempts + 1, 3)
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
await writeRemTriggerStateAtomic(input.stateDir, nextState);
|
|
676
|
+
let cause;
|
|
677
|
+
try {
|
|
678
|
+
await dispatchRemWave(input, scope, correlationId);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
cause = errorMessage(error);
|
|
681
|
+
}
|
|
682
|
+
if (cause === void 0) {
|
|
683
|
+
nextState = replaceScopeState(nextState, scope, {
|
|
684
|
+
...requiredScopeState(nextState, scope),
|
|
685
|
+
missed_window: null
|
|
686
|
+
});
|
|
687
|
+
await writeRemTriggerStateAtomic(input.stateDir, nextState);
|
|
688
|
+
return {
|
|
689
|
+
state: nextState,
|
|
690
|
+
dispatched: true
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
await appendAuditEntryStrict(auditStateDir, {
|
|
694
|
+
event: "rem_failed",
|
|
695
|
+
resultStatus: "error",
|
|
696
|
+
scope,
|
|
697
|
+
details: {
|
|
698
|
+
outcome: "dispatch-failed",
|
|
699
|
+
cause,
|
|
700
|
+
correlation_id: correlationId,
|
|
701
|
+
source: "gateway-trigger"
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
return {
|
|
705
|
+
state: nextState,
|
|
706
|
+
dispatched: true
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
async function dispatchRemWave(input, scope, correlationId) {
|
|
710
|
+
const started = performance.now();
|
|
711
|
+
const discovery = discoverySchema.parse(JSON.parse(await readFile(input.discoveryPath ?? getRemDiscoveryPath(), "utf8")));
|
|
712
|
+
const response = await fetch(`http://${REM_SIDECAR_HOST}:${discovery.port}${REM_RUN_PATH}`, {
|
|
713
|
+
method: "POST",
|
|
714
|
+
headers: {
|
|
715
|
+
"content-type": "application/json",
|
|
716
|
+
[REM_SIDECAR_TOKEN_HEADER]: discovery.token,
|
|
717
|
+
[REM_CORRELATION_ID_HEADER]: correlationId
|
|
718
|
+
},
|
|
719
|
+
body: JSON.stringify({
|
|
720
|
+
types: input.requestedOperations,
|
|
721
|
+
scope
|
|
722
|
+
}),
|
|
723
|
+
signal: AbortSignal.timeout(input.dispatchTimeoutMs ?? 1e4)
|
|
724
|
+
});
|
|
725
|
+
if (response.status !== 202) throw new Error(`REM sidecar returned HTTP ${response.status}`);
|
|
726
|
+
const body = z.object({
|
|
727
|
+
job_id: z.string().min(1),
|
|
728
|
+
waveId: z.string().min(1)
|
|
729
|
+
}).passthrough().parse(await response.json());
|
|
730
|
+
if (body.job_id !== body.waveId) throw new Error("REM sidecar confirmation identities disagree");
|
|
731
|
+
withLogContext$1({
|
|
732
|
+
operation_id: body.job_id,
|
|
733
|
+
job_id: body.job_id,
|
|
734
|
+
session_reference: scope,
|
|
735
|
+
external_reference: correlationId
|
|
736
|
+
}, () => {
|
|
737
|
+
log$1.info("REM dispatch accepted", {
|
|
738
|
+
outcome: "success",
|
|
739
|
+
job_id: body.job_id,
|
|
740
|
+
status: response.status,
|
|
741
|
+
duration_ms: performance.now() - started
|
|
742
|
+
}, {
|
|
743
|
+
event_name: "sidecar.trigger.dispatch.accepted",
|
|
744
|
+
file: "packages/memory/src/sidecar/rem-trigger.ts",
|
|
745
|
+
function: "dispatchRemWave",
|
|
746
|
+
site_id: "sidecar.trigger.dispatch.accepted"
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async function applyCompletedBaselines(auditStateDir, triggerStateDir, state) {
|
|
751
|
+
let content;
|
|
752
|
+
try {
|
|
753
|
+
content = await readAuditTail(getAuditPath(auditStateDir));
|
|
754
|
+
} catch (error) {
|
|
755
|
+
if (isNodeError(error) && error.code === "ENOENT") return state;
|
|
756
|
+
log$1.warn("REM completion audit could not be read", {
|
|
757
|
+
error,
|
|
758
|
+
outcome: "skipped",
|
|
759
|
+
reason_code: "audit_read_failed"
|
|
760
|
+
}, {
|
|
761
|
+
event_name: "sidecar.trigger.audit_read.failed",
|
|
762
|
+
file: "packages/memory/src/sidecar/rem-trigger.ts",
|
|
763
|
+
function: "applyCompletedBaselines",
|
|
764
|
+
site_id: "sidecar.trigger.audit_read.failed"
|
|
765
|
+
});
|
|
766
|
+
return state;
|
|
767
|
+
}
|
|
768
|
+
const dispatched = /* @__PURE__ */ new Map();
|
|
769
|
+
let nextState = state;
|
|
770
|
+
let malformedCount = 0;
|
|
771
|
+
for (const line of content.split("\n")) {
|
|
772
|
+
if (line.trim() === "") continue;
|
|
773
|
+
try {
|
|
774
|
+
const entry = JSON.parse(line);
|
|
775
|
+
const details = asRecord(entry["details"]);
|
|
776
|
+
const correlationId = details?.["correlation_id"];
|
|
777
|
+
const scope = entry["scope"];
|
|
778
|
+
if (entry["event"] === "rem_trigger_evaluated" && details?.["row"] === "dispatch" && typeof correlationId === "string" && typeof scope === "string") {
|
|
779
|
+
const trigger = details["trigger"];
|
|
780
|
+
const passAt = details["pass_at"];
|
|
781
|
+
const localDate = details["local_date"];
|
|
782
|
+
dispatched.set(correlationId, {
|
|
783
|
+
scope,
|
|
784
|
+
...trigger === "daily" || trigger === "volume" ? { trigger } : {},
|
|
785
|
+
...typeof passAt === "string" ? { passAt } : {},
|
|
786
|
+
...typeof localDate === "string" ? { localDate } : {}
|
|
787
|
+
});
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (entry["event"] === "rem_skipped" && typeof correlationId === "string") {
|
|
791
|
+
const skipped = dispatched.get(correlationId);
|
|
792
|
+
if (skipped && nextState.scopes[skipped.scope]?.attempts.identity === correlationId) nextState = replaceScopeState(nextState, skipped.scope, {
|
|
793
|
+
...requiredScopeState(nextState, skipped.scope),
|
|
794
|
+
attempts: {
|
|
795
|
+
identity: null,
|
|
796
|
+
count: 0
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (entry["event"] !== "rem_completed" || typeof correlationId !== "string") continue;
|
|
802
|
+
const dispatch = dispatched.get(correlationId);
|
|
803
|
+
if (dispatch === void 0 || state.scopes[dispatch.scope] === void 0 || entry["scope"] !== dispatch.scope) continue;
|
|
804
|
+
const currentScopeState = requiredScopeState(nextState, dispatch.scope);
|
|
805
|
+
let completedScopeState = {
|
|
806
|
+
...currentScopeState,
|
|
807
|
+
...dispatch.trigger === "daily" && isValidInstant(dispatch.passAt) ? { last_pass_at: dispatch.passAt } : {},
|
|
808
|
+
...dispatch.trigger === "daily" && isLocalDate(dispatch.localDate) ? { last_volume_pass_date: dispatch.localDate } : {},
|
|
809
|
+
...dispatch.trigger === "volume" && isLocalDate(dispatch.localDate) ? { last_volume_pass_date: dispatch.localDate } : {},
|
|
810
|
+
...currentScopeState.attempts.identity === correlationId ? { attempts: {
|
|
811
|
+
identity: null,
|
|
812
|
+
count: 0
|
|
813
|
+
} } : {}
|
|
814
|
+
};
|
|
815
|
+
const measured = asRecord(asRecord(details?.["stats"])?.["measured"]);
|
|
816
|
+
const rowsConsidered = measured?.["rows_considered"];
|
|
817
|
+
if (measured?.["pair_cap_binding"] === false && Number.isInteger(rowsConsidered) && Number(rowsConsidered) >= 0) completedScopeState = {
|
|
818
|
+
...completedScopeState,
|
|
819
|
+
last_covered_count: Number(rowsConsidered)
|
|
820
|
+
};
|
|
821
|
+
if (JSON.stringify(completedScopeState) === JSON.stringify(currentScopeState)) continue;
|
|
822
|
+
nextState = replaceScopeState(nextState, dispatch.scope, completedScopeState);
|
|
823
|
+
} catch {
|
|
824
|
+
malformedCount += 1;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (malformedCount > 0) log$1.warn("Malformed REM audit records skipped", {
|
|
828
|
+
malformed_count: malformedCount,
|
|
829
|
+
outcome: "partial"
|
|
830
|
+
}, {
|
|
831
|
+
event_name: "sidecar.trigger.audit_records.skipped",
|
|
832
|
+
file: "packages/memory/src/sidecar/rem-trigger.ts",
|
|
833
|
+
function: "applyCompletedBaselines",
|
|
834
|
+
site_id: "sidecar.trigger.audit_records.skipped"
|
|
835
|
+
});
|
|
836
|
+
if (nextState !== state) await writeRemTriggerStateAtomic(triggerStateDir, nextState);
|
|
837
|
+
return nextState;
|
|
838
|
+
}
|
|
839
|
+
async function readAuditTail(auditPath) {
|
|
840
|
+
const handle = await open(auditPath, "r");
|
|
841
|
+
try {
|
|
842
|
+
const size = (await handle.stat()).size;
|
|
843
|
+
const bytesToRead = Math.min(size, REM_COMPLETION_AUDIT_TAIL_BYTES);
|
|
844
|
+
if (bytesToRead === 0) return "";
|
|
845
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
846
|
+
const { bytesRead } = await handle.read(buffer, 0, bytesToRead, size - bytesToRead);
|
|
847
|
+
let content = buffer.subarray(0, bytesRead).toString("utf8");
|
|
848
|
+
if (size > bytesToRead) {
|
|
849
|
+
const firstLineEnd = content.indexOf("\n");
|
|
850
|
+
content = firstLineEnd === -1 ? "" : content.slice(firstLineEnd + 1);
|
|
851
|
+
}
|
|
852
|
+
return content;
|
|
853
|
+
} finally {
|
|
854
|
+
await handle.close();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function remAutomaticCorrelationId(trigger, scope, triggerKey) {
|
|
858
|
+
return `rem-auto-${trigger}-${createHash("sha256").update(JSON.stringify([
|
|
859
|
+
"rem-automatic-v1",
|
|
860
|
+
trigger,
|
|
861
|
+
scope,
|
|
862
|
+
triggerKey
|
|
863
|
+
])).digest("hex")}`;
|
|
864
|
+
}
|
|
865
|
+
/** The daily due time, pushed to the schedule after the last used-up local day. */
|
|
866
|
+
function nextDailyDue(scopeState) {
|
|
867
|
+
const scheduled = computeRemDailyDue(scopeState.last_pass_at, scopeState.schedule_zone);
|
|
868
|
+
const volumeDate = scopeState.last_volume_pass_date;
|
|
869
|
+
if (volumeDate === null) return scheduled;
|
|
870
|
+
const parsed = /^(\d{4})-(\d{2})-(\d{2})$/u.exec(volumeDate);
|
|
871
|
+
if (parsed === null) return scheduled;
|
|
872
|
+
const following = addCalendarDays({
|
|
873
|
+
year: Number(parsed[1]),
|
|
874
|
+
month: Number(parsed[2]),
|
|
875
|
+
day: Number(parsed[3])
|
|
876
|
+
}, 1);
|
|
877
|
+
const afterVolume = zonedInstant(scopeState.schedule_zone, following.year, following.month, following.day);
|
|
878
|
+
return afterVolume.getTime() > scheduled.getTime() ? afterVolume : scheduled;
|
|
879
|
+
}
|
|
880
|
+
function computeRemDailyDue(lastPassAt, scheduleZone) {
|
|
881
|
+
const lastPass = new Date(lastPassAt);
|
|
882
|
+
if (Number.isNaN(lastPass.getTime())) throw new Error("REM last pass time is invalid");
|
|
883
|
+
const localDate = localDateParts(lastPass, scheduleZone);
|
|
884
|
+
let due = zonedInstant(scheduleZone, localDate.year, localDate.month, localDate.day);
|
|
885
|
+
if (due.getTime() <= lastPass.getTime()) {
|
|
886
|
+
const nextDate = addCalendarDays(localDate, 1);
|
|
887
|
+
due = zonedInstant(scheduleZone, nextDate.year, nextDate.month, nextDate.day);
|
|
888
|
+
}
|
|
889
|
+
return due;
|
|
890
|
+
}
|
|
891
|
+
function zonedInstant(zone, year, month, day) {
|
|
892
|
+
const target = Date.UTC(year, month - 1, day, 3, 0, 0, 0);
|
|
893
|
+
let instant = target;
|
|
894
|
+
let firstExistingAfter;
|
|
895
|
+
for (let iteration = 0; iteration < 5; iteration++) {
|
|
896
|
+
const parts = zonedDateTimeParts(new Date(instant), zone);
|
|
897
|
+
const represented = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
898
|
+
const adjustment = target - represented;
|
|
899
|
+
if (adjustment === 0) return new Date(instant);
|
|
900
|
+
if (represented > target && represented < (firstExistingAfter?.represented ?? Number.POSITIVE_INFINITY)) firstExistingAfter = {
|
|
901
|
+
instant,
|
|
902
|
+
represented
|
|
903
|
+
};
|
|
904
|
+
instant += adjustment;
|
|
905
|
+
}
|
|
906
|
+
if (firstExistingAfter !== void 0) return new Date(firstExistingAfter.instant);
|
|
907
|
+
throw new Error(`REM scheduled instant cannot be resolved in zone ${zone}`);
|
|
908
|
+
}
|
|
909
|
+
function localDateAt(instant, zone) {
|
|
910
|
+
const parts = localDateParts(instant, zone);
|
|
911
|
+
return `${parts.year.toString().padStart(4, "0")}-${parts.month.toString().padStart(2, "0")}-${parts.day.toString().padStart(2, "0")}`;
|
|
912
|
+
}
|
|
913
|
+
function localDateParts(instant, zone) {
|
|
914
|
+
const parts = zonedDateTimeParts(instant, zone);
|
|
915
|
+
return {
|
|
916
|
+
year: parts.year,
|
|
917
|
+
month: parts.month,
|
|
918
|
+
day: parts.day
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function zonedDateTimeParts(instant, zone) {
|
|
922
|
+
const values = {};
|
|
923
|
+
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
924
|
+
timeZone: zone,
|
|
925
|
+
year: "numeric",
|
|
926
|
+
month: "2-digit",
|
|
927
|
+
day: "2-digit",
|
|
928
|
+
hour: "2-digit",
|
|
929
|
+
minute: "2-digit",
|
|
930
|
+
second: "2-digit",
|
|
931
|
+
hourCycle: "h23"
|
|
932
|
+
});
|
|
933
|
+
for (const part of formatter.formatToParts(instant)) if (part.type !== "literal") values[part.type] = Number(part.value);
|
|
934
|
+
const year = values["year"];
|
|
935
|
+
const month = values["month"];
|
|
936
|
+
const day = values["day"];
|
|
937
|
+
const hour = values["hour"];
|
|
938
|
+
const minute = values["minute"];
|
|
939
|
+
const second = values["second"];
|
|
940
|
+
if ([
|
|
941
|
+
year,
|
|
942
|
+
month,
|
|
943
|
+
day,
|
|
944
|
+
hour,
|
|
945
|
+
minute,
|
|
946
|
+
second
|
|
947
|
+
].some((value) => value === void 0)) throw new Error(`REM schedule zone ${zone} did not produce complete date parts`);
|
|
948
|
+
return {
|
|
949
|
+
year: Number(year),
|
|
950
|
+
month: Number(month),
|
|
951
|
+
day: Number(day),
|
|
952
|
+
hour: Number(hour),
|
|
953
|
+
minute: Number(minute),
|
|
954
|
+
second: Number(second)
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function addCalendarDays(date, days) {
|
|
958
|
+
const instant = new Date(Date.UTC(date.year, date.month - 1, date.day + days));
|
|
959
|
+
return {
|
|
960
|
+
year: instant.getUTCFullYear(),
|
|
961
|
+
month: instant.getUTCMonth() + 1,
|
|
962
|
+
day: instant.getUTCDate()
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function growthDetails(candidateCount, coveredCount, threshold) {
|
|
966
|
+
const growth = candidateCount - coveredCount;
|
|
967
|
+
return {
|
|
968
|
+
candidate_count: candidateCount,
|
|
969
|
+
last_covered_count: coveredCount,
|
|
970
|
+
delta: growth,
|
|
971
|
+
threshold,
|
|
972
|
+
remaining: Math.max(0, threshold - growth)
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
async function recordDecision(stateDir, scope, resultStatus, details) {
|
|
976
|
+
await appendAuditEntryStrict(stateDir, {
|
|
977
|
+
event: "rem_trigger_evaluated",
|
|
978
|
+
resultStatus,
|
|
979
|
+
...scope === void 0 ? {} : { scope },
|
|
980
|
+
details
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
function requiredScopeState(state, scope) {
|
|
984
|
+
const scopeState = state.scopes[scope];
|
|
985
|
+
if (scopeState === void 0) throw new Error(`REM trigger state missing scope ${scope}`);
|
|
986
|
+
return scopeState;
|
|
987
|
+
}
|
|
988
|
+
function replaceScopeState(state, scope, scopeState) {
|
|
989
|
+
return {
|
|
990
|
+
version: 1,
|
|
991
|
+
scopes: {
|
|
992
|
+
...state.scopes,
|
|
993
|
+
[scope]: scopeState
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function asRecord(value) {
|
|
998
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
999
|
+
}
|
|
1000
|
+
function isValidInstant(value) {
|
|
1001
|
+
return value !== void 0 && !Number.isNaN(new Date(value).getTime());
|
|
1002
|
+
}
|
|
1003
|
+
function isLocalDate(value) {
|
|
1004
|
+
return value !== void 0 && /^\d{4}-\d{2}-\d{2}$/u.test(value);
|
|
1005
|
+
}
|
|
1006
|
+
function isNodeError(error) {
|
|
1007
|
+
return error instanceof Error && "code" in error;
|
|
1008
|
+
}
|
|
1009
|
+
function errorMessage(error) {
|
|
1010
|
+
return error instanceof Error ? error.message : String(error);
|
|
1011
|
+
}
|
|
1012
|
+
//#endregion
|
|
1013
|
+
//#region src/store/maintenance.ts
|
|
1014
|
+
/** @file maintenance.ts
|
|
1015
|
+
* @purpose Ordered maintenance pass: integrity check, outbox drain, usage-event retention, FTS
|
|
1016
|
+
* merge, planner statistics, backup, REM trigger check. Each job runs on its own interval.
|
|
1017
|
+
* @boundary Owns the gateway maintenance timer; integrity failures are logged without blocking storage.
|
|
1018
|
+
* @see backup.ts, memory-telemetry-outbox.ts, sqlite-runtime.ts.
|
|
1019
|
+
*/
|
|
1020
|
+
const log = createLogger("sno-station-mem:maintenance");
|
|
1021
|
+
const DAY_MS = 864e5;
|
|
1022
|
+
/**
|
|
1023
|
+
* Usage events ('recall'/'inject') older than this are pruned from the
|
|
1024
|
+
* `nodix_memory_events` ledger. MUST stay >= the purge-safety recall lookback
|
|
1025
|
+
* (RECENT_RECALL_WINDOW_MS, 30 d) — coupled by a unit test — so retention can
|
|
1026
|
+
* never erase evidence the cascade-purge preview depends on. Lifecycle events
|
|
1027
|
+
* are never pruned (database-enforced by the guarded delete trigger).
|
|
1028
|
+
*/
|
|
1029
|
+
const MEMORY_EVENTS_USAGE_RETENTION_MS = 90 * DAY_MS;
|
|
1030
|
+
/** Quarantined outbox rows older than this are deleted by the maintenance pass. */
|
|
1031
|
+
const OUTBOX_QUARANTINE_RETENTION_MS = 30 * DAY_MS;
|
|
1032
|
+
/** The first tick runs shortly after boot, so every job gets near-boot coverage. */
|
|
1033
|
+
const MAINTENANCE_FIRST_TICK_DELAY_MS = 6e4;
|
|
1034
|
+
/** Each job's own cadence; see the MAINTENANCE section of config/index.ts. */
|
|
1035
|
+
const MAINTENANCE_INTERVALS = {
|
|
1036
|
+
integrity: INTEGRITY_CHECK_INTERVAL_MS,
|
|
1037
|
+
"usage-outbox": USAGE_OUTBOX_INTERVAL_MS,
|
|
1038
|
+
"usage-retention": USAGE_EVENT_RETENTION_INTERVAL_MS,
|
|
1039
|
+
"fts-merge": FTS_MERGE_INTERVAL_MS,
|
|
1040
|
+
"planner-statistics": PLANNER_STATISTICS_INTERVAL_MS,
|
|
1041
|
+
backup: BACKUP_INTERVAL_MS,
|
|
1042
|
+
"rem-trigger": REM_TRIGGER_CHECK_INTERVAL_MS
|
|
1043
|
+
};
|
|
1044
|
+
const ALL_MAINTENANCE_JOBS = new Set(Object.keys(MAINTENANCE_INTERVALS));
|
|
1045
|
+
/** Every job on one interval — what the sidecar's maintenance-interval override asks for. */
|
|
1046
|
+
function uniformMaintenanceIntervals(intervalMs) {
|
|
1047
|
+
return Object.fromEntries([...ALL_MAINTENANCE_JOBS].map((job) => [job, intervalMs]));
|
|
1048
|
+
}
|
|
1049
|
+
const OUTBOX_DRAIN_BUDGET_MS = 3e4;
|
|
1050
|
+
const RETENTION_DELETE_BATCH = 5e3;
|
|
1051
|
+
const RETENTION_PRUNE_BUDGET_MS = 5e3;
|
|
1052
|
+
const FTS_MERGE_MAX_ITERATIONS = 10;
|
|
1053
|
+
function integrityMessages(rawDb, table) {
|
|
1054
|
+
const pragma = table ? `integrity_check('${table}')` : "integrity_check";
|
|
1055
|
+
return rawDb.pragma(pragma).map((row) => row["integrity_check"]).filter((value) => typeof value === "string");
|
|
1056
|
+
}
|
|
1057
|
+
function isCleanIntegrityResult(messages) {
|
|
1058
|
+
return messages.length === 1 && messages[0] === "ok";
|
|
1059
|
+
}
|
|
1060
|
+
function hashInventoryRows(hash, rawDb, table) {
|
|
1061
|
+
hash.update(`${table}\0`);
|
|
1062
|
+
const rows = rawDb.prepare(`SELECT rowid, * FROM ${table} ORDER BY rowid`).iterate();
|
|
1063
|
+
for (const row of rows) hash.update(JSON.stringify(row)).update("\0");
|
|
1064
|
+
}
|
|
1065
|
+
function sourceInventory(rawDb) {
|
|
1066
|
+
const hash = createHash("sha256");
|
|
1067
|
+
hashInventoryRows(hash, rawDb, "nodix_memories");
|
|
1068
|
+
hashInventoryRows(hash, rawDb, "nodix_memory_chunks");
|
|
1069
|
+
return hash.digest("hex");
|
|
1070
|
+
}
|
|
1071
|
+
function isDerivedFtsOnlyFailure(rawDb) {
|
|
1072
|
+
const fullMessages = integrityMessages(rawDb);
|
|
1073
|
+
if (isCleanIntegrityResult(fullMessages) || fullMessages.some((message) => !/fts5:.*nodix_memory_chunks_fts/i.test(message))) return false;
|
|
1074
|
+
if (!isCleanIntegrityResult(integrityMessages(rawDb, "nodix_memories"))) return false;
|
|
1075
|
+
if (!isCleanIntegrityResult(integrityMessages(rawDb, "nodix_memory_chunks"))) return false;
|
|
1076
|
+
try {
|
|
1077
|
+
rawDb.exec("INSERT INTO nodix_memory_chunks_fts(nodix_memory_chunks_fts, rank) VALUES('integrity-check', 1)");
|
|
1078
|
+
return false;
|
|
1079
|
+
} catch {
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
function recordIntegrityAudit(stateDir, decision, resultStatus, details) {
|
|
1084
|
+
appendAuditEntry(stateDir, {
|
|
1085
|
+
event: "storage_integrity",
|
|
1086
|
+
resultStatus,
|
|
1087
|
+
decision,
|
|
1088
|
+
details
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
function tryRecoverDerivedFts(deps) {
|
|
1092
|
+
return deps.store.sqlite.runRecoveryOperation((rawDb) => recoverDerivedFts(deps, rawDb));
|
|
1093
|
+
}
|
|
1094
|
+
function recoverDerivedFts(deps, rawDb) {
|
|
1095
|
+
let inventoryBefore;
|
|
1096
|
+
try {
|
|
1097
|
+
if (!isDerivedFtsOnlyFailure(rawDb)) {
|
|
1098
|
+
recordIntegrityAudit(deps.stateDir, "retained", "error", { reason: "integrity failure is not provably confined to the derived FTS index" });
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
inventoryBefore = sourceInventory(rawDb);
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
recordIntegrityAudit(deps.stateDir, "retained", "error", { reason: error instanceof Error ? error.message : String(error) });
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
recordIntegrityAudit(deps.stateDir, "rebuild_started", "ok");
|
|
1107
|
+
log.warn("rebuilding derived FTS index after verified derived-only integrity failure", void 0, {
|
|
1108
|
+
event_name: "sno_station_mem.maintenance.rebuilding.derived.fts.index.after.verified.derived.only.integrity.fai",
|
|
1109
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1110
|
+
function: "recoverDerivedFts",
|
|
1111
|
+
site_id: "maintenance.recoverDerivedFts.753697bd94"
|
|
1112
|
+
});
|
|
1113
|
+
try {
|
|
1114
|
+
rawDb.exec("INSERT INTO nodix_memory_chunks_fts(nodix_memory_chunks_fts) VALUES('rebuild')");
|
|
1115
|
+
if (sourceInventory(rawDb) !== inventoryBefore) throw new Error("source inventory changed during derived FTS rebuild");
|
|
1116
|
+
runIntegrityCheck(rawDb);
|
|
1117
|
+
rawDb.exec("INSERT INTO nodix_memory_chunks_fts(nodix_memory_chunks_fts, rank) VALUES('integrity-check', 1)");
|
|
1118
|
+
recordIntegrityAudit(deps.stateDir, "recovered", "ok");
|
|
1119
|
+
log.info("derived FTS index rebuilt and integrity reverified", void 0, {
|
|
1120
|
+
event_name: "sno_station_mem.maintenance.derived.fts.index.rebuilt.and.integrity.reverified",
|
|
1121
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1122
|
+
function: "recoverDerivedFts",
|
|
1123
|
+
site_id: "maintenance.recoverDerivedFts.2cf5f62053"
|
|
1124
|
+
});
|
|
1125
|
+
return true;
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
recordIntegrityAudit(deps.stateDir, "retained", "error", { reason: error instanceof Error ? error.message : String(error) });
|
|
1128
|
+
log.error("derived FTS recovery failed; storage continues serving", { error }, {
|
|
1129
|
+
event_name: "memory.storage.fts.recovery.failed",
|
|
1130
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1131
|
+
function: "recoverDerivedFts",
|
|
1132
|
+
site_id: "maintenance.recoverDerivedFts.c07ad948a2"
|
|
1133
|
+
});
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/** Records an integrity failure without blocking subsequent SQL. */
|
|
1138
|
+
function recordIntegrityFailure(deps, error) {
|
|
1139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1140
|
+
try {
|
|
1141
|
+
recordMemoryTelemetryIncident(deps.store.sqlite, {
|
|
1142
|
+
incidentType: "storage_integrity_check_failed",
|
|
1143
|
+
severity: "error",
|
|
1144
|
+
message: "database integrity check failed; storage continues serving",
|
|
1145
|
+
payload: {
|
|
1146
|
+
error_code: "integrity_check_failed",
|
|
1147
|
+
last_error: message.slice(0, 500)
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
} catch {}
|
|
1151
|
+
log.error("storage continues serving after integrity failure", { error: message }, {
|
|
1152
|
+
event_name: "memory.storage.integrity.failed",
|
|
1153
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1154
|
+
function: "recordIntegrityFailure",
|
|
1155
|
+
site_id: "maintenance.failClosed.dd7955e647"
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Bounded catch-up: a backlog left by a maintenance gap or a first-time
|
|
1160
|
+
* retention rollout can be far larger than one tick's budget. Deleting it
|
|
1161
|
+
* all synchronously would block the gateway event loop for minutes (host
|
|
1162
|
+
* adversarial review 2026-07-13). Leave the remainder for later ticks —
|
|
1163
|
+
* safe, since usage-event retention is a hygiene job, not a safety
|
|
1164
|
+
* invariant (the purge-safety recall lookback only needs the last 30 days,
|
|
1165
|
+
* well inside this 90-day window either way).
|
|
1166
|
+
*/
|
|
1167
|
+
function pruneExpiredUsageEvents(store, now, budgetMs) {
|
|
1168
|
+
const cutoff = now - MEMORY_EVENTS_USAGE_RETENTION_MS;
|
|
1169
|
+
const statement = store.sqlite.prepare(`DELETE FROM nodix_memory_events
|
|
1170
|
+
WHERE id IN (
|
|
1171
|
+
SELECT id FROM nodix_memory_events
|
|
1172
|
+
WHERE event_type IN ('recall', 'inject') AND timestamp_ms < ?
|
|
1173
|
+
LIMIT ${RETENTION_DELETE_BATCH}
|
|
1174
|
+
)`);
|
|
1175
|
+
const deadline = Date.now() + budgetMs;
|
|
1176
|
+
let pruned = 0;
|
|
1177
|
+
do {
|
|
1178
|
+
const changes = statement.run(cutoff).changes ?? 0;
|
|
1179
|
+
pruned += changes;
|
|
1180
|
+
if (changes === 0) break;
|
|
1181
|
+
} while (Date.now() < deadline);
|
|
1182
|
+
return pruned;
|
|
1183
|
+
}
|
|
1184
|
+
function mergeFtsSegments(store) {
|
|
1185
|
+
if (!store.hasFtsSupport) return;
|
|
1186
|
+
const statement = store.sqlite.prepare("INSERT INTO nodix_memory_chunks_fts(nodix_memory_chunks_fts, rank) VALUES('merge', 500)");
|
|
1187
|
+
for (let i = 0; i < FTS_MERGE_MAX_ITERATIONS; i++) if ((statement.run().changes ?? 0) === 0) break;
|
|
1188
|
+
}
|
|
1189
|
+
/** Full integrity check and optional repair; reads and writes stay available. */
|
|
1190
|
+
function sweepIntegrity(deps, report) {
|
|
1191
|
+
const started = Date.now();
|
|
1192
|
+
try {
|
|
1193
|
+
const sweep = deps.integrityCheck ?? runIntegrityCheck;
|
|
1194
|
+
deps.store.sqlite.runRecoveryOperation((raw) => raw.transaction(() => {
|
|
1195
|
+
if (deps.store.hasFtsSupport) raw.prepare("SELECT rowid FROM nodix_memory_chunks_fts LIMIT 1").get();
|
|
1196
|
+
sweep(raw);
|
|
1197
|
+
}).deferred());
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
recordIntegrityFailure(deps, error);
|
|
1200
|
+
report.integrityRecovery = tryRecoverDerivedFts(deps) ? "recovered" : "retained";
|
|
1201
|
+
} finally {
|
|
1202
|
+
report.integrityMs = Date.now() - started;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
/** Runs the requested maintenance jobs independently. */
|
|
1206
|
+
function runMaintenancePass(deps, due = ALL_MAINTENANCE_JOBS, backupIntervalMs = BACKUP_INTERVAL_MS) {
|
|
1207
|
+
const report = {
|
|
1208
|
+
aborted: false,
|
|
1209
|
+
integrityRecovery: "none",
|
|
1210
|
+
integrityMs: 0,
|
|
1211
|
+
outboxFlushed: 0,
|
|
1212
|
+
quarantinePruned: 0,
|
|
1213
|
+
usageEventsPruned: 0,
|
|
1214
|
+
backupPath: void 0
|
|
1215
|
+
};
|
|
1216
|
+
if (deps.store.closed) {
|
|
1217
|
+
report.aborted = true;
|
|
1218
|
+
log.info("stopping maintenance for closed or replaced runtime", { dbPath: deps.dbPath }, {
|
|
1219
|
+
event_name: "sno_station_mem.maintenance.stopping.maintenance.for.closed.or.replaced.runtime",
|
|
1220
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1221
|
+
function: "runMaintenancePass",
|
|
1222
|
+
site_id: "maintenance.runMaintenancePass.747b7cbef0"
|
|
1223
|
+
});
|
|
1224
|
+
return report;
|
|
1225
|
+
}
|
|
1226
|
+
deps.store.db.retrySetup();
|
|
1227
|
+
deps.store.hasFtsSupport = deps.store.sqlite.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'nodix_memory_chunks_fts' LIMIT 1").get() !== void 0;
|
|
1228
|
+
const now = Date.now();
|
|
1229
|
+
if (due.has("integrity")) sweepIntegrity(deps, report);
|
|
1230
|
+
if (deps.usageOutbox && due.has("usage-outbox")) try {
|
|
1231
|
+
report.outboxFlushed = deps.usageOutbox.drainPending(OUTBOX_DRAIN_BUDGET_MS).inserted;
|
|
1232
|
+
report.quarantinePruned = deps.usageOutbox.pruneQuarantined(now - OUTBOX_QUARANTINE_RETENTION_MS);
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
log.warn("outbox maintenance failed", { error }, {
|
|
1235
|
+
event_name: "sno_station_mem.maintenance.outbox.maintenance.failed",
|
|
1236
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1237
|
+
function: "runMaintenancePass",
|
|
1238
|
+
site_id: "maintenance.runMaintenancePass.c72575daaa"
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
if (due.has("usage-retention")) try {
|
|
1242
|
+
report.usageEventsPruned = pruneExpiredUsageEvents(deps.store, now, deps.retentionPruneBudgetMs ?? RETENTION_PRUNE_BUDGET_MS);
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
log.warn("usage-event retention failed", { error }, {
|
|
1245
|
+
event_name: "sno_station_mem.maintenance.usage.event.retention.failed",
|
|
1246
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1247
|
+
function: "runMaintenancePass",
|
|
1248
|
+
site_id: "maintenance.runMaintenancePass.ef7a1e225c"
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
if (due.has("fts-merge")) try {
|
|
1252
|
+
mergeFtsSegments(deps.store);
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
log.warn("fts merge failed", { error }, {
|
|
1255
|
+
event_name: "sno_station_mem.maintenance.fts.merge.failed",
|
|
1256
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1257
|
+
function: "runMaintenancePass",
|
|
1258
|
+
site_id: "maintenance.runMaintenancePass.90f01cc543"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
if (due.has("planner-statistics")) try {
|
|
1262
|
+
deps.store.sqlite.exec("PRAGMA analysis_limit=400");
|
|
1263
|
+
deps.store.sqlite.exec("PRAGMA optimize");
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
log.warn("pragma optimize failed", { error }, {
|
|
1266
|
+
event_name: "sno_station_mem.maintenance.pragma.optimize.failed",
|
|
1267
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1268
|
+
function: "runMaintenancePass",
|
|
1269
|
+
site_id: "maintenance.runMaintenancePass.3a7c9e73cf"
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
if (due.has("backup")) try {
|
|
1273
|
+
if (isBackupDue(deps.backupDir, now, backupIntervalMs)) report.backupPath = runBackup(deps.dbPath, deps.backupDir);
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
log.warn("periodic backup failed", { error }, {
|
|
1276
|
+
event_name: "sno_station_mem.maintenance.periodic.backup.failed",
|
|
1277
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1278
|
+
function: "runMaintenancePass",
|
|
1279
|
+
site_id: "maintenance.runMaintenancePass.11beb09895"
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
log.info("maintenance pass complete", {
|
|
1283
|
+
integrityMs: report.integrityMs,
|
|
1284
|
+
outboxFlushed: report.outboxFlushed,
|
|
1285
|
+
quarantinePruned: report.quarantinePruned,
|
|
1286
|
+
usageEventsPruned: report.usageEventsPruned
|
|
1287
|
+
}, {
|
|
1288
|
+
event_name: "sno_station_mem.maintenance.maintenance.pass.complete",
|
|
1289
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1290
|
+
function: "runMaintenancePass",
|
|
1291
|
+
site_id: "maintenance.runMaintenancePass.6a1e15dc18"
|
|
1292
|
+
});
|
|
1293
|
+
return report;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Gateway maintenance schedule: first tick MAINTENANCE_FIRST_TICK_DELAY_MS after boot, then every
|
|
1297
|
+
* tickMs. Each tick runs the jobs whose own interval has elapsed since they last ran; the first
|
|
1298
|
+
* tick runs all of them. Stops when its store closes.
|
|
1299
|
+
*/
|
|
1300
|
+
function startMaintenanceTimer(deps, tickMs = MAINTENANCE_TICK_MS, firstTickDelayMs = MAINTENANCE_FIRST_TICK_DELAY_MS, intervals = MAINTENANCE_INTERVALS) {
|
|
1301
|
+
let interval = null;
|
|
1302
|
+
let firstTick = null;
|
|
1303
|
+
let inFlight = false;
|
|
1304
|
+
let stopped = false;
|
|
1305
|
+
const lastRun = /* @__PURE__ */ new Map();
|
|
1306
|
+
const slackMs = tickMs / 2;
|
|
1307
|
+
const dueJobs = (now) => {
|
|
1308
|
+
const due = /* @__PURE__ */ new Set();
|
|
1309
|
+
for (const job of ALL_MAINTENANCE_JOBS) {
|
|
1310
|
+
const last = lastRun.get(job);
|
|
1311
|
+
if (job === "backup" || last === void 0 || now - last >= intervals[job] - slackMs) due.add(job);
|
|
1312
|
+
}
|
|
1313
|
+
return due;
|
|
1314
|
+
};
|
|
1315
|
+
const stop = () => {
|
|
1316
|
+
stopped = true;
|
|
1317
|
+
if (firstTick) {
|
|
1318
|
+
clearTimeout(firstTick);
|
|
1319
|
+
firstTick = null;
|
|
1320
|
+
}
|
|
1321
|
+
if (interval) {
|
|
1322
|
+
clearInterval(interval);
|
|
1323
|
+
interval = null;
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
const tick = () => {
|
|
1327
|
+
if (inFlight) {
|
|
1328
|
+
log.warn("skipping maintenance tick: previous pass still in flight", void 0, {
|
|
1329
|
+
event_name: "sno_station_mem.maintenance.skipping.maintenance.tick.previous.pass.still.in.flight",
|
|
1330
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1331
|
+
function: "tick",
|
|
1332
|
+
site_id: "maintenance.tick.e111b66415"
|
|
1333
|
+
});
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (deps.store.closed) {
|
|
1337
|
+
log.info("stopping maintenance for closed or replaced runtime", { dbPath: deps.dbPath }, {
|
|
1338
|
+
event_name: "sno_station_mem.maintenance.stopping.maintenance.for.closed.or.replaced.runtime",
|
|
1339
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1340
|
+
function: "tick",
|
|
1341
|
+
site_id: "maintenance.tick.6bd3e0102c"
|
|
1342
|
+
});
|
|
1343
|
+
stop();
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
inFlight = true;
|
|
1347
|
+
const now = Date.now();
|
|
1348
|
+
const due = dueJobs(now);
|
|
1349
|
+
for (const job of due) lastRun.set(job, now);
|
|
1350
|
+
(async () => {
|
|
1351
|
+
try {
|
|
1352
|
+
if (runMaintenancePass(deps, due, intervals.backup - slackMs).aborted) {
|
|
1353
|
+
stop();
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (!due.has("rem-trigger")) return;
|
|
1357
|
+
await evaluateRemAutomaticTriggers({
|
|
1358
|
+
database: deps.store.sqlite,
|
|
1359
|
+
stateDir: deps.stateDir,
|
|
1360
|
+
auditStateDir: getSnoStationMemStateDir(),
|
|
1361
|
+
requestedOperations: deps.remSettings.requestedOperations,
|
|
1362
|
+
tickEnabled: deps.remSettings.tickEnabled,
|
|
1363
|
+
modelCalls: deps.modelCalls,
|
|
1364
|
+
mode: deps.mode,
|
|
1365
|
+
hasConnectedHost: deps.hasConnectedHost,
|
|
1366
|
+
now: deps.remClock,
|
|
1367
|
+
volumeThreshold: deps.remVolumeThreshold
|
|
1368
|
+
});
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
log.warn("maintenance pass threw", { error }, {
|
|
1371
|
+
event_name: "sno_station_mem.maintenance.maintenance.pass.threw",
|
|
1372
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1373
|
+
function: "<anonymous callback>",
|
|
1374
|
+
site_id: "maintenance.<anonymous callback>.f90713a2e3"
|
|
1375
|
+
});
|
|
1376
|
+
} finally {
|
|
1377
|
+
inFlight = false;
|
|
1378
|
+
}
|
|
1379
|
+
})();
|
|
1380
|
+
};
|
|
1381
|
+
firstTick = setTimeout(() => {
|
|
1382
|
+
firstTick = null;
|
|
1383
|
+
tick();
|
|
1384
|
+
if (stopped) return;
|
|
1385
|
+
interval = setInterval(tick, tickMs);
|
|
1386
|
+
interval.unref?.();
|
|
1387
|
+
}, firstTickDelayMs);
|
|
1388
|
+
firstTick.unref?.();
|
|
1389
|
+
log.debug("maintenance timer started", {
|
|
1390
|
+
tickMs,
|
|
1391
|
+
intervals
|
|
1392
|
+
}, {
|
|
1393
|
+
event_name: "sno_station_mem.maintenance.maintenance.timer.started",
|
|
1394
|
+
file: "packages/memory/src/store/maintenance.ts",
|
|
1395
|
+
function: "startMaintenanceTimer",
|
|
1396
|
+
site_id: "maintenance.startMaintenanceTimer.06c1e98584"
|
|
1397
|
+
});
|
|
1398
|
+
return { stop };
|
|
1399
|
+
}
|
|
1400
|
+
//#endregion
|
|
1401
|
+
export { MAINTENANCE_FIRST_TICK_DELAY_MS, MAINTENANCE_INTERVALS, MEMORY_EVENTS_USAGE_RETENTION_MS, OUTBOX_QUARANTINE_RETENTION_MS, runMaintenancePass, startMaintenanceTimer, uniformMaintenanceIntervals };
|