@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,2149 @@
|
|
|
1
|
+
import { MAX_LIST_LIMIT } from "./internal/config/index.js";
|
|
2
|
+
import { SnoStationMemError, StorageError } from "./internal/engine/shared/errors.js";
|
|
3
|
+
import { a as buildInsightMetadata, c as stringifyInsightMetadata, h as deriveFactKey, s as parseInsightMetadata } from "./memory-metadata-codec-DL1sbJiN.js";
|
|
4
|
+
import { n as LlmClientTerminalError } from "./llm-client-BSYUAcUb.js";
|
|
5
|
+
import { resolveLlmRoute } from "./internal/model/llm-mode-routing.js";
|
|
6
|
+
import { boundSectionContent, buildIndexedText } from "./internal/engine/extraction/extraction-text-sanitizer.js";
|
|
7
|
+
import { d as runWithMutationAttempt, i as currentMutationAttemptId } from "./runtime-audit-log-BCeujlEp.js";
|
|
8
|
+
import { adapterAViewFromRecord, orderAdapterAPair, parseAdapterAChatVerdict, prospectiveProfileView, renderAdapterAChatPrompt, renderAdapterAPrompt } from "./internal/engine/rem/index.js";
|
|
9
|
+
import { C as TaskLifecycleStaleResolutionError, a as resolveTaskLifecycle, c as buildTaskLifecycleCommandClaim, i as findUniqueActiveTaskByDescription, l as resolveTaskLifecycleEffectiveTime, n as acceptsTaskLifecycleJudgment, o as taskLifecycleCandidateSetVersion, r as buildTaskLifecycleCandidateSet, s as admitTaskLifecycleAssertion, x as StaleSupersedeTargetError } from "./store-nPql26YM.js";
|
|
10
|
+
import { o as canonicalizeProfileSectionName, r as recordTokenCounter } from "./memory-store-write-validation-B_hDG2zf.js";
|
|
11
|
+
import { canExtractorWrite } from "./internal/engine/shared/memory-kind-policy.js";
|
|
12
|
+
import { resolveHookAgentId } from "./internal/engine/bindings/sno-station-mem-runtime-mode.js";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { createLogger, privateLogReference } from "@snoai/utils/logger";
|
|
15
|
+
import path, { dirname } from "node:path";
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
//#region src/engine/extraction/retired-position-judgment-skill.ts
|
|
20
|
+
/** @file retired-position-judgment-skill.ts
|
|
21
|
+
* @purpose Loads the model-readable retired-position judgment method shipped with the plugin.
|
|
22
|
+
* @boundary Package-relative read of one trusted bundled skill.
|
|
23
|
+
*/
|
|
24
|
+
const SKILL_PATH = path.join("skills", "judge-retired-position", "SKILL.md");
|
|
25
|
+
function resolveSkillPath$1() {
|
|
26
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
for (const root of [
|
|
28
|
+
path.resolve(moduleDir, ".."),
|
|
29
|
+
path.resolve(moduleDir, "../.."),
|
|
30
|
+
path.resolve(moduleDir, "../../..")
|
|
31
|
+
]) {
|
|
32
|
+
const candidate = path.join(root, SKILL_PATH);
|
|
33
|
+
if (existsSync(candidate)) return candidate;
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`sno-station-mem retired-position judgment skill is missing: ${SKILL_PATH}`);
|
|
36
|
+
}
|
|
37
|
+
const skillText = readFileSync(resolveSkillPath$1(), "utf8");
|
|
38
|
+
function readSection$1(heading) {
|
|
39
|
+
const marker = `## ${heading}\n`;
|
|
40
|
+
const start = skillText.indexOf(marker);
|
|
41
|
+
if (start === -1) throw new Error(`retired-position judgment skill is missing section: ${heading}`);
|
|
42
|
+
const bodyStart = start + marker.length;
|
|
43
|
+
const nextSection = skillText.indexOf("\n## ", bodyStart);
|
|
44
|
+
return skillText.slice(bodyStart, nextSection === -1 ? void 0 : nextSection).trim();
|
|
45
|
+
}
|
|
46
|
+
const RETIRED_POSITION_JUDGMENT_SKILL = {
|
|
47
|
+
currentPositionMatch: readSection$1("Current Position Match"),
|
|
48
|
+
retirementRecheck: readSection$1("Retirement Recheck")
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/engine/extraction/retire-by-name.ts
|
|
52
|
+
/** @file retire-by-name.ts
|
|
53
|
+
* @purpose Extends one profile-owned retirement to model-named current-position rows.
|
|
54
|
+
* @boundary Extraction-triggered shortlist, judgment, and metadata-only invalidation writes.
|
|
55
|
+
*/
|
|
56
|
+
const SHORTLIST_LIMIT = 8;
|
|
57
|
+
const RETIRE_BY_NAME_MAX_DURATION_MS = 3e4;
|
|
58
|
+
const SEARCH_LIMIT = 64;
|
|
59
|
+
const JUDGMENT_MAX_TOKENS = 64;
|
|
60
|
+
const RETIRE_BY_NAME_SAMPLING = {
|
|
61
|
+
temperature: "provider-default",
|
|
62
|
+
enableThinking: false,
|
|
63
|
+
maxTokens: JUDGMENT_MAX_TOKENS
|
|
64
|
+
};
|
|
65
|
+
const RETIRE_BY_NAME_PROMPT_TEMPLATE = [
|
|
66
|
+
RETIRED_POSITION_JUDGMENT_SKILL.currentPositionMatch,
|
|
67
|
+
"Return JSON only: {\"retire\":true} or {\"retire\":false}.",
|
|
68
|
+
"Use exactly one boolean field named retire."
|
|
69
|
+
].join("\n\n");
|
|
70
|
+
const RETIRE_BY_NAME_PROMPT_SHA256 = createHash("sha256").update(RETIRE_BY_NAME_PROMPT_TEMPLATE).digest("hex");
|
|
71
|
+
function words(text) {
|
|
72
|
+
return new Set(text.toLocaleLowerCase().match(/[\p{L}\p{N}]+/gu) ?? []);
|
|
73
|
+
}
|
|
74
|
+
function shortlistRetireByNameCandidates(retiredPosition, candidates) {
|
|
75
|
+
const retiredWords = words(retiredPosition);
|
|
76
|
+
return candidates.map((entry) => {
|
|
77
|
+
const candidateWords = words(entry.text);
|
|
78
|
+
const overlap = [...candidateWords].filter((word) => retiredWords.has(word)).length;
|
|
79
|
+
return {
|
|
80
|
+
entry,
|
|
81
|
+
score: candidateWords.size === 0 ? 0 : overlap / candidateWords.size
|
|
82
|
+
};
|
|
83
|
+
}).filter((candidate) => candidate.score > 0).sort((left, right) => right.score - left.score || left.entry.id.localeCompare(right.entry.id)).slice(0, SHORTLIST_LIMIT).map((candidate) => candidate.entry);
|
|
84
|
+
}
|
|
85
|
+
function parseRetireVerdict(value) {
|
|
86
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
87
|
+
const record = value;
|
|
88
|
+
if (Object.keys(record).length !== 1 || typeof record["retire"] !== "boolean") return;
|
|
89
|
+
return record["retire"];
|
|
90
|
+
}
|
|
91
|
+
function judgmentPrompt(retiredPosition, candidate) {
|
|
92
|
+
return [
|
|
93
|
+
RETIRE_BY_NAME_PROMPT_TEMPLATE,
|
|
94
|
+
`Retired position: ${JSON.stringify(retiredPosition)}`,
|
|
95
|
+
`Candidate row: ${JSON.stringify(candidate.text)}`
|
|
96
|
+
].join("\n\n");
|
|
97
|
+
}
|
|
98
|
+
async function retireRowsByName(params) {
|
|
99
|
+
const llm = params.llm;
|
|
100
|
+
if (!params.retiredPosition.trim()) return {
|
|
101
|
+
retiredIds: [],
|
|
102
|
+
completed: true
|
|
103
|
+
};
|
|
104
|
+
if (!llm) return {
|
|
105
|
+
retiredIds: [],
|
|
106
|
+
completed: false
|
|
107
|
+
};
|
|
108
|
+
const excludedIds = /* @__PURE__ */ new Set([params.currentPositionId, ...params.excludedRowIds ?? []]);
|
|
109
|
+
const matches = await params.store.searchKeyword(params.retiredPosition, {
|
|
110
|
+
limit: SEARCH_LIMIT,
|
|
111
|
+
projectIdFilter: [params.scope],
|
|
112
|
+
includeRefused: true,
|
|
113
|
+
excludeInvalidatedBefore: Number.MAX_SAFE_INTEGER,
|
|
114
|
+
excludeMemoryIds: [...excludedIds]
|
|
115
|
+
});
|
|
116
|
+
const searchCapped = matches.length >= SEARCH_LIMIT;
|
|
117
|
+
const eligible = matches.flatMap(({ entry }) => {
|
|
118
|
+
if (excludedIds.has(entry.id)) return [];
|
|
119
|
+
if (entry.category === "profile" || !canExtractorWrite(entry.category)) {
|
|
120
|
+
params.judgedCandidateIds.add(entry.id);
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const metadata = parseInsightMetadata(entry.metadata, entry);
|
|
124
|
+
const writtenByTriggeringEvent = metadata.source_message_id === params.triggeringEventIdentity || params.triggeringSessionIdentity !== void 0 && metadata.source_session === params.triggeringSessionIdentity && metadata.asserted_at >= params.eventTime;
|
|
125
|
+
if (metadata.invalidated_at !== void 0) return [];
|
|
126
|
+
if (writtenByTriggeringEvent) {
|
|
127
|
+
params.judgedCandidateIds.add(entry.id);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
return [entry];
|
|
131
|
+
});
|
|
132
|
+
const shortlist = shortlistRetireByNameCandidates(params.retiredPosition, eligible);
|
|
133
|
+
if (shortlist.length === 0) {
|
|
134
|
+
if (searchCapped) for (const entry of eligible) params.judgedCandidateIds.add(entry.id);
|
|
135
|
+
return {
|
|
136
|
+
retiredIds: [],
|
|
137
|
+
completed: !searchCapped
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (params.judgmentBudget && shortlist.length > params.judgmentBudget.remaining) return {
|
|
141
|
+
retiredIds: [],
|
|
142
|
+
completed: false
|
|
143
|
+
};
|
|
144
|
+
let resolved;
|
|
145
|
+
try {
|
|
146
|
+
resolved = await llm.getResolvedConfig();
|
|
147
|
+
} catch {
|
|
148
|
+
return {
|
|
149
|
+
retiredIds: [],
|
|
150
|
+
completed: false
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const route = params.routing ? resolveLlmRoute({
|
|
154
|
+
callId: "P6",
|
|
155
|
+
config: params.routing
|
|
156
|
+
}) : null;
|
|
157
|
+
const destination = route === null ? "host" : "off" in route ? "off" : route.destination;
|
|
158
|
+
if (params.judgmentBudget) params.judgmentBudget.remaining -= shortlist.length;
|
|
159
|
+
const judgments = [];
|
|
160
|
+
let completed = !searchCapped && eligible.length === shortlist.length;
|
|
161
|
+
for (let offset = 0; offset < shortlist.length; offset += 4) {
|
|
162
|
+
if (params.deadlineMs !== void 0 && Date.now() >= params.deadlineMs) {
|
|
163
|
+
completed = false;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
const batch = shortlist.slice(offset, offset + 4);
|
|
167
|
+
judgments.push(...await Promise.all(batch.map(async (candidate) => {
|
|
168
|
+
try {
|
|
169
|
+
const remainingMs = params.deadlineMs === void 0 ? void 0 : Math.max(1, params.deadlineMs - Date.now());
|
|
170
|
+
return {
|
|
171
|
+
candidate,
|
|
172
|
+
verdict: parseRetireVerdict(await llm.completeJson({
|
|
173
|
+
prompt: judgmentPrompt(params.retiredPosition, candidate),
|
|
174
|
+
callId: "P6",
|
|
175
|
+
maxTokens: JUDGMENT_MAX_TOKENS,
|
|
176
|
+
enableThinking: false,
|
|
177
|
+
...remainingMs === void 0 && params.timeoutMs === void 0 ? {} : { timeoutMs: remainingMs === void 0 ? params.timeoutMs : Math.min(params.timeoutMs ?? remainingMs, remainingMs) },
|
|
178
|
+
...params.signal === void 0 ? {} : { signal: params.signal },
|
|
179
|
+
accept: (value) => parseRetireVerdict(value) !== void 0
|
|
180
|
+
}))
|
|
181
|
+
};
|
|
182
|
+
} catch {
|
|
183
|
+
return {
|
|
184
|
+
candidate,
|
|
185
|
+
verdict: void 0
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
})));
|
|
189
|
+
}
|
|
190
|
+
const retiredIds = [];
|
|
191
|
+
for (const { candidate, verdict } of judgments) {
|
|
192
|
+
if (verdict === void 0) {
|
|
193
|
+
completed = false;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!verdict) {
|
|
197
|
+
params.judgedCandidateIds.add(candidate.id);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const receiptPatch = {
|
|
201
|
+
invalidated_at: params.eventTime,
|
|
202
|
+
superseded_by: params.currentPositionId,
|
|
203
|
+
retire_by_name_receipt: {
|
|
204
|
+
call_id: "P6",
|
|
205
|
+
destination,
|
|
206
|
+
prompt_sha256: RETIRE_BY_NAME_PROMPT_SHA256,
|
|
207
|
+
model: destination === "host" ? "host" : resolved.model,
|
|
208
|
+
preset: destination === "host" ? "host" : resolved.preset,
|
|
209
|
+
sampling: RETIRE_BY_NAME_SAMPLING,
|
|
210
|
+
triggering_event_identity: params.triggeringEventIdentity
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const candidateMetadata = parseInsightMetadata(candidate.metadata, candidate);
|
|
214
|
+
let wroteRetirement = false;
|
|
215
|
+
let contentChangedUnderneath = false;
|
|
216
|
+
try {
|
|
217
|
+
await params.store.applyMetadataDeltas([{
|
|
218
|
+
memoryId: candidate.id,
|
|
219
|
+
deltaFn: (current) => {
|
|
220
|
+
if (current.invalidated_at !== void 0 || typeof current.valid_from === "number" && current.valid_from > params.eventTime) return;
|
|
221
|
+
if (current.l2_content !== candidateMetadata.l2_content) {
|
|
222
|
+
contentChangedUnderneath = true;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
wroteRetirement = true;
|
|
226
|
+
return receiptPatch;
|
|
227
|
+
}
|
|
228
|
+
}]);
|
|
229
|
+
} catch {
|
|
230
|
+
completed = false;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (contentChangedUnderneath) {
|
|
234
|
+
completed = false;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
params.judgedCandidateIds.add(candidate.id);
|
|
238
|
+
if (!wroteRetirement) continue;
|
|
239
|
+
retiredIds.push(candidate.id);
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
retiredIds,
|
|
243
|
+
completed
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/engine/extraction/task-lifecycle-judgment-skill.ts
|
|
248
|
+
/** @file task-lifecycle-judgment-skill.ts
|
|
249
|
+
* @purpose Loads the model-readable active-task judgment method shipped with the plugin.
|
|
250
|
+
* @boundary Package-relative read of one trusted bundled skill.
|
|
251
|
+
*/
|
|
252
|
+
const TASK_SKILL_PATH = path.join("skills", "judge-task-lifecycle", "SKILL.md");
|
|
253
|
+
const ATOMIC_SKILL_PATH = path.join("skills", "extract-atomic-memory", "SKILL.md");
|
|
254
|
+
function resolveSkillPath(skillPath) {
|
|
255
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
256
|
+
for (const root of [
|
|
257
|
+
path.resolve(moduleDir, ".."),
|
|
258
|
+
path.resolve(moduleDir, "../.."),
|
|
259
|
+
path.resolve(moduleDir, "../../..")
|
|
260
|
+
]) {
|
|
261
|
+
const candidate = path.join(root, skillPath);
|
|
262
|
+
if (existsSync(candidate)) return candidate;
|
|
263
|
+
}
|
|
264
|
+
throw new Error(`sno-station-mem judgment skill is missing: ${skillPath}`);
|
|
265
|
+
}
|
|
266
|
+
const taskSkillText = readFileSync(resolveSkillPath(TASK_SKILL_PATH), "utf8");
|
|
267
|
+
const atomicSkillText = readFileSync(resolveSkillPath(ATOMIC_SKILL_PATH), "utf8");
|
|
268
|
+
function readSection(skillText, heading) {
|
|
269
|
+
const marker = `## ${heading}\n`;
|
|
270
|
+
const start = skillText.indexOf(marker);
|
|
271
|
+
if (start === -1) throw new Error(`task lifecycle judgment skill is missing section: ${heading}`);
|
|
272
|
+
const bodyStart = start + marker.length;
|
|
273
|
+
const nextSection = skillText.indexOf("\n## ", bodyStart);
|
|
274
|
+
return skillText.slice(bodyStart, nextSection === -1 ? void 0 : nextSection).trim();
|
|
275
|
+
}
|
|
276
|
+
const TASK_LIFECYCLE_JUDGMENT_SKILL = {
|
|
277
|
+
activeTaskState: [readSection(atomicSkillText, "To-do Boundary"), readSection(taskSkillText, "Active Task State")].join("\n\n"),
|
|
278
|
+
existingTaskRelation: readSection(taskSkillText, "Existing Task Relation")
|
|
279
|
+
};
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/engine/extraction/task-lifecycle-route.ts
|
|
282
|
+
/** @file task-lifecycle-route.ts
|
|
283
|
+
* @purpose Routes one typed task assertion through admission, bounded judgment, and atomic storage.
|
|
284
|
+
* @boundary The only production task mutation ingress; models may select only an admitted identifier.
|
|
285
|
+
*/
|
|
286
|
+
var TaskLifecycleJudgmentUnavailableError = class extends SnoStationMemError {
|
|
287
|
+
constructor(message) {
|
|
288
|
+
super("task_lifecycle_judgment_unavailable", message);
|
|
289
|
+
this.name = "TaskLifecycleJudgmentUnavailableError";
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
function prepareDeterministicTaskLifecycleWrite(input) {
|
|
293
|
+
const commandClaim = buildTaskLifecycleCommandClaim({
|
|
294
|
+
assertion: input.assertion,
|
|
295
|
+
source: input.source
|
|
296
|
+
});
|
|
297
|
+
const admission = admitTaskLifecycleAssertion(input.store, {
|
|
298
|
+
assertion: input.assertion,
|
|
299
|
+
source: input.source,
|
|
300
|
+
commandClaim,
|
|
301
|
+
firstResolutionNowMs: input.firstResolutionNowMs
|
|
302
|
+
});
|
|
303
|
+
const instances = input.store.readTaskLifecycleInstances(admission.assertion.projectId);
|
|
304
|
+
const candidates = buildTaskLifecycleCandidateSet(admission.assertion, instances);
|
|
305
|
+
const exact = findUniqueActiveTaskByDescription(admission.assertion.description, instances);
|
|
306
|
+
const judgment = exact === void 0 ? admission.assertion.action === "open_or_refine" ? {
|
|
307
|
+
status: "completed",
|
|
308
|
+
value: { result: "distinct_instance" },
|
|
309
|
+
candidateSetVersion: taskLifecycleCandidateSetVersion(candidates)
|
|
310
|
+
} : {
|
|
311
|
+
status: "completed",
|
|
312
|
+
value: { result: "none" },
|
|
313
|
+
candidateSetVersion: taskLifecycleCandidateSetVersion(candidates)
|
|
314
|
+
} : {
|
|
315
|
+
status: "completed",
|
|
316
|
+
value: {
|
|
317
|
+
result: "same_instance",
|
|
318
|
+
activeTaskId: exact.activeTaskId
|
|
319
|
+
},
|
|
320
|
+
candidateSetVersion: taskLifecycleCandidateSetVersion(candidates)
|
|
321
|
+
};
|
|
322
|
+
return {
|
|
323
|
+
admission,
|
|
324
|
+
commandClaim,
|
|
325
|
+
resolution: resolveTaskLifecycle({
|
|
326
|
+
assertion: admission.assertion,
|
|
327
|
+
instances,
|
|
328
|
+
judgment
|
|
329
|
+
}),
|
|
330
|
+
todoProvenance: input.todoProvenance
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const TASK_LIFECYCLE_MODEL_ACTIONS = [
|
|
334
|
+
"open_or_refine",
|
|
335
|
+
"complete",
|
|
336
|
+
"remove",
|
|
337
|
+
"none"
|
|
338
|
+
];
|
|
339
|
+
const taskLifecycleModelVerdictSchema = z.object({
|
|
340
|
+
action: z.enum(TASK_LIFECYCLE_MODEL_ACTIONS),
|
|
341
|
+
taskId: z.string().min(1).nullable()
|
|
342
|
+
}).strict();
|
|
343
|
+
async function requestTaskLifecycleJson(input) {
|
|
344
|
+
return input.llm.completeJson({
|
|
345
|
+
prompt: input.prompt,
|
|
346
|
+
callId: "T1",
|
|
347
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
348
|
+
...input.signal === void 0 ? {} : { signal: input.signal },
|
|
349
|
+
accept: input.accept
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async function requestModelJudgment(input) {
|
|
353
|
+
if (!input.llm) throw new TaskLifecycleJudgmentUnavailableError("task lifecycle judgment requires an available model route");
|
|
354
|
+
let value;
|
|
355
|
+
try {
|
|
356
|
+
value = await requestTaskLifecycleJson({
|
|
357
|
+
llm: input.llm,
|
|
358
|
+
accept: (value) => taskLifecycleModelVerdictSchema.safeParse(value).success,
|
|
359
|
+
prompt: [
|
|
360
|
+
TASK_LIFECYCLE_JUDGMENT_SKILL.activeTaskState,
|
|
361
|
+
"Return exactly one JSON object: {\"action\":\"open_or_refine|complete|remove|none\",\"taskId\":\"presented-id-or-null\"}.",
|
|
362
|
+
"For complete or remove, taskId must be one identifier from Active tasks.",
|
|
363
|
+
"For open_or_refine, taskId is an existing task to refine or null for a distinct task.",
|
|
364
|
+
"For none, taskId must be null.",
|
|
365
|
+
`Allowed actions: ${JSON.stringify(input.allowedActions)}`,
|
|
366
|
+
`Candidate: ${JSON.stringify(input.candidateText)}`,
|
|
367
|
+
`Active tasks: ${JSON.stringify(input.candidates)}`
|
|
368
|
+
].join("\n"),
|
|
369
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
370
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
371
|
+
});
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (error instanceof LlmClientTerminalError && error.category === "cancelled" && !error.requestTimedOut) throw error;
|
|
374
|
+
throw new TaskLifecycleJudgmentUnavailableError(`task lifecycle judgment failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
375
|
+
}
|
|
376
|
+
const parsed = taskLifecycleModelVerdictSchema.safeParse(value);
|
|
377
|
+
if (!parsed.success) throw new TaskLifecycleJudgmentUnavailableError("task lifecycle judgment returned an invalid shape");
|
|
378
|
+
const verdict = parsed.data;
|
|
379
|
+
if (!input.allowedActions.includes(verdict.action)) throw new TaskLifecycleJudgmentUnavailableError("task lifecycle judgment returned an action outside the admitted enum");
|
|
380
|
+
if (verdict.action === "none") {
|
|
381
|
+
if (verdict.taskId !== null) throw new TaskLifecycleJudgmentUnavailableError("task lifecycle none verdict returned an active task identifier");
|
|
382
|
+
return verdict;
|
|
383
|
+
}
|
|
384
|
+
if (verdict.action !== "open_or_refine" && verdict.taskId === null) throw new TaskLifecycleJudgmentUnavailableError("task lifecycle terminal verdict omitted the active task identifier");
|
|
385
|
+
if (verdict.taskId !== null && !input.candidates.some((candidate) => candidate.activeTaskId === verdict.taskId)) throw new TaskLifecycleJudgmentUnavailableError("task lifecycle judgment selected an identifier outside the presented set");
|
|
386
|
+
return verdict;
|
|
387
|
+
}
|
|
388
|
+
function provisionalAssertion(input) {
|
|
389
|
+
const effectiveTime = resolveTaskLifecycleEffectiveTime(input);
|
|
390
|
+
return {
|
|
391
|
+
kind: "task_lifecycle",
|
|
392
|
+
action: "open_or_refine",
|
|
393
|
+
commandId: "0".repeat(64),
|
|
394
|
+
projectId: input.projectId,
|
|
395
|
+
subject: "user",
|
|
396
|
+
description: input.candidateText,
|
|
397
|
+
effectiveAtMs: effectiveTime.effectiveAtMs,
|
|
398
|
+
timeSource: effectiveTime.timeSource,
|
|
399
|
+
occurrenceAnchors: input.occurrenceAnchors ?? {},
|
|
400
|
+
revisionDetails: input.revisionDetails ?? {},
|
|
401
|
+
...input.evidenceMemoryId === void 0 ? {} : { evidenceMemoryId: input.evidenceMemoryId }
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function readCurrentOpenTaskInstances(store, projectId) {
|
|
405
|
+
return store.readTaskLifecycleInstances(projectId).filter((instance) => instance.terminalAtMs === void 0);
|
|
406
|
+
}
|
|
407
|
+
async function routeTaskLifecycleCandidateOnce(input) {
|
|
408
|
+
const candidateText = input.candidateText.trim();
|
|
409
|
+
if (!candidateText) throw new TaskLifecycleJudgmentUnavailableError("task lifecycle judgment requires non-empty candidate text");
|
|
410
|
+
const provisional = provisionalAssertion({
|
|
411
|
+
...input,
|
|
412
|
+
candidateText
|
|
413
|
+
});
|
|
414
|
+
let candidateInstances = readCurrentOpenTaskInstances(input.store, input.projectId);
|
|
415
|
+
let candidates = buildTaskLifecycleCandidateSet(provisional, candidateInstances);
|
|
416
|
+
if (!input.confirmedTaskCandidate && candidates.length === 0) return { status: "none" };
|
|
417
|
+
const allowedActions = input.confirmedTaskCandidate ? TASK_LIFECYCLE_MODEL_ACTIONS : [
|
|
418
|
+
"complete",
|
|
419
|
+
"remove",
|
|
420
|
+
"none"
|
|
421
|
+
];
|
|
422
|
+
let conflictRetries = 0;
|
|
423
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
424
|
+
const judgedCandidateSetVersion = taskLifecycleCandidateSetVersion(candidates);
|
|
425
|
+
let verdict;
|
|
426
|
+
let modelUnavailable = false;
|
|
427
|
+
try {
|
|
428
|
+
verdict = await requestModelJudgment({
|
|
429
|
+
candidateText,
|
|
430
|
+
allowedActions,
|
|
431
|
+
candidates,
|
|
432
|
+
...input.llm === void 0 ? {} : { llm: input.llm },
|
|
433
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
434
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
435
|
+
});
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (error instanceof LlmClientTerminalError && error.category === "cancelled" && !error.requestTimedOut) throw error;
|
|
438
|
+
modelUnavailable = true;
|
|
439
|
+
verdict = {
|
|
440
|
+
action: input.confirmedTaskCandidate ? "open_or_refine" : "none",
|
|
441
|
+
taskId: null
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const currentInstances = readCurrentOpenTaskInstances(input.store, input.projectId);
|
|
445
|
+
const currentCandidates = buildTaskLifecycleCandidateSet(provisional, currentInstances);
|
|
446
|
+
if (taskLifecycleCandidateSetVersion(currentCandidates) !== judgedCandidateSetVersion) {
|
|
447
|
+
conflictRetries += 1;
|
|
448
|
+
candidateInstances = currentInstances;
|
|
449
|
+
candidates = currentCandidates;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (verdict.action === "none") return { status: "none" };
|
|
453
|
+
const selected = verdict.taskId === null ? void 0 : candidates.find((candidate) => candidate.activeTaskId === verdict.taskId);
|
|
454
|
+
const assertion = {
|
|
455
|
+
kind: "task_lifecycle",
|
|
456
|
+
action: verdict.action,
|
|
457
|
+
projectId: input.projectId,
|
|
458
|
+
subject: "user",
|
|
459
|
+
description: verdict.action === "open_or_refine" ? candidateText : selected?.currentDescription ?? candidateText,
|
|
460
|
+
occurrenceAnchors: input.occurrenceAnchors ?? {},
|
|
461
|
+
revisionDetails: input.revisionDetails ?? {},
|
|
462
|
+
...input.evidenceMemoryId === void 0 ? {} : { evidenceMemoryId: input.evidenceMemoryId }
|
|
463
|
+
};
|
|
464
|
+
const finalCandidateSetVersion = taskLifecycleCandidateSetVersion(buildTaskLifecycleCandidateSet({
|
|
465
|
+
...provisional,
|
|
466
|
+
action: verdict.action,
|
|
467
|
+
description: assertion.description
|
|
468
|
+
}, candidateInstances));
|
|
469
|
+
const precomputedJudgment = verdict.taskId === null ? {
|
|
470
|
+
status: "completed",
|
|
471
|
+
value: { result: "distinct_instance" },
|
|
472
|
+
candidateSetVersion: finalCandidateSetVersion
|
|
473
|
+
} : {
|
|
474
|
+
status: "completed",
|
|
475
|
+
value: {
|
|
476
|
+
result: "same_instance",
|
|
477
|
+
activeTaskId: verdict.taskId
|
|
478
|
+
},
|
|
479
|
+
candidateSetVersion: finalCandidateSetVersion
|
|
480
|
+
};
|
|
481
|
+
try {
|
|
482
|
+
return {
|
|
483
|
+
status: "routed",
|
|
484
|
+
result: {
|
|
485
|
+
...await routeTaskLifecycleAssertion({
|
|
486
|
+
assertion,
|
|
487
|
+
source: input.source,
|
|
488
|
+
eventAt: input.eventAt,
|
|
489
|
+
sessionTime: input.sessionTime,
|
|
490
|
+
firstResolutionNowMs: input.firstResolutionNowMs,
|
|
491
|
+
store: input.store,
|
|
492
|
+
...modelUnavailable || input.llm === void 0 ? {} : { llm: input.llm },
|
|
493
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
494
|
+
...input.signal === void 0 ? {} : { signal: input.signal },
|
|
495
|
+
...modelUnavailable ? {} : { precomputedJudgment },
|
|
496
|
+
precomputedInstances: candidateInstances
|
|
497
|
+
}),
|
|
498
|
+
conflictRetries
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if (!(error instanceof TaskLifecycleStaleResolutionError)) throw error;
|
|
503
|
+
conflictRetries += 1;
|
|
504
|
+
candidateInstances = readCurrentOpenTaskInstances(input.store, input.projectId);
|
|
505
|
+
candidates = buildTaskLifecycleCandidateSet(provisional, candidateInstances);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
throw new TaskLifecycleJudgmentUnavailableError("task lifecycle open-task set remained stale after three fresh judgments");
|
|
509
|
+
}
|
|
510
|
+
function taskLifecycleMutationOutcome(result) {
|
|
511
|
+
if (result.status === "none" || result.result.write.replayed) return { outcome: "no-mutation" };
|
|
512
|
+
if (result.result.write.result === "uncertain") return {
|
|
513
|
+
outcome: "preserved-without-adjudication",
|
|
514
|
+
refusalReason: "task_lifecycle_judgment_uncertain"
|
|
515
|
+
};
|
|
516
|
+
if (result.result.write.result === "none") return { outcome: "no-mutation" };
|
|
517
|
+
return { outcome: "committed" };
|
|
518
|
+
}
|
|
519
|
+
async function routeTaskLifecycleCandidate(input) {
|
|
520
|
+
return runWithMutationAttempt({
|
|
521
|
+
stateDir: dirname(input.store.dbPath),
|
|
522
|
+
event: "memory_updated",
|
|
523
|
+
operation: "task-lifecycle-update",
|
|
524
|
+
writer: "task-lifecycle",
|
|
525
|
+
subject: "active_tasks",
|
|
526
|
+
run: () => routeTaskLifecycleCandidateOnce(input),
|
|
527
|
+
completedOutcome: taskLifecycleMutationOutcome,
|
|
528
|
+
failedOutcome: (error) => error instanceof TaskLifecycleJudgmentUnavailableError ? {
|
|
529
|
+
outcome: "preserved-without-adjudication",
|
|
530
|
+
refusalReason: error.message
|
|
531
|
+
} : { outcome: "failed" }
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
async function requestJudgment(input, assertion) {
|
|
535
|
+
if (!input.llm) return { status: "absent" };
|
|
536
|
+
const candidates = buildTaskLifecycleCandidateSet(assertion, readCurrentOpenTaskInstances(input.store, assertion.projectId));
|
|
537
|
+
if (candidates.length === 0) return { status: "absent" };
|
|
538
|
+
try {
|
|
539
|
+
const value = await requestTaskLifecycleJson({
|
|
540
|
+
llm: input.llm,
|
|
541
|
+
accept: acceptsTaskLifecycleJudgment,
|
|
542
|
+
prompt: [
|
|
543
|
+
TASK_LIFECYCLE_JUDGMENT_SKILL.existingTaskRelation,
|
|
544
|
+
"Return exactly one JSON object: {\"result\":\"same_instance\",\"activeTaskId\":\"...\"} or {\"result\":\"distinct_instance\"} or {\"result\":\"none\"} or {\"result\":\"uncertain\"}.",
|
|
545
|
+
"Select same_instance only with an activeTaskId from candidates.",
|
|
546
|
+
`Assertion: ${JSON.stringify(assertion)}`,
|
|
547
|
+
`Candidates: ${JSON.stringify(candidates)}`
|
|
548
|
+
].join("\n"),
|
|
549
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
550
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
551
|
+
});
|
|
552
|
+
return value === null ? { status: "absent" } : {
|
|
553
|
+
status: "completed",
|
|
554
|
+
value,
|
|
555
|
+
candidateSetVersion: taskLifecycleCandidateSetVersion(candidates)
|
|
556
|
+
};
|
|
557
|
+
} catch {
|
|
558
|
+
return { status: "absent" };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function routeTaskLifecycleAssertion(input) {
|
|
562
|
+
const commandClaim = buildTaskLifecycleCommandClaim({
|
|
563
|
+
assertion: input.assertion,
|
|
564
|
+
source: input.source
|
|
565
|
+
});
|
|
566
|
+
const admission = admitTaskLifecycleAssertion(input.store, {
|
|
567
|
+
assertion: input.assertion,
|
|
568
|
+
source: input.source,
|
|
569
|
+
commandClaim,
|
|
570
|
+
...input.eventAt === void 0 ? {} : { eventAt: input.eventAt },
|
|
571
|
+
...input.sessionTime === void 0 ? {} : { sessionTime: input.sessionTime },
|
|
572
|
+
firstResolutionNowMs: input.firstResolutionNowMs
|
|
573
|
+
});
|
|
574
|
+
const replay = input.store.findTaskLifecycleCommandReplay({
|
|
575
|
+
admission,
|
|
576
|
+
commandClaim
|
|
577
|
+
});
|
|
578
|
+
if (replay) return {
|
|
579
|
+
resolution: null,
|
|
580
|
+
write: replay,
|
|
581
|
+
effectiveAtMs: admission.assertion.effectiveAtMs,
|
|
582
|
+
timeSource: admission.assertion.timeSource,
|
|
583
|
+
conflictRetries: 0
|
|
584
|
+
};
|
|
585
|
+
const instances = input.precomputedInstances ?? input.store.readTaskLifecycleInstances(admission.assertion.projectId);
|
|
586
|
+
const deterministic = resolveTaskLifecycle({
|
|
587
|
+
assertion: admission.assertion,
|
|
588
|
+
instances,
|
|
589
|
+
judgment: { status: "absent" }
|
|
590
|
+
});
|
|
591
|
+
const resolution = input.precomputedJudgment !== void 0 ? resolveTaskLifecycle({
|
|
592
|
+
assertion: admission.assertion,
|
|
593
|
+
instances,
|
|
594
|
+
judgment: input.precomputedJudgment
|
|
595
|
+
}) : deterministic.result !== "uncertain" ? deterministic : resolveTaskLifecycle({
|
|
596
|
+
assertion: admission.assertion,
|
|
597
|
+
instances,
|
|
598
|
+
judgment: await requestJudgment(input, admission.assertion)
|
|
599
|
+
});
|
|
600
|
+
return {
|
|
601
|
+
resolution,
|
|
602
|
+
write: await input.store.applyTaskLifecycleResolution({
|
|
603
|
+
admission,
|
|
604
|
+
commandClaim,
|
|
605
|
+
resolution
|
|
606
|
+
}),
|
|
607
|
+
effectiveAtMs: admission.assertion.effectiveAtMs,
|
|
608
|
+
timeSource: admission.assertion.timeSource,
|
|
609
|
+
conflictRetries: 0
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
//#endregion
|
|
613
|
+
//#region src/engine/extraction/profile-section-writer.ts
|
|
614
|
+
/** @file profile-section-writer.ts
|
|
615
|
+
* @purpose Mutation-native profile section writer for current-state memory.
|
|
616
|
+
* @boundary Owns profile rows only; no retrieval injection or conflict scan.
|
|
617
|
+
*/
|
|
618
|
+
function createRetireByNameRunBudget(timeoutMs) {
|
|
619
|
+
const durationMs = Math.min(timeoutMs ?? 3e4, RETIRE_BY_NAME_MAX_DURATION_MS);
|
|
620
|
+
return {
|
|
621
|
+
remaining: 8,
|
|
622
|
+
deadlineMs: Date.now() + durationMs
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const PROFILE_IDENTITY_SECTION = "identity";
|
|
626
|
+
const retiredProfileSectionMarker = (canonicalSection) => `${canonicalSection}: none`;
|
|
627
|
+
const PROFILE_CONFLICT_TOP_K = 5;
|
|
628
|
+
const CLEAR_RETIRE_BY_NAME_PENDING_ATTEMPTS = 3;
|
|
629
|
+
const ADVANCE_PROFILE_CONFIRMATION_ATTEMPTS = 3;
|
|
630
|
+
/** Work items one profile row's pending marker may hold. Past this the oldest is dropped. */
|
|
631
|
+
const RETIRE_BY_NAME_MAX_PENDING_WORK_ITEMS = 16;
|
|
632
|
+
const log = createLogger("sno-station-mem:profile-section-writer");
|
|
633
|
+
function adapterAViewFromEntry(entry) {
|
|
634
|
+
const metadata = parseInsightMetadata(entry.metadata, entry);
|
|
635
|
+
return adapterAViewFromRecord({
|
|
636
|
+
text: metadata.l2_content ?? entry.text,
|
|
637
|
+
kind: entry.category,
|
|
638
|
+
validFrom: metadata.valid_from ?? entry.timestamp,
|
|
639
|
+
assertedAt: metadata.asserted_at ?? entry.timestamp,
|
|
640
|
+
contentHash: entry.contentHash
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const profileJudgmentResponseSchema = z.object({
|
|
644
|
+
verdict: z.enum([
|
|
645
|
+
"merge",
|
|
646
|
+
"tombstone",
|
|
647
|
+
"no-op"
|
|
648
|
+
]),
|
|
649
|
+
retired_clause_indices: z.array(z.number().int().nonnegative())
|
|
650
|
+
}).strict();
|
|
651
|
+
const profileTextResponseSchema = z.object({
|
|
652
|
+
abstract: z.string().trim().min(1),
|
|
653
|
+
overview: z.string().trim().min(1),
|
|
654
|
+
content: z.string()
|
|
655
|
+
}).strict();
|
|
656
|
+
const lifecycleRetirementResponseSchema = z.object({ lifecycle_retired_clause_indices: z.array(z.number().int().nonnegative()) }).strict();
|
|
657
|
+
const PROFILE_SECTION_JUDGMENT_INSTRUCTIONS = [
|
|
658
|
+
"Judge one current-state profile update.",
|
|
659
|
+
"Return JSON only: {\"verdict\":\"merge|tombstone|no-op\",\"retired_clause_indices\":[0]}.",
|
|
660
|
+
"Do not return content, summaries, explanations, or any generated prose.",
|
|
661
|
+
"Indices are zero-based positions into the supplied array. Use each index at most once.",
|
|
662
|
+
"For merge, retire only clauses that the incoming assertion replaces, removes, or makes historical.",
|
|
663
|
+
"For tombstone, retire the whole section and return an empty index array.",
|
|
664
|
+
"Choose tombstone when the incoming clause explicitly asks to delete, remove, forget, or stop tracking the entire saved section or preference. A changed or negated preference that still states current user information is merge, not tombstone.",
|
|
665
|
+
"For no-op, return an empty index array and use it only when the stored state already covers every incoming clause.",
|
|
666
|
+
"Choose no-op when stored and incoming clauses state the same fact or preference with synonymous wording; a paraphrase is not a replacement.",
|
|
667
|
+
"Never retire every incoming clause.",
|
|
668
|
+
"For preferences.general, ownership is an independent retirement reason and does not require the incoming clause to contradict or replace the stored clause. Each listed sibling suffix names the topic it owns. Retire a stored general clause when its meaning belongs to that topic; for example, when preferences.films is listed, retire a stored general clause saying the user likes James Stewart movies. Keep a general clause when no listed sibling owns its topic. One fact family has one owner."
|
|
669
|
+
];
|
|
670
|
+
function renderProfileSectionJudgmentPrompt(input) {
|
|
671
|
+
return [
|
|
672
|
+
...PROFILE_SECTION_JUDGMENT_INSTRUCTIONS,
|
|
673
|
+
`Section: ${input.sectionName}`,
|
|
674
|
+
input.topic ? `Topic: ${input.topic}` : "Topic: <missing>",
|
|
675
|
+
`Eligible clauses: ${JSON.stringify(input.clauses)}`,
|
|
676
|
+
`Live specific sibling sections: ${JSON.stringify(input.liveSiblingSectionNames ?? [])}`,
|
|
677
|
+
input.evidence ? `Evidence:\n${input.evidence}` : ""
|
|
678
|
+
].join("\n\n");
|
|
679
|
+
}
|
|
680
|
+
function renderProfileSectionTextPrompt(input) {
|
|
681
|
+
return [
|
|
682
|
+
"Write one current-state profile section after a separate judgment has already finished.",
|
|
683
|
+
"Return JSON only: {\"abstract\":\"...\",\"overview\":\"...\",\"content\":\"...\"}.",
|
|
684
|
+
"Do not decide or name superseded clauses. Do not return a verdict or clause indices.",
|
|
685
|
+
"The content must contain every retained clause below verbatim as a whole clause. Do not add connective text or any other clause. Do not reword or omit a retained clause.",
|
|
686
|
+
`Section: ${input.sectionName}`,
|
|
687
|
+
input.topic ? `Topic: ${input.topic}` : "Topic: <missing>",
|
|
688
|
+
`Retained clauses: ${JSON.stringify(input.retainedClauses)}`,
|
|
689
|
+
`Already retired clauses: ${JSON.stringify(input.retiredClauses)}`,
|
|
690
|
+
input.evidence ? `Evidence:\n${input.evidence}` : ""
|
|
691
|
+
].join("\n\n");
|
|
692
|
+
}
|
|
693
|
+
function parseProfileSectionJudgment(response, clauses) {
|
|
694
|
+
const parsed = profileJudgmentResponseSchema.safeParse(response);
|
|
695
|
+
if (!parsed.success) return void 0;
|
|
696
|
+
const indices = parsed.data.retired_clause_indices;
|
|
697
|
+
if (new Set(indices).size !== indices.length) return void 0;
|
|
698
|
+
if (indices.some((index) => index >= clauses.length)) return void 0;
|
|
699
|
+
if (parsed.data.verdict !== "merge" && indices.length > 0) return void 0;
|
|
700
|
+
const incomingIndices = clauses.flatMap((clause, index) => clause.origin === "incoming" ? [index] : []);
|
|
701
|
+
if (incomingIndices.length > 0 && incomingIndices.every((index) => indices.includes(index))) return;
|
|
702
|
+
return {
|
|
703
|
+
verdict: parsed.data.verdict,
|
|
704
|
+
retiredClauseIndices: indices,
|
|
705
|
+
retiredClauses: indices.map((index) => clauses[index]?.value ?? "")
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Deferrals a single lifecycle work item may collect before it is dropped. A route that is ON
|
|
710
|
+
* but keeps answering unusably (transport error, malformed reply) would otherwise re-queue the
|
|
711
|
+
* same positions on every write for the life of the store.
|
|
712
|
+
*/
|
|
713
|
+
const LIFECYCLE_RETIREMENT_MAX_DEFERRALS = 3;
|
|
714
|
+
/**
|
|
715
|
+
* Only an ANSWER can exhaust the bound. A transport error or timeout says nothing about whether
|
|
716
|
+
* the route can ever answer, and three timeouts is an ordinary afternoon on a busy GPU — counting
|
|
717
|
+
* them would throw away a retirement and leave the stale row it named live for good. A malformed
|
|
718
|
+
* or empty answer from a route that resolved ON is the failure that repeats, so it is the one
|
|
719
|
+
* that counts.
|
|
720
|
+
*/
|
|
721
|
+
function deferLifecycleRetirement(retiredClauses, incomingAssertion, liveSiblingSectionNames, attempts = 0, failure = "malformed-answer") {
|
|
722
|
+
const nextAttempts = failure === "transport" ? attempts : attempts + 1;
|
|
723
|
+
if (nextAttempts >= LIFECYCLE_RETIREMENT_MAX_DEFERRALS) {
|
|
724
|
+
log.warn("lifecycle retirement dropped after repeated unusable answers", {
|
|
725
|
+
positions: retiredClauses.length,
|
|
726
|
+
attempts: nextAttempts,
|
|
727
|
+
failure
|
|
728
|
+
}, {
|
|
729
|
+
event_name: "sno_station_mem.profile-section-writer.lifecycle.retirement.dropped.after.repeated.unusable.answers",
|
|
730
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
731
|
+
function: "deferLifecycleRetirement",
|
|
732
|
+
site_id: "profile-section-writer.deferLifecycleRetirement.f94b354ff4"
|
|
733
|
+
});
|
|
734
|
+
return {
|
|
735
|
+
kind: "classified",
|
|
736
|
+
positions: []
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
kind: "deferred",
|
|
741
|
+
work: {
|
|
742
|
+
positions: [...retiredClauses],
|
|
743
|
+
context: {
|
|
744
|
+
incomingAssertion,
|
|
745
|
+
liveSiblingSectionNames: [...liveSiblingSectionNames],
|
|
746
|
+
attempts: nextAttempts,
|
|
747
|
+
lastFailure: failure
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
async function lifecycleRetireByNamePositions(params, liveSiblingSectionNames, retiredClauses, incomingAssertion = params.newAssertion, attempts = 0) {
|
|
753
|
+
if (params.sectionName !== "preferences.general" || liveSiblingSectionNames.length === 0 || retiredClauses.length === 0) return {
|
|
754
|
+
kind: "classified",
|
|
755
|
+
positions: [...retiredClauses]
|
|
756
|
+
};
|
|
757
|
+
if (profileMergeCallHasNoModel(params, "P2")) return {
|
|
758
|
+
kind: "classified",
|
|
759
|
+
positions: []
|
|
760
|
+
};
|
|
761
|
+
let response;
|
|
762
|
+
try {
|
|
763
|
+
response = await params.llm?.completeJson({
|
|
764
|
+
prompt: [
|
|
765
|
+
"Separate lifecycle retirement from profile ownership cleanup.",
|
|
766
|
+
"Return JSON only: {\"lifecycle_retired_clause_indices\":[0]}.",
|
|
767
|
+
"Indices are zero-based positions into Removed clauses.",
|
|
768
|
+
"Include a clause only when the incoming assertion replaces it, removes it, or makes it historical.",
|
|
769
|
+
"Exclude a clause removed only because a live specific sibling section owns its topic.",
|
|
770
|
+
`Incoming assertion: ${JSON.stringify(incomingAssertion)}`,
|
|
771
|
+
`Removed clauses: ${JSON.stringify(retiredClauses)}`,
|
|
772
|
+
`Live specific sibling sections: ${JSON.stringify(liveSiblingSectionNames)}`
|
|
773
|
+
].join("\n\n"),
|
|
774
|
+
callId: "P2",
|
|
775
|
+
...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
|
|
776
|
+
...params.signal ? { signal: params.signal } : {}
|
|
777
|
+
});
|
|
778
|
+
} catch (error) {
|
|
779
|
+
rethrowCallerCancellation(error);
|
|
780
|
+
return deferLifecycleRetirement(retiredClauses, incomingAssertion, liveSiblingSectionNames, attempts, "transport");
|
|
781
|
+
}
|
|
782
|
+
const parsed = lifecycleRetirementResponseSchema.safeParse(response);
|
|
783
|
+
if (!parsed.success) return deferLifecycleRetirement(retiredClauses, incomingAssertion, liveSiblingSectionNames, attempts);
|
|
784
|
+
const indices = parsed.data.lifecycle_retired_clause_indices;
|
|
785
|
+
if (new Set(indices).size !== indices.length || indices.some((index) => index >= retiredClauses.length)) return deferLifecycleRetirement(retiredClauses, incomingAssertion, liveSiblingSectionNames, attempts);
|
|
786
|
+
return {
|
|
787
|
+
kind: "classified",
|
|
788
|
+
positions: indices.map((index) => retiredClauses[index] ?? "").filter(Boolean)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
/** The caller cancelled the whole operation; a model failure or a request timeout still degrades. */
|
|
792
|
+
function rethrowCallerCancellation(error) {
|
|
793
|
+
if (error instanceof LlmClientTerminalError && error.category === "cancelled" && !error.requestTimedOut) throw error;
|
|
794
|
+
}
|
|
795
|
+
async function runProfileSectionUpdate(params) {
|
|
796
|
+
const started = performance.now();
|
|
797
|
+
try {
|
|
798
|
+
const trimmedSectionName = params.sectionName.trim();
|
|
799
|
+
if (!trimmedSectionName) throw new Error("profile-section-writer: sectionName is required");
|
|
800
|
+
if (!params.scope.trim()) throw new Error("profile-section-writer: scope is required");
|
|
801
|
+
const sectionName = canonicalizeProfileSectionName(trimmedSectionName);
|
|
802
|
+
const normalizedParams = sectionName === params.sectionName ? params : {
|
|
803
|
+
...params,
|
|
804
|
+
sectionName
|
|
805
|
+
};
|
|
806
|
+
if (sectionName === "active_tasks") {
|
|
807
|
+
const result = await runTypedActiveTaskUpdate(normalizedParams);
|
|
808
|
+
log.info("Profile task update completed", {
|
|
809
|
+
outcome: result.outcome,
|
|
810
|
+
memory_id: result.rowId,
|
|
811
|
+
duration_ms: performance.now() - started,
|
|
812
|
+
committed_count: "unavailable",
|
|
813
|
+
committed_count_reason: "task_route_returns_outcome_not_count"
|
|
814
|
+
}, {
|
|
815
|
+
event_name: "memory.profile.completed",
|
|
816
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
817
|
+
function: "runProfileSectionUpdate",
|
|
818
|
+
site_id: "memory.profile.task.completed"
|
|
819
|
+
});
|
|
820
|
+
return result;
|
|
821
|
+
}
|
|
822
|
+
const result = await runWithMutationAttempt({
|
|
823
|
+
stateDir: dirname(params.store.dbPath),
|
|
824
|
+
event: "memory_superseded",
|
|
825
|
+
operation: "profile-section-update",
|
|
826
|
+
writer: "profile-section",
|
|
827
|
+
subject: sectionName,
|
|
828
|
+
run: async () => {
|
|
829
|
+
try {
|
|
830
|
+
return await runProfileSectionUpdateOnce(normalizedParams);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (!(error instanceof StaleSupersedeTargetError)) throw error;
|
|
833
|
+
return runProfileSectionUpdateOnce(normalizedParams);
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
completedOutcome: (completed) => ({
|
|
837
|
+
outcome: completed.mutationOutcome,
|
|
838
|
+
...completed.refusalReason === void 0 ? {} : { refusalReason: completed.refusalReason }
|
|
839
|
+
})
|
|
840
|
+
});
|
|
841
|
+
log.info("Profile section update completed", {
|
|
842
|
+
outcome: result.outcome,
|
|
843
|
+
mutation_outcome: result.mutationOutcome,
|
|
844
|
+
memory_id: result.rowId,
|
|
845
|
+
duration_ms: performance.now() - started,
|
|
846
|
+
reason_code: result.refusalReason ?? "not_refused",
|
|
847
|
+
committed_count: "unavailable",
|
|
848
|
+
committed_count_reason: "writer_returns_outcome_not_count"
|
|
849
|
+
}, {
|
|
850
|
+
event_name: "memory.profile.completed",
|
|
851
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
852
|
+
function: "runProfileSectionUpdate",
|
|
853
|
+
site_id: "memory.profile.section.completed"
|
|
854
|
+
});
|
|
855
|
+
return {
|
|
856
|
+
outcome: result.outcome,
|
|
857
|
+
rowId: result.rowId
|
|
858
|
+
};
|
|
859
|
+
} catch (error) {
|
|
860
|
+
log.warn("Profile section update failed", {
|
|
861
|
+
outcome: "failed",
|
|
862
|
+
error,
|
|
863
|
+
duration_ms: performance.now() - started,
|
|
864
|
+
committed_count: "unavailable",
|
|
865
|
+
committed_count_reason: "multi_step_write_failed"
|
|
866
|
+
}, {
|
|
867
|
+
event_name: "memory.profile.failed",
|
|
868
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
869
|
+
function: "runProfileSectionUpdate",
|
|
870
|
+
site_id: "memory.profile.failed"
|
|
871
|
+
});
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function runTypedActiveTaskUpdate(params) {
|
|
876
|
+
const sessionKey = params.source.sessionKey ?? params.source.messageId;
|
|
877
|
+
const replayIdentity = params.source.messageId;
|
|
878
|
+
if (!sessionKey || !replayIdentity) throw new Error("profile-section-writer: active task updates require an authorized replay identity");
|
|
879
|
+
const route = await routeTaskLifecycleCandidate({
|
|
880
|
+
projectId: params.scope,
|
|
881
|
+
candidateText: params.newAssertion,
|
|
882
|
+
confirmedTaskCandidate: params.confirmedTaskCandidate !== false,
|
|
883
|
+
source: {
|
|
884
|
+
kind: "authorized_untraced",
|
|
885
|
+
sessionKey,
|
|
886
|
+
replayIdentity,
|
|
887
|
+
assertionOrdinal: 0
|
|
888
|
+
},
|
|
889
|
+
firstResolutionNowMs: params.at,
|
|
890
|
+
store: params.store,
|
|
891
|
+
...params.llm === void 0 ? {} : { llm: params.llm },
|
|
892
|
+
...params.timeoutMs === void 0 ? {} : { timeoutMs: params.timeoutMs },
|
|
893
|
+
...params.signal === void 0 ? {} : { signal: params.signal }
|
|
894
|
+
});
|
|
895
|
+
if (route.status === "none") {
|
|
896
|
+
const safetyNet = await writeEpisodicSafetyNet(params, "task_lifecycle_none");
|
|
897
|
+
return {
|
|
898
|
+
outcome: safetyNet.replayed ? "no-op" : "appended",
|
|
899
|
+
rowId: safetyNet.rowId
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
const { result } = route;
|
|
903
|
+
return {
|
|
904
|
+
outcome: result.write.replayed || result.write.result === "none" || result.write.result === "uncertain" ? "no-op" : result.write.result === "created_instance" || result.write.result === "created_unresolved_instance" ? "appended" : "merged",
|
|
905
|
+
rowId: result.write.activeTaskId ?? ""
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
async function runProfileSectionUpdateOnce(params) {
|
|
909
|
+
const sectionName = params.sectionName;
|
|
910
|
+
const factKey = profileFactKey(sectionName);
|
|
911
|
+
const countRecordTokens = await recordTokenCounter(params.store.embedder);
|
|
912
|
+
const existing = params.store.getByFactKey(params.scope, factKey);
|
|
913
|
+
if (!existing) {
|
|
914
|
+
const content = params.newAssertion;
|
|
915
|
+
const written = await writeProfileRow({
|
|
916
|
+
params,
|
|
917
|
+
content,
|
|
918
|
+
input: profileStoreInput({
|
|
919
|
+
countRecordTokens,
|
|
920
|
+
scope: params.scope,
|
|
921
|
+
sectionName,
|
|
922
|
+
topic: params.topic,
|
|
923
|
+
content,
|
|
924
|
+
evidence: params.evidence,
|
|
925
|
+
metadataPatch: params.metadataPatch,
|
|
926
|
+
source: params.source,
|
|
927
|
+
at: params.at,
|
|
928
|
+
supersedes: void 0
|
|
929
|
+
}),
|
|
930
|
+
outcome: "appended"
|
|
931
|
+
});
|
|
932
|
+
return {
|
|
933
|
+
...written,
|
|
934
|
+
mutationOutcome: written.outcome === "no-op" ? "no-mutation" : "committed"
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
const existingValidFrom = parseInsightMetadata(existing.metadata, existing).valid_from;
|
|
938
|
+
if (typeof existingValidFrom === "number" && params.at < existingValidFrom) {
|
|
939
|
+
await resumeRetireByNamePending(existing, params);
|
|
940
|
+
return {
|
|
941
|
+
outcome: "no-op",
|
|
942
|
+
rowId: existing.id,
|
|
943
|
+
mutationOutcome: "no-mutation"
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const existingContent = parseInsightMetadata(existing.metadata, existing).l2_content;
|
|
947
|
+
const existingIsRetiredMarker = typeof existingContent === "string" && normalizeExactAssertionText(existingContent) === normalizeExactAssertionText(retiredProfileSectionMarker(sectionName));
|
|
948
|
+
if (typeof existingContent === "string" && normalizeExactAssertionText(existingContent) === normalizeExactAssertionText(params.newAssertion)) {
|
|
949
|
+
await resumeRetireByNamePending(await advanceProfileConfirmation(existing, existingValidFrom, params), params);
|
|
950
|
+
return {
|
|
951
|
+
outcome: "no-op",
|
|
952
|
+
rowId: existing.id,
|
|
953
|
+
mutationOutcome: "no-mutation"
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
const tombstoneReplay = await tombstoneReplayResult(params, factKey);
|
|
957
|
+
if (tombstoneReplay) return {
|
|
958
|
+
...tombstoneReplay,
|
|
959
|
+
mutationOutcome: "no-mutation"
|
|
960
|
+
};
|
|
961
|
+
const merged = await mergeProfileSection(existing, params);
|
|
962
|
+
if (merged.kind === "preserved-without-adjudication") {
|
|
963
|
+
if (typeof existingContent === "string" && normalizeExactAssertionText(merged.content) === normalizeExactAssertionText(existingContent)) {
|
|
964
|
+
await resumeRetireByNamePending(existing, params);
|
|
965
|
+
return {
|
|
966
|
+
outcome: "no-op",
|
|
967
|
+
rowId: existing.id,
|
|
968
|
+
mutationOutcome: "no-mutation"
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const written = await writeProfileRow({
|
|
972
|
+
params,
|
|
973
|
+
content: merged.content,
|
|
974
|
+
input: profileStoreInput({
|
|
975
|
+
countRecordTokens,
|
|
976
|
+
scope: params.scope,
|
|
977
|
+
sectionName,
|
|
978
|
+
topic: params.topic,
|
|
979
|
+
content: merged.content,
|
|
980
|
+
evidence: params.evidence,
|
|
981
|
+
metadataPatch: {
|
|
982
|
+
...params.metadataPatch,
|
|
983
|
+
merged_without_adjudication: true,
|
|
984
|
+
adjudication_failure_reason: merged.reason
|
|
985
|
+
},
|
|
986
|
+
source: params.source,
|
|
987
|
+
at: params.at,
|
|
988
|
+
supersedes: existing.id
|
|
989
|
+
}),
|
|
990
|
+
existing,
|
|
991
|
+
outcome: "merged"
|
|
992
|
+
});
|
|
993
|
+
return {
|
|
994
|
+
...written,
|
|
995
|
+
mutationOutcome: written.outcome === "no-op" ? "no-mutation" : "preserved-without-adjudication"
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (merged.kind === "no-op") {
|
|
999
|
+
await resumeRetireByNamePending(await advanceProfileConfirmation(existing, existingValidFrom, params), params);
|
|
1000
|
+
return {
|
|
1001
|
+
outcome: "no-op",
|
|
1002
|
+
rowId: existing.id,
|
|
1003
|
+
mutationOutcome: "no-mutation"
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
if (merged.kind === "tombstone") {
|
|
1007
|
+
if (existingIsRetiredMarker) {
|
|
1008
|
+
await resumeRetireByNamePending(existing, params);
|
|
1009
|
+
let event;
|
|
1010
|
+
try {
|
|
1011
|
+
event = await params.store.supersede({
|
|
1012
|
+
create: tombstoneEventStoreInput(params, existing.id),
|
|
1013
|
+
closes: [],
|
|
1014
|
+
activeFactGuard: {
|
|
1015
|
+
factKey,
|
|
1016
|
+
expectedId: existing.id
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
const replay = await tombstoneReplayResult(params, factKey);
|
|
1021
|
+
if (replay) return {
|
|
1022
|
+
...replay,
|
|
1023
|
+
mutationOutcome: "no-mutation"
|
|
1024
|
+
};
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
log.warn("retired profile section tombstone reaffirmed", {
|
|
1028
|
+
eventRowId: event.id,
|
|
1029
|
+
markerRowId: existing.id,
|
|
1030
|
+
section: sectionName,
|
|
1031
|
+
verdict: merged.kind
|
|
1032
|
+
}, {
|
|
1033
|
+
event_name: "sno_station_mem.profile-section-writer.retired.profile.section.tombstone.reaffirmed",
|
|
1034
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
1035
|
+
function: "runProfileSectionUpdateOnce",
|
|
1036
|
+
site_id: "profile-section-writer.runProfileSectionUpdateOnce.28936dcd3c"
|
|
1037
|
+
});
|
|
1038
|
+
return {
|
|
1039
|
+
outcome: "tombstoned",
|
|
1040
|
+
rowId: existing.id,
|
|
1041
|
+
mutationOutcome: "committed"
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
const tombstonePending = appendRetireByNameWork(readRetireByNamePending(existing), profileMergeCallHasNoModel(params, "P6") ? [] : splitExactClauses(existingContent ?? existing.text), params, [existing.id], "profile-tombstone");
|
|
1045
|
+
const markerInput = profileStoreInput({
|
|
1046
|
+
countRecordTokens,
|
|
1047
|
+
scope: params.scope,
|
|
1048
|
+
sectionName,
|
|
1049
|
+
topic: params.topic,
|
|
1050
|
+
content: retiredProfileSectionMarker(sectionName),
|
|
1051
|
+
metadataPatch: {
|
|
1052
|
+
idempotency_key: tombstoneMarkerIdempotencyKey(params),
|
|
1053
|
+
content_identity_key: tombstoneMarkerIdempotencyKey(params)
|
|
1054
|
+
},
|
|
1055
|
+
source: params.source,
|
|
1056
|
+
at: params.at,
|
|
1057
|
+
supersedes: existing.id
|
|
1058
|
+
});
|
|
1059
|
+
const replacement = tombstonePending === void 0 ? markerInput : {
|
|
1060
|
+
...markerInput,
|
|
1061
|
+
metadata: addRetireByNamePending(markerInput.metadata, tombstonePending)
|
|
1062
|
+
};
|
|
1063
|
+
let created;
|
|
1064
|
+
try {
|
|
1065
|
+
created = await params.store.createEventAndSupersede({
|
|
1066
|
+
event: tombstoneEventStoreInput(params, existing.id),
|
|
1067
|
+
replacement,
|
|
1068
|
+
closeExisting: [{
|
|
1069
|
+
id: existing.id,
|
|
1070
|
+
buildMetadata: ({ replacementId }) => closeProfileMetadata(existing, replacementId, params.at)
|
|
1071
|
+
}],
|
|
1072
|
+
profileRecovery: {
|
|
1073
|
+
mutationAttemptId: currentMutationAttemptId(),
|
|
1074
|
+
sectionName,
|
|
1075
|
+
removedAtMs: params.at
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
const replay = await tombstoneReplayResult(params, factKey);
|
|
1080
|
+
if (replay) return {
|
|
1081
|
+
...replay,
|
|
1082
|
+
mutationOutcome: "no-mutation"
|
|
1083
|
+
};
|
|
1084
|
+
throw error;
|
|
1085
|
+
}
|
|
1086
|
+
if (tombstonePending) await completeRetireByNamePending(created.replacement, params, tombstonePending);
|
|
1087
|
+
log.warn("profile section tombstoned; incoming assertion preserved", {
|
|
1088
|
+
closedRowId: existing.id,
|
|
1089
|
+
eventRowId: created.event.id,
|
|
1090
|
+
markerRowId: created.replacement.id,
|
|
1091
|
+
section: sectionName,
|
|
1092
|
+
verdict: merged.kind
|
|
1093
|
+
}, {
|
|
1094
|
+
event_name: "sno_station_mem.profile-section-writer.profile.section.tombstoned.incoming.assertion.preserved",
|
|
1095
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
1096
|
+
function: "runProfileSectionUpdateOnce",
|
|
1097
|
+
site_id: "profile-section-writer.runProfileSectionUpdateOnce.e3afdfb6d8"
|
|
1098
|
+
});
|
|
1099
|
+
return {
|
|
1100
|
+
outcome: "tombstoned",
|
|
1101
|
+
rowId: created.replacement.id,
|
|
1102
|
+
mutationOutcome: "committed"
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
if (typeof existingContent === "string" && normalizeExactAssertionText(merged.content) === normalizeExactAssertionText(existingContent)) {
|
|
1106
|
+
await resumeRetireByNamePending(existing, params);
|
|
1107
|
+
return {
|
|
1108
|
+
outcome: "no-op",
|
|
1109
|
+
rowId: existing.id,
|
|
1110
|
+
mutationOutcome: "no-mutation"
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
const written = await writeProfileRow({
|
|
1114
|
+
params,
|
|
1115
|
+
content: merged.content,
|
|
1116
|
+
input: profileStoreInput({
|
|
1117
|
+
countRecordTokens,
|
|
1118
|
+
scope: params.scope,
|
|
1119
|
+
sectionName,
|
|
1120
|
+
topic: params.topic,
|
|
1121
|
+
content: merged.content,
|
|
1122
|
+
evidence: params.evidence,
|
|
1123
|
+
metadataPatch: {
|
|
1124
|
+
...params.metadataPatch,
|
|
1125
|
+
...merged.retirementRecheck ? { profile_retirement_recheck: merged.retirementRecheck } : {}
|
|
1126
|
+
},
|
|
1127
|
+
source: params.source,
|
|
1128
|
+
at: params.at,
|
|
1129
|
+
supersedes: existing.id,
|
|
1130
|
+
abstract: sectionName === "identity" ? void 0 : merged.abstract,
|
|
1131
|
+
overview: sectionName === "identity" ? void 0 : merged.overview
|
|
1132
|
+
}),
|
|
1133
|
+
existing,
|
|
1134
|
+
retiredPositions: merged.retireByNamePositions,
|
|
1135
|
+
...merged.deferredLifecycleRetirement ? { deferredLifecycleRetirement: merged.deferredLifecycleRetirement } : {},
|
|
1136
|
+
outcome: "merged"
|
|
1137
|
+
});
|
|
1138
|
+
if (merged.textRepair !== void 0) log.warn("profile text step not used; row written from the judged clauses", {
|
|
1139
|
+
memory_id: existing.id,
|
|
1140
|
+
section: sectionName,
|
|
1141
|
+
reason_code: "text_repair_required",
|
|
1142
|
+
reason_detail: merged.textRepair.reason
|
|
1143
|
+
}, {
|
|
1144
|
+
event_name: "sno_station_mem.profile-section-writer.profile.text.step.not.used.row.written.from.the.judged.clauses",
|
|
1145
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
1146
|
+
function: "runProfileSectionUpdateOnce",
|
|
1147
|
+
site_id: "profile-section-writer.runProfileSectionUpdateOnce.80241da6d8"
|
|
1148
|
+
});
|
|
1149
|
+
return {
|
|
1150
|
+
...written,
|
|
1151
|
+
mutationOutcome: written.outcome === "no-op" ? "no-mutation" : merged.textRepair !== void 0 ? "repaired-merge-text" : "committed"
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function profileFactKey(sectionName) {
|
|
1155
|
+
const factKey = deriveFactKey({
|
|
1156
|
+
kind: "profile",
|
|
1157
|
+
section_name: sectionName
|
|
1158
|
+
});
|
|
1159
|
+
if (!factKey) throw new Error("profile-section-writer: unable to derive profile fact_key");
|
|
1160
|
+
return factKey;
|
|
1161
|
+
}
|
|
1162
|
+
const PROFILE_RETIREMENT_RECHECK_CONCURRENCY = 4;
|
|
1163
|
+
const PROFILE_RETIREMENT_RECHECK_MAX_TOKENS = 64;
|
|
1164
|
+
const PROFILE_RETIREMENT_RECHECK_SIBLING_CHARS = 600;
|
|
1165
|
+
const PROFILE_RETIREMENT_RECHECK_PROMPT_TEMPLATE = [RETIRED_POSITION_JUDGMENT_SKILL.retirementRecheck, "Return JSON only, exactly one boolean field named retire: {\"retire\":true} or {\"retire\":false}."].join("\n\n");
|
|
1166
|
+
const PROFILE_RETIREMENT_RECHECK_PROMPT_SHA256 = createHash("sha256").update(PROFILE_RETIREMENT_RECHECK_PROMPT_TEMPLATE).digest("hex");
|
|
1167
|
+
const PROFILE_RETIREMENT_RECHECK_SAMPLING = {
|
|
1168
|
+
temperature: "provider-default",
|
|
1169
|
+
enableThinking: false,
|
|
1170
|
+
maxTokens: PROFILE_RETIREMENT_RECHECK_MAX_TOKENS
|
|
1171
|
+
};
|
|
1172
|
+
function parseRecheckVerdict(value) {
|
|
1173
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1174
|
+
const record = value;
|
|
1175
|
+
if (Object.keys(record).length !== 1 || typeof record["retire"] !== "boolean") return;
|
|
1176
|
+
return record["retire"];
|
|
1177
|
+
}
|
|
1178
|
+
function renderRetirementRecheckPrompt(input) {
|
|
1179
|
+
const siblings = {};
|
|
1180
|
+
if (input.sectionName === "preferences.general") for (const [name, content] of input.siblingContents) siblings[name] = content.slice(0, PROFILE_RETIREMENT_RECHECK_SIBLING_CHARS);
|
|
1181
|
+
return [
|
|
1182
|
+
PROFILE_RETIREMENT_RECHECK_PROMPT_TEMPLATE,
|
|
1183
|
+
`Section: ${input.sectionName}`,
|
|
1184
|
+
`Stored clause (retired): ${JSON.stringify(input.storedClause)}`,
|
|
1185
|
+
`Incoming assertion: ${JSON.stringify(input.incomingAssertion)}`,
|
|
1186
|
+
`Live sibling sections and their content: ${JSON.stringify(siblings)}`
|
|
1187
|
+
].join("\n\n");
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* The second key on every clause the profile judgement retired from `preferences.general`.
|
|
1191
|
+
* Measured 2026-09-01 on the Memora store: 753 clauses were retired across 1,149
|
|
1192
|
+
* rewrites, 317 of them shared no content word with the incoming assertion, and 238 of those in
|
|
1193
|
+
* the general section had no live sibling carrying them; replayed one by one, 39 of 40 such
|
|
1194
|
+
* retirements were judged wrong, while 14 of 20 genuine replacements were still retired. A
|
|
1195
|
+
* retirement stands only when this focused question also answers yes; an unusable answer keeps
|
|
1196
|
+
* the clause.
|
|
1197
|
+
*/
|
|
1198
|
+
async function recheckProfileRetirements(input) {
|
|
1199
|
+
const llm = input.params.llm;
|
|
1200
|
+
if (!llm || input.retiredClauseIndices.length === 0) return { retiredClauseIndices: [...input.retiredClauseIndices] };
|
|
1201
|
+
const durationMs = Math.min(input.params.timeoutMs ?? 3e4, RETIRE_BY_NAME_MAX_DURATION_MS);
|
|
1202
|
+
const deadlineMs = Date.now() + durationMs;
|
|
1203
|
+
const candidates = input.retiredClauseIndices.slice(0, 16);
|
|
1204
|
+
const checked = [];
|
|
1205
|
+
const verdicts = [];
|
|
1206
|
+
for (let offset = 0; offset < candidates.length;) {
|
|
1207
|
+
if (Date.now() >= deadlineMs) break;
|
|
1208
|
+
const batchSize = Math.min(PROFILE_RETIREMENT_RECHECK_CONCURRENCY, candidates.length - offset);
|
|
1209
|
+
const batch = candidates.slice(offset, offset + batchSize);
|
|
1210
|
+
const remainingMs = Math.max(1, deadlineMs - Date.now());
|
|
1211
|
+
checked.push(...batch);
|
|
1212
|
+
verdicts.push(...await Promise.all(batch.map(async (index) => {
|
|
1213
|
+
const clause = input.clauses[index];
|
|
1214
|
+
if (!clause) return void 0;
|
|
1215
|
+
try {
|
|
1216
|
+
return parseRecheckVerdict(await llm.completeJson({
|
|
1217
|
+
prompt: renderRetirementRecheckPrompt({
|
|
1218
|
+
sectionName: input.params.sectionName,
|
|
1219
|
+
storedClause: clause.value,
|
|
1220
|
+
incomingAssertion: input.params.newAssertion,
|
|
1221
|
+
siblingContents: input.siblingContents
|
|
1222
|
+
}),
|
|
1223
|
+
callId: "P3",
|
|
1224
|
+
maxTokens: PROFILE_RETIREMENT_RECHECK_MAX_TOKENS,
|
|
1225
|
+
enableThinking: false,
|
|
1226
|
+
timeoutMs: Math.min(input.params.timeoutMs ?? remainingMs, remainingMs),
|
|
1227
|
+
...input.params.signal ? { signal: input.params.signal } : {}
|
|
1228
|
+
}));
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
rethrowCallerCancellation(error);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
})));
|
|
1234
|
+
offset += batch.length;
|
|
1235
|
+
}
|
|
1236
|
+
let resolved;
|
|
1237
|
+
try {
|
|
1238
|
+
resolved = await llm.getResolvedConfig();
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
log.warn("profile retirement recheck could not resolve the model for its receipt", {
|
|
1241
|
+
section: input.params.sectionName,
|
|
1242
|
+
error
|
|
1243
|
+
}, {
|
|
1244
|
+
event_name: "sno_station_mem.profile-section-writer.profile.retirement.recheck.could.not.resolve.the.model.for.its.receipt",
|
|
1245
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
1246
|
+
function: "recheckProfileRetirements",
|
|
1247
|
+
site_id: "profile-section-writer.recheckProfileRetirements.613c569527"
|
|
1248
|
+
});
|
|
1249
|
+
resolved = {
|
|
1250
|
+
model: "unresolved",
|
|
1251
|
+
preset: "unresolved"
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const retiredClauseIndices = [];
|
|
1255
|
+
const retired = [];
|
|
1256
|
+
const kept = [];
|
|
1257
|
+
checked.forEach((index, position) => {
|
|
1258
|
+
const value = input.clauses[index]?.value ?? "";
|
|
1259
|
+
if (verdicts[position] === true) {
|
|
1260
|
+
retiredClauseIndices.push(index);
|
|
1261
|
+
retired.push(value);
|
|
1262
|
+
} else kept.push(value);
|
|
1263
|
+
});
|
|
1264
|
+
const unchecked = input.retiredClauseIndices.length - checked.length;
|
|
1265
|
+
const route = input.params.routing ? resolveLlmRoute({
|
|
1266
|
+
callId: "P3",
|
|
1267
|
+
config: input.params.routing
|
|
1268
|
+
}) : null;
|
|
1269
|
+
const destination = route === null ? "host" : "off" in route ? "off" : route.destination;
|
|
1270
|
+
return {
|
|
1271
|
+
retiredClauseIndices,
|
|
1272
|
+
receipt: {
|
|
1273
|
+
call_id: "P3",
|
|
1274
|
+
destination,
|
|
1275
|
+
prompt_sha256: PROFILE_RETIREMENT_RECHECK_PROMPT_SHA256,
|
|
1276
|
+
model: destination === "host" ? "host" : resolved.model,
|
|
1277
|
+
preset: destination === "host" ? "host" : resolved.preset,
|
|
1278
|
+
sampling: PROFILE_RETIREMENT_RECHECK_SAMPLING,
|
|
1279
|
+
retired,
|
|
1280
|
+
kept,
|
|
1281
|
+
unchecked
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
async function mergeProfileSection(existing, params) {
|
|
1286
|
+
const existingContent = parseInsightMetadata(existing.metadata, existing).l2_content;
|
|
1287
|
+
const mergeBase = typeof existingContent === "string" && normalizeExactAssertionText(existingContent) === normalizeExactAssertionText(retiredProfileSectionMarker(params.sectionName)) ? "" : typeof existingContent === "string" ? existingContent : existing.text;
|
|
1288
|
+
const clausesByValue = /* @__PURE__ */ new Map();
|
|
1289
|
+
for (const value of splitExactClauses(mergeBase)) clausesByValue.set(value, {
|
|
1290
|
+
origin: "stored",
|
|
1291
|
+
value
|
|
1292
|
+
});
|
|
1293
|
+
for (const value of splitExactClauses(params.newAssertion)) clausesByValue.set(value, {
|
|
1294
|
+
origin: "incoming",
|
|
1295
|
+
value
|
|
1296
|
+
});
|
|
1297
|
+
const clauses = [...clausesByValue.values()];
|
|
1298
|
+
const liveSiblings = await listLiveSpecificSiblings(params);
|
|
1299
|
+
const liveSiblingSectionNames = liveSiblings.map((sibling) => sibling.name);
|
|
1300
|
+
const judgmentPrompt = renderProfileSectionJudgmentPrompt({
|
|
1301
|
+
sectionName: params.sectionName,
|
|
1302
|
+
topic: params.topic,
|
|
1303
|
+
clauses,
|
|
1304
|
+
evidence: params.evidence,
|
|
1305
|
+
liveSiblingSectionNames
|
|
1306
|
+
});
|
|
1307
|
+
let response;
|
|
1308
|
+
let transportFailure;
|
|
1309
|
+
if (params.llm) try {
|
|
1310
|
+
response = await params.llm.completeJson({
|
|
1311
|
+
prompt: judgmentPrompt,
|
|
1312
|
+
callId: "P4",
|
|
1313
|
+
...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
|
|
1314
|
+
...params.signal ? { signal: params.signal } : {},
|
|
1315
|
+
accept: (value) => parseProfileSectionJudgment(value, clauses) !== void 0
|
|
1316
|
+
});
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
rethrowCallerCancellation(error);
|
|
1319
|
+
transportFailure = error instanceof LlmClientTerminalError ? error.category : "unknown";
|
|
1320
|
+
}
|
|
1321
|
+
const judgment = parseProfileSectionJudgment(response, clauses);
|
|
1322
|
+
const noOpWouldDrop = judgment?.verdict === "no-op" && !sectionAlreadyCovers(mergeBase, params.newAssertion);
|
|
1323
|
+
if (!judgment || noOpWouldDrop) {
|
|
1324
|
+
const reason = noOpWouldDrop ? "no-op-would-drop-incoming" : transportFailure !== void 0 ? `transport-${transportFailure}` : response == null ? "null-or-absent-judgment" : "malformed-judgment";
|
|
1325
|
+
log.warn("profile judgment unusable; deterministic preservation selected", {
|
|
1326
|
+
section: params.sectionName,
|
|
1327
|
+
reason
|
|
1328
|
+
}, {
|
|
1329
|
+
event_name: "sno_station_mem.profile-section-writer.profile.judgment.unusable.deterministic.preservation.selected",
|
|
1330
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
1331
|
+
function: "mergeProfileSection",
|
|
1332
|
+
site_id: "profile-section-writer.mergeProfileSection.c707285ef3"
|
|
1333
|
+
});
|
|
1334
|
+
return {
|
|
1335
|
+
kind: "preserved-without-adjudication",
|
|
1336
|
+
content: fallbackProfileMerge(mergeBase, params.newAssertion),
|
|
1337
|
+
reason
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
if (judgment.verdict === "no-op") return { kind: "no-op" };
|
|
1341
|
+
if (judgment.verdict === "tombstone") return { kind: "tombstone" };
|
|
1342
|
+
const recheck = params.sectionName === "preferences.general" ? await recheckProfileRetirements({
|
|
1343
|
+
params,
|
|
1344
|
+
clauses,
|
|
1345
|
+
retiredClauseIndices: judgment.retiredClauseIndices,
|
|
1346
|
+
siblingContents: judgment.retiredClauseIndices.length > 0 ? new Map(liveSiblings.map((sibling) => [sibling.name, sibling.content])) : /* @__PURE__ */ new Map()
|
|
1347
|
+
}) : { retiredClauseIndices: [...judgment.retiredClauseIndices] };
|
|
1348
|
+
const retiredClauses = recheck.retiredClauseIndices.map((index) => clauses[index]?.value ?? "");
|
|
1349
|
+
const lifecycleRetirement = await lifecycleRetireByNamePositions(params, liveSiblingSectionNames, retiredClauses);
|
|
1350
|
+
const retireByNamePositions = lifecycleRetirement.kind === "classified" ? lifecycleRetirement.positions : [];
|
|
1351
|
+
const deferredLifecycleRetirement = lifecycleRetirement.kind === "deferred" ? lifecycleRetirement.work : void 0;
|
|
1352
|
+
const retiredIndices = new Set(recheck.retiredClauseIndices);
|
|
1353
|
+
const retainedClauses = clauses.flatMap((clause, index) => retiredIndices.has(index) ? [] : [clause.value]);
|
|
1354
|
+
let textResponse;
|
|
1355
|
+
try {
|
|
1356
|
+
textResponse = await params.llm?.completeJson({
|
|
1357
|
+
prompt: renderProfileSectionTextPrompt({
|
|
1358
|
+
sectionName: params.sectionName,
|
|
1359
|
+
topic: params.topic,
|
|
1360
|
+
retainedClauses,
|
|
1361
|
+
retiredClauses,
|
|
1362
|
+
evidence: params.evidence
|
|
1363
|
+
}),
|
|
1364
|
+
callId: "P5",
|
|
1365
|
+
...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
|
|
1366
|
+
...params.signal ? { signal: params.signal } : {}
|
|
1367
|
+
});
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
rethrowCallerCancellation(error);
|
|
1370
|
+
return judgedClauseMerge(retainedClauses, retireByNamePositions, `text-step-${error instanceof LlmClientTerminalError ? error.category : "unknown"}`, deferredLifecycleRetirement, recheck.receipt);
|
|
1371
|
+
}
|
|
1372
|
+
if (textResponse == null || typeof textResponse === "object" && textResponse !== null && "content" in textResponse && typeof textResponse.content === "string" && textResponse.content.trim() === "") return judgedClauseMerge(retainedClauses, retireByNamePositions, "text-step-returned-empty", deferredLifecycleRetirement, recheck.receipt);
|
|
1373
|
+
const parsedText = profileTextResponseSchema.safeParse(textResponse);
|
|
1374
|
+
if (!parsedText.success) return judgedClauseMerge(retainedClauses, retireByNamePositions, "text-step-malformed-response", deferredLifecycleRetirement, recheck.receipt);
|
|
1375
|
+
const content = parsedText.data.content.trim();
|
|
1376
|
+
if (!content) return judgedClauseMerge(retainedClauses, retireByNamePositions, "text-step-returned-empty", deferredLifecycleRetirement, recheck.receipt);
|
|
1377
|
+
const contentClauses = splitExactClauses(content);
|
|
1378
|
+
const sortedRetainedClauses = [...retainedClauses].sort();
|
|
1379
|
+
const sortedContentClauses = [...contentClauses].sort();
|
|
1380
|
+
if (sortedContentClauses.length === sortedRetainedClauses.length && sortedContentClauses.every((clause, index) => clause === sortedRetainedClauses[index])) return {
|
|
1381
|
+
kind: "merge",
|
|
1382
|
+
content,
|
|
1383
|
+
abstract: parsedText.data.abstract,
|
|
1384
|
+
overview: parsedText.data.overview,
|
|
1385
|
+
retireByNamePositions,
|
|
1386
|
+
...deferredLifecycleRetirement ? { deferredLifecycleRetirement } : {},
|
|
1387
|
+
retainedClauses,
|
|
1388
|
+
...recheck.receipt ? { retirementRecheck: recheck.receipt } : {}
|
|
1389
|
+
};
|
|
1390
|
+
return judgedClauseMerge(retainedClauses, retireByNamePositions, "text-step-changed-the-clause-set", deferredLifecycleRetirement, recheck.receipt);
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* The merge the judgment itself already decided: exactly the clauses it kept, one per line.
|
|
1394
|
+
*
|
|
1395
|
+
* Used whenever the text step cannot be trusted to produce them — it failed, it returned nothing
|
|
1396
|
+
* usable, or it returned a different clause set. This never refuses. A refusal here used to leave
|
|
1397
|
+
* the row stale AND file the incoming fact as an episodic row, so an ordinary dependency failure
|
|
1398
|
+
* silently cost the profile its update while the caller was told a row had been written.
|
|
1399
|
+
*/
|
|
1400
|
+
function judgedClauseMerge(retainedClauses, retireByNamePositions, reason, deferredLifecycleRetirement, retirementRecheck) {
|
|
1401
|
+
return {
|
|
1402
|
+
kind: "merge",
|
|
1403
|
+
content: retainedClauses.join("\n"),
|
|
1404
|
+
retireByNamePositions,
|
|
1405
|
+
...deferredLifecycleRetirement ? { deferredLifecycleRetirement } : {},
|
|
1406
|
+
retainedClauses,
|
|
1407
|
+
textRepair: { reason },
|
|
1408
|
+
...retirementRecheck ? { retirementRecheck } : {}
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
function splitExactClauses(text) {
|
|
1412
|
+
const rawClauses = [];
|
|
1413
|
+
const separators = [];
|
|
1414
|
+
let cursor = 0;
|
|
1415
|
+
for (const boundary of text.matchAll(/\n+|(?<=[.!?;])\s+/gu)) {
|
|
1416
|
+
rawClauses.push(text.slice(cursor, boundary.index));
|
|
1417
|
+
separators.push(boundary[0]);
|
|
1418
|
+
cursor = boundary.index + boundary[0].length;
|
|
1419
|
+
}
|
|
1420
|
+
rawClauses.push(text.slice(cursor));
|
|
1421
|
+
if (rawClauses.map((clause, index) => `${clause}${separators[index] ?? ""}`).join("") !== text) throw new Error("profile-section-writer: exact clause segmentation was not lossless");
|
|
1422
|
+
return rawClauses.map((clause) => clause.trim()).filter((clause) => clause.length > 0);
|
|
1423
|
+
}
|
|
1424
|
+
function profileStoreInput(args) {
|
|
1425
|
+
const writerAgentId = resolveHookAgentId(args.source.agentId, args.source.sessionKey).agentId;
|
|
1426
|
+
const rawText = args.content.trim();
|
|
1427
|
+
const abstract = args.abstract?.trim() || firstSentence(rawText);
|
|
1428
|
+
const text = boundSectionContent(rawText, abstract, args.countRecordTokens);
|
|
1429
|
+
const overview = args.overview?.trim() || `- ${text}`;
|
|
1430
|
+
const metadata = buildInsightMetadata({
|
|
1431
|
+
text,
|
|
1432
|
+
category: "profile",
|
|
1433
|
+
timestamp: args.at
|
|
1434
|
+
}, {
|
|
1435
|
+
l0_abstract: abstract,
|
|
1436
|
+
l1_overview: overview,
|
|
1437
|
+
l2_content: text,
|
|
1438
|
+
tier: "core",
|
|
1439
|
+
access_count: 0,
|
|
1440
|
+
confidence: .85,
|
|
1441
|
+
last_accessed_at: args.at,
|
|
1442
|
+
asserted_at: args.at,
|
|
1443
|
+
valid_from: args.at,
|
|
1444
|
+
source_session: args.source.sessionKey,
|
|
1445
|
+
state: "confirmed",
|
|
1446
|
+
source: args.source.source ?? "ambient-learning",
|
|
1447
|
+
injected_count: 0,
|
|
1448
|
+
bad_recall_count: 0,
|
|
1449
|
+
suppressed_until_turn: 0,
|
|
1450
|
+
...args.metadataPatch,
|
|
1451
|
+
...writerAgentId ? { writer_agent_id: writerAgentId } : {},
|
|
1452
|
+
section_name: args.sectionName,
|
|
1453
|
+
...args.supersedes ? { supersedes: args.supersedes } : {},
|
|
1454
|
+
...args.topic ? { topic: args.topic } : {},
|
|
1455
|
+
...args.evidence ? { evidence: args.evidence } : {},
|
|
1456
|
+
...args.sectionName === "preferences.general" && !args.topic ? { "profile.preference.missing_topic": true } : {}
|
|
1457
|
+
});
|
|
1458
|
+
return {
|
|
1459
|
+
text: buildIndexedText(abstract, text),
|
|
1460
|
+
category: "profile",
|
|
1461
|
+
projectId: args.scope,
|
|
1462
|
+
importance: .85,
|
|
1463
|
+
timestamp: args.at,
|
|
1464
|
+
metadata: stringifyInsightMetadata(metadata),
|
|
1465
|
+
trusted: true
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
async function writeEpisodicSafetyNet(params, reason) {
|
|
1469
|
+
const writerAgentId = resolveHookAgentId(params.source.agentId, params.source.sessionKey).agentId;
|
|
1470
|
+
const assertion = params.newAssertion.trim();
|
|
1471
|
+
const idempotencyKey = profileSafetyNetIdempotencyKey(params);
|
|
1472
|
+
const metadata = buildInsightMetadata({
|
|
1473
|
+
text: assertion,
|
|
1474
|
+
category: "episodic",
|
|
1475
|
+
timestamp: params.at
|
|
1476
|
+
}, {
|
|
1477
|
+
l0_abstract: assertion,
|
|
1478
|
+
l1_overview: params.evidence?.trim() || `- ${assertion}`,
|
|
1479
|
+
l2_content: assertion,
|
|
1480
|
+
tier: "working",
|
|
1481
|
+
access_count: 0,
|
|
1482
|
+
confidence: .7,
|
|
1483
|
+
last_accessed_at: params.at,
|
|
1484
|
+
asserted_at: params.at,
|
|
1485
|
+
valid_from: params.at,
|
|
1486
|
+
...params.source.sessionKey ? { source_session: params.source.sessionKey } : {},
|
|
1487
|
+
...writerAgentId ? { writer_agent_id: writerAgentId } : {},
|
|
1488
|
+
...params.source.messageId ? { source_message_id: params.source.messageId } : {},
|
|
1489
|
+
state: "confirmed",
|
|
1490
|
+
source: params.source.source ?? "ambient-learning",
|
|
1491
|
+
injected_count: 0,
|
|
1492
|
+
bad_recall_count: 0,
|
|
1493
|
+
suppressed_until_turn: 0,
|
|
1494
|
+
idempotency_key: idempotencyKey
|
|
1495
|
+
});
|
|
1496
|
+
const stored = await params.store.store({
|
|
1497
|
+
text: assertion,
|
|
1498
|
+
category: "episodic",
|
|
1499
|
+
projectId: params.scope,
|
|
1500
|
+
importance: .7,
|
|
1501
|
+
timestamp: params.at,
|
|
1502
|
+
metadata: stringifyInsightMetadata(metadata)
|
|
1503
|
+
});
|
|
1504
|
+
log.warn("profile candidate routed to episodic safety net", {
|
|
1505
|
+
reason,
|
|
1506
|
+
rowId: stored.id
|
|
1507
|
+
}, {
|
|
1508
|
+
event_name: "sno_station_mem.profile-section-writer.profile.candidate.routed.to.episodic.safety.net",
|
|
1509
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
1510
|
+
function: "writeEpisodicSafetyNet",
|
|
1511
|
+
site_id: "profile-section-writer.writeEpisodicSafetyNet.ae69446388"
|
|
1512
|
+
});
|
|
1513
|
+
return {
|
|
1514
|
+
rowId: stored.id,
|
|
1515
|
+
replayed: stored.storeWriteOutcome === "existing"
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
function profileSafetyNetIdempotencyKey(params) {
|
|
1519
|
+
return `profile_safety_net_${stableShortHash(JSON.stringify([
|
|
1520
|
+
tombstoneReplayIdentity(params),
|
|
1521
|
+
canonicalizeProfileSectionName(params.sectionName),
|
|
1522
|
+
normalizeExactAssertionText(params.newAssertion)
|
|
1523
|
+
]))}`;
|
|
1524
|
+
}
|
|
1525
|
+
function tombstoneEventStoreInput(params, closedProfileRowId) {
|
|
1526
|
+
const writerAgentId = resolveHookAgentId(params.source.agentId, params.source.sessionKey).agentId;
|
|
1527
|
+
const assertion = params.newAssertion.trim();
|
|
1528
|
+
const metadata = buildInsightMetadata({
|
|
1529
|
+
text: assertion,
|
|
1530
|
+
category: "episodic",
|
|
1531
|
+
timestamp: params.at
|
|
1532
|
+
}, {
|
|
1533
|
+
l0_abstract: assertion,
|
|
1534
|
+
l1_overview: `- ${assertion}`,
|
|
1535
|
+
l2_content: assertion,
|
|
1536
|
+
tier: "working",
|
|
1537
|
+
access_count: 0,
|
|
1538
|
+
confidence: .8,
|
|
1539
|
+
last_accessed_at: params.at,
|
|
1540
|
+
asserted_at: params.at,
|
|
1541
|
+
valid_from: params.at,
|
|
1542
|
+
...params.source.sessionKey ? { source_session: params.source.sessionKey } : {},
|
|
1543
|
+
...writerAgentId ? { writer_agent_id: writerAgentId } : {},
|
|
1544
|
+
...params.source.messageId ? { source_message_id: params.source.messageId } : {},
|
|
1545
|
+
state: "confirmed",
|
|
1546
|
+
source: params.source.source ?? "ambient-learning",
|
|
1547
|
+
injected_count: 0,
|
|
1548
|
+
bad_recall_count: 0,
|
|
1549
|
+
suppressed_until_turn: 0,
|
|
1550
|
+
section_name: params.sectionName,
|
|
1551
|
+
closed_profile_row_id: closedProfileRowId,
|
|
1552
|
+
profile_tombstone_verdict: "tombstone",
|
|
1553
|
+
idempotency_key: tombstoneEventIdempotencyKey(params),
|
|
1554
|
+
content_identity_key: tombstoneEventIdempotencyKey(params)
|
|
1555
|
+
});
|
|
1556
|
+
return {
|
|
1557
|
+
text: assertion,
|
|
1558
|
+
category: "episodic",
|
|
1559
|
+
projectId: params.scope,
|
|
1560
|
+
importance: .7,
|
|
1561
|
+
timestamp: params.at,
|
|
1562
|
+
metadata: stringifyInsightMetadata(metadata)
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
function tombstoneMarkerIdempotencyKey(params) {
|
|
1566
|
+
return `profile_tombstone_marker_${stableShortHash([
|
|
1567
|
+
params.scope,
|
|
1568
|
+
params.sectionName,
|
|
1569
|
+
tombstoneReplayIdentity(params)
|
|
1570
|
+
].join("\0"))}`;
|
|
1571
|
+
}
|
|
1572
|
+
function tombstoneEventIdempotencyKey(params) {
|
|
1573
|
+
return `profile_tombstone_event_${stableShortHash([
|
|
1574
|
+
params.scope,
|
|
1575
|
+
params.sectionName,
|
|
1576
|
+
tombstoneReplayIdentity(params)
|
|
1577
|
+
].join("\0"))}`;
|
|
1578
|
+
}
|
|
1579
|
+
async function tombstoneReplayResult(params, factKey) {
|
|
1580
|
+
if (!params.store.findByExtractionIdempotencyKey(params.scope, tombstoneEventIdempotencyKey(params))) return;
|
|
1581
|
+
const current = params.store.getByFactKey(params.scope, factKey);
|
|
1582
|
+
if (!current) return void 0;
|
|
1583
|
+
await resumeRetireByNamePending(current, params);
|
|
1584
|
+
return {
|
|
1585
|
+
outcome: "no-op",
|
|
1586
|
+
rowId: current.id
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function tombstoneReplayIdentity(params) {
|
|
1590
|
+
const messageId = params.source.messageId?.trim();
|
|
1591
|
+
if (messageId) return `message:${messageId}`;
|
|
1592
|
+
return [
|
|
1593
|
+
"fallback",
|
|
1594
|
+
params.source.sessionKey?.trim() ?? "",
|
|
1595
|
+
String(params.at),
|
|
1596
|
+
normalizeExactAssertionText(params.newAssertion)
|
|
1597
|
+
].join("\0");
|
|
1598
|
+
}
|
|
1599
|
+
function closeProfileMetadata(row, supersededBy, at) {
|
|
1600
|
+
const metadata = parseInsightMetadata(row.metadata, row);
|
|
1601
|
+
const invalidatedAt = Math.max(at, metadata.valid_from ?? at);
|
|
1602
|
+
return stringifyInsightMetadata(buildInsightMetadata(row, {
|
|
1603
|
+
...metadata,
|
|
1604
|
+
invalidated_at: invalidatedAt,
|
|
1605
|
+
superseded_by: supersededBy
|
|
1606
|
+
}));
|
|
1607
|
+
}
|
|
1608
|
+
const retireByNameWorkItemSchema = z.object({
|
|
1609
|
+
positions: z.array(z.string()).min(1),
|
|
1610
|
+
eventTime: z.number(),
|
|
1611
|
+
triggeringEventIdentity: z.string(),
|
|
1612
|
+
triggeringSessionIdentity: z.string().optional(),
|
|
1613
|
+
excludedRowIds: z.array(z.string()),
|
|
1614
|
+
lifecycleClassification: z.object({
|
|
1615
|
+
incomingAssertion: z.string(),
|
|
1616
|
+
liveSiblingSectionNames: z.array(z.string()),
|
|
1617
|
+
attempts: z.number().int().nonnegative().optional(),
|
|
1618
|
+
lastFailure: z.enum(["malformed-answer", "transport"]).optional()
|
|
1619
|
+
}).strict().optional()
|
|
1620
|
+
}).strict();
|
|
1621
|
+
const retireByNamePendingSchema = z.object({ workItems: z.array(retireByNameWorkItemSchema).min(1) }).strict();
|
|
1622
|
+
/**
|
|
1623
|
+
* Collect the pending queues of every row this write is folding into one. A conflict scan can
|
|
1624
|
+
* close a live row that carries unfinished retirement work, and only the current row is ever
|
|
1625
|
+
* resumed, so anything left behind on the closed row is unreachable for good.
|
|
1626
|
+
*/
|
|
1627
|
+
function mergeRetireByNamePending(sources) {
|
|
1628
|
+
const workItems = sources.flatMap((source) => source?.workItems ?? []);
|
|
1629
|
+
return workItems.length === 0 ? void 0 : { workItems };
|
|
1630
|
+
}
|
|
1631
|
+
function appendRetireByNameWork(pending, positions, params, excludedRowIds, fallbackTriggeringEventIdentity, lifecycleClassification) {
|
|
1632
|
+
const workItems = pending?.workItems ?? [];
|
|
1633
|
+
const triggeringEventIdentity = params.source.messageId ?? params.source.sessionKey ?? fallbackTriggeringEventIdentity;
|
|
1634
|
+
const carriedPositions = new Set(workItems.filter((item) => item.eventTime === params.at && item.triggeringEventIdentity === triggeringEventIdentity && item.triggeringSessionIdentity === params.source.sessionKey).flatMap((item) => item.positions.map(normalizeExactAssertionText)));
|
|
1635
|
+
const newPositions = [...new Set(positions)].filter((position) => !carriedPositions.has(normalizeExactAssertionText(position)));
|
|
1636
|
+
if (newPositions.length === 0) return workItems.length === 0 ? void 0 : { workItems: capRetireByNameWorkItems(workItems, params) };
|
|
1637
|
+
return { workItems: capRetireByNameWorkItems([...workItems, {
|
|
1638
|
+
positions: newPositions,
|
|
1639
|
+
eventTime: params.at,
|
|
1640
|
+
triggeringEventIdentity,
|
|
1641
|
+
...params.source.sessionKey ? { triggeringSessionIdentity: params.source.sessionKey } : {},
|
|
1642
|
+
excludedRowIds,
|
|
1643
|
+
...lifecycleClassification ? { lifecycleClassification } : {}
|
|
1644
|
+
}], params) };
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* The last stop against an unbounded marker. Only an ANSWER exhausts the per-item bound, so a
|
|
1648
|
+
* route that is ON and only ever times out defers each item forever while new ones keep arriving
|
|
1649
|
+
* per event. Past the cap the OLDEST item goes, and it goes LOUDLY: a permanently broken route
|
|
1650
|
+
* then costs one warning per event instead of a metadata blob that grows until writes crawl.
|
|
1651
|
+
* 16 is the recheck cap, and the largest general rewrite observed on the 2026-09-01 store retired
|
|
1652
|
+
* 9 clauses, so an ordinary day never reaches it. Owner ruling via CTS, 2026-09-02.
|
|
1653
|
+
*/
|
|
1654
|
+
function capRetireByNameWorkItems(workItems, params) {
|
|
1655
|
+
if (workItems.length <= RETIRE_BY_NAME_MAX_PENDING_WORK_ITEMS) return [...workItems];
|
|
1656
|
+
const dropped = workItems.slice(0, workItems.length - RETIRE_BY_NAME_MAX_PENDING_WORK_ITEMS);
|
|
1657
|
+
for (const item of dropped) log.warn("retire-by-name work dropped at the pending cap", {
|
|
1658
|
+
scope: params.scope,
|
|
1659
|
+
section: params.sectionName,
|
|
1660
|
+
factKey: profileFactKey(params.sectionName),
|
|
1661
|
+
position: item.positions[0],
|
|
1662
|
+
positions: item.positions.length,
|
|
1663
|
+
failure: item.lifecycleClassification?.lastFailure,
|
|
1664
|
+
attempts: item.lifecycleClassification?.attempts
|
|
1665
|
+
}, {
|
|
1666
|
+
event_name: "sno_station_mem.profile-section-writer.retire.by.name.work.dropped.at.the.pending.cap",
|
|
1667
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
1668
|
+
function: "capRetireByNameWorkItems",
|
|
1669
|
+
site_id: "profile-section-writer.capRetireByNameWorkItems.3c47653418"
|
|
1670
|
+
});
|
|
1671
|
+
return workItems.slice(dropped.length);
|
|
1672
|
+
}
|
|
1673
|
+
async function writeProfileRow(args) {
|
|
1674
|
+
const plan = await planProfileConflicts(args.params, args.content, [args.existing?.id].filter((id) => id !== void 0));
|
|
1675
|
+
if (plan.blockedBy) {
|
|
1676
|
+
const safetyNet = await writeEpisodicSafetyNet(args.params, "superseded-by-existing");
|
|
1677
|
+
const blockingSection = parseInsightMetadata(plan.blockedBy.metadata, plan.blockedBy).section_name;
|
|
1678
|
+
log.warn("profile conflict blocked candidate routed to episodic", {
|
|
1679
|
+
candidateRowId: safetyNet.rowId,
|
|
1680
|
+
blockingRowId: plan.blockedBy.id,
|
|
1681
|
+
incomingSection: args.params.sectionName,
|
|
1682
|
+
blockingSection,
|
|
1683
|
+
verdict: "replacement"
|
|
1684
|
+
}, {
|
|
1685
|
+
event_name: "sno_station_mem.profile-section-writer.profile.conflict.blocked.candidate.routed.to.episodic",
|
|
1686
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
1687
|
+
function: "writeProfileRow",
|
|
1688
|
+
site_id: "profile-section-writer.writeProfileRow.3e3e26a255"
|
|
1689
|
+
});
|
|
1690
|
+
return {
|
|
1691
|
+
outcome: safetyNet.replayed ? "no-op" : "appended",
|
|
1692
|
+
rowId: safetyNet.rowId
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
const closeRows = uniqueRows([args.existing, ...plan.closes].filter((row) => row !== void 0));
|
|
1696
|
+
const retainedClauses = new Set(splitExactClauses(args.content).map(normalizeExactAssertionText));
|
|
1697
|
+
const conflictRetiredPositions = plan.closes.flatMap((row) => {
|
|
1698
|
+
return splitExactClauses(parseInsightMetadata(row.metadata, row).l2_content ?? row.text).filter((clause) => !retainedClauses.has(normalizeExactAssertionText(clause)));
|
|
1699
|
+
});
|
|
1700
|
+
const conflictLifecycleRetirement = conflictRetiredPositions.length === 0 ? {
|
|
1701
|
+
kind: "classified",
|
|
1702
|
+
positions: []
|
|
1703
|
+
} : await lifecycleRetireByNamePositions(args.params, await listLiveSpecificSiblingSections(args.params), conflictRetiredPositions);
|
|
1704
|
+
const judgmentHasNoModel = profileMergeCallHasNoModel(args.params, "P6");
|
|
1705
|
+
const lifecycleHasNoModel = profileMergeCallHasNoModel(args.params, "P2");
|
|
1706
|
+
const modelOnlyPositions = judgmentHasNoModel ? [] : [...args.retiredPositions ?? [], ...conflictLifecycleRetirement.kind === "classified" ? conflictLifecycleRetirement.positions : []];
|
|
1707
|
+
let pending = mergeRetireByNamePending([...args.existing ? [args.existing] : [], ...closeRows].filter((row, index, rows) => rows.findIndex((other) => other.id === row.id) === index).map(readRetireByNamePending));
|
|
1708
|
+
pending = appendRetireByNameWork(pending, modelOnlyPositions, args.params, closeRows.map((row) => row.id), "profile-update");
|
|
1709
|
+
const deferredRetirements = (judgmentHasNoModel || lifecycleHasNoModel ? [] : [args.deferredLifecycleRetirement, conflictLifecycleRetirement.kind === "deferred" ? conflictLifecycleRetirement.work : void 0]).filter((work) => work !== void 0);
|
|
1710
|
+
for (const deferred of deferredRetirements) pending = appendRetireByNameWork(pending, deferred.positions, args.params, closeRows.map((row) => row.id), "profile-update", deferred.context);
|
|
1711
|
+
const createInput = pending === void 0 ? args.input : {
|
|
1712
|
+
...args.input,
|
|
1713
|
+
metadata: addRetireByNamePending(args.input.metadata, pending)
|
|
1714
|
+
};
|
|
1715
|
+
const created = await args.params.store.supersede({
|
|
1716
|
+
create: createInput,
|
|
1717
|
+
closes: closeRows.map((row) => ({
|
|
1718
|
+
id: row.id,
|
|
1719
|
+
buildMetadata: (createdId) => closeProfileMetadata(row, createdId, args.params.at)
|
|
1720
|
+
})),
|
|
1721
|
+
activeFactGuard: {
|
|
1722
|
+
factKey: profileFactKey(args.params.sectionName),
|
|
1723
|
+
expectedId: args.existing?.id ?? null
|
|
1724
|
+
},
|
|
1725
|
+
reviveInvalidatedExisting: true,
|
|
1726
|
+
...closeRows.length === 0 ? {} : { profileRecovery: {
|
|
1727
|
+
mutationAttemptId: currentMutationAttemptId(),
|
|
1728
|
+
sectionName: args.params.sectionName,
|
|
1729
|
+
removedAtMs: args.params.at
|
|
1730
|
+
} }
|
|
1731
|
+
});
|
|
1732
|
+
if (pending) await completeRetireByNamePending(created, args.params, pending);
|
|
1733
|
+
return {
|
|
1734
|
+
outcome: args.outcome,
|
|
1735
|
+
rowId: created.id
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function addRetireByNamePending(metadata, pending) {
|
|
1739
|
+
const parsed = JSON.parse(metadata ?? "{}");
|
|
1740
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("profile-section-writer: profile metadata must be an object");
|
|
1741
|
+
return JSON.stringify({
|
|
1742
|
+
...parsed,
|
|
1743
|
+
retire_by_name_pending: pending
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
function readRetireByNamePending(entry) {
|
|
1747
|
+
const value = parseInsightMetadata(entry.metadata, entry).retire_by_name_pending;
|
|
1748
|
+
const parsed = retireByNamePendingSchema.safeParse(value);
|
|
1749
|
+
return parsed.success ? parsed.data : void 0;
|
|
1750
|
+
}
|
|
1751
|
+
function classifiedRetireByNameWorkItem(workItem, positions) {
|
|
1752
|
+
return {
|
|
1753
|
+
positions,
|
|
1754
|
+
eventTime: workItem.eventTime,
|
|
1755
|
+
triggeringEventIdentity: workItem.triggeringEventIdentity,
|
|
1756
|
+
...workItem.triggeringSessionIdentity ? { triggeringSessionIdentity: workItem.triggeringSessionIdentity } : {},
|
|
1757
|
+
excludedRowIds: workItem.excludedRowIds
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
async function completeRetireByNamePending(current, params, pending) {
|
|
1761
|
+
if (profileMergeCallHasNoModel(params, "P6")) return;
|
|
1762
|
+
const lifecycleHasNoModel = profileMergeCallHasNoModel(params, "P2");
|
|
1763
|
+
const judgmentBudget = params.retireByNameBudget ?? createRetireByNameRunBudget(params.timeoutMs);
|
|
1764
|
+
const deadlineMs = judgmentBudget.deadlineMs;
|
|
1765
|
+
let remainingWorkItems = [];
|
|
1766
|
+
workLoop: for (let workIndex = 0; workIndex < pending.workItems.length; workIndex += 1) {
|
|
1767
|
+
const workItem = pending.workItems[workIndex];
|
|
1768
|
+
if (!workItem) continue;
|
|
1769
|
+
let activeWorkItem = workItem;
|
|
1770
|
+
if (workItem.lifecycleClassification) {
|
|
1771
|
+
if (lifecycleHasNoModel) {
|
|
1772
|
+
remainingWorkItems.push(workItem);
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
if (Date.now() >= deadlineMs) {
|
|
1776
|
+
remainingWorkItems = [...remainingWorkItems, ...pending.workItems.slice(workIndex)];
|
|
1777
|
+
break;
|
|
1778
|
+
}
|
|
1779
|
+
const remainingMs = Math.max(1, deadlineMs - Date.now());
|
|
1780
|
+
const classification = await lifecycleRetireByNamePositions({
|
|
1781
|
+
...params,
|
|
1782
|
+
timeoutMs: Math.min(params.timeoutMs ?? remainingMs, remainingMs)
|
|
1783
|
+
}, workItem.lifecycleClassification.liveSiblingSectionNames, workItem.positions, workItem.lifecycleClassification.incomingAssertion, workItem.lifecycleClassification.attempts ?? 0);
|
|
1784
|
+
if (classification.kind === "deferred") {
|
|
1785
|
+
remainingWorkItems = [
|
|
1786
|
+
...remainingWorkItems,
|
|
1787
|
+
{
|
|
1788
|
+
...workItem,
|
|
1789
|
+
lifecycleClassification: classification.work.context
|
|
1790
|
+
},
|
|
1791
|
+
...pending.workItems.slice(workIndex + 1)
|
|
1792
|
+
];
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
if (classification.positions.length === 0) continue;
|
|
1796
|
+
activeWorkItem = classifiedRetireByNameWorkItem(workItem, classification.positions);
|
|
1797
|
+
}
|
|
1798
|
+
for (let positionIndex = 0; positionIndex < activeWorkItem.positions.length; positionIndex += 1) {
|
|
1799
|
+
const retiredPosition = activeWorkItem.positions[positionIndex];
|
|
1800
|
+
if (retiredPosition === void 0 || judgmentBudget.remaining <= 0 || Date.now() >= deadlineMs) {
|
|
1801
|
+
remainingWorkItems = [
|
|
1802
|
+
...remainingWorkItems,
|
|
1803
|
+
{
|
|
1804
|
+
...activeWorkItem,
|
|
1805
|
+
positions: activeWorkItem.positions.slice(positionIndex)
|
|
1806
|
+
},
|
|
1807
|
+
...pending.workItems.slice(workIndex + 1)
|
|
1808
|
+
];
|
|
1809
|
+
break workLoop;
|
|
1810
|
+
}
|
|
1811
|
+
const judgedCandidateIds = /* @__PURE__ */ new Set();
|
|
1812
|
+
if (!(await retireRowsByName({
|
|
1813
|
+
store: params.store,
|
|
1814
|
+
llm: params.llm,
|
|
1815
|
+
routing: params.routing,
|
|
1816
|
+
scope: params.scope,
|
|
1817
|
+
retiredPosition,
|
|
1818
|
+
currentPositionId: current.id,
|
|
1819
|
+
triggeringEventIdentity: activeWorkItem.triggeringEventIdentity,
|
|
1820
|
+
...activeWorkItem.triggeringSessionIdentity ? { triggeringSessionIdentity: activeWorkItem.triggeringSessionIdentity } : {},
|
|
1821
|
+
eventTime: activeWorkItem.eventTime,
|
|
1822
|
+
excludedRowIds: activeWorkItem.excludedRowIds,
|
|
1823
|
+
...params.timeoutMs === void 0 ? {} : { timeoutMs: params.timeoutMs },
|
|
1824
|
+
...params.signal === void 0 ? {} : { signal: params.signal },
|
|
1825
|
+
judgmentBudget,
|
|
1826
|
+
judgedCandidateIds,
|
|
1827
|
+
deadlineMs
|
|
1828
|
+
})).completed) {
|
|
1829
|
+
const laterPositions = activeWorkItem.positions.slice(positionIndex + 1);
|
|
1830
|
+
remainingWorkItems = [
|
|
1831
|
+
...remainingWorkItems,
|
|
1832
|
+
{
|
|
1833
|
+
...activeWorkItem,
|
|
1834
|
+
positions: [retiredPosition],
|
|
1835
|
+
excludedRowIds: [.../* @__PURE__ */ new Set([...activeWorkItem.excludedRowIds, ...judgedCandidateIds])]
|
|
1836
|
+
},
|
|
1837
|
+
...laterPositions.length === 0 ? [] : [{
|
|
1838
|
+
...activeWorkItem,
|
|
1839
|
+
positions: laterPositions
|
|
1840
|
+
}],
|
|
1841
|
+
...pending.workItems.slice(workIndex + 1)
|
|
1842
|
+
];
|
|
1843
|
+
break workLoop;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
await writeRetireByNamePending(current, params.store, remainingWorkItems.length === 0 ? void 0 : { workItems: remainingWorkItems });
|
|
1848
|
+
}
|
|
1849
|
+
async function writeRetireByNamePending(current, store, pending) {
|
|
1850
|
+
let latest = current;
|
|
1851
|
+
for (let attempt = 0; attempt < CLEAR_RETIRE_BY_NAME_PENDING_ATTEMPTS; attempt += 1) {
|
|
1852
|
+
const parsed = JSON.parse(latest.metadata);
|
|
1853
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("profile-section-writer: profile metadata must be an object");
|
|
1854
|
+
const next = { ...parsed };
|
|
1855
|
+
if (pending) next["retire_by_name_pending"] = pending;
|
|
1856
|
+
else delete next["retire_by_name_pending"];
|
|
1857
|
+
try {
|
|
1858
|
+
if (!await store.update(latest.id, {
|
|
1859
|
+
writerAuthority: "profile-writer",
|
|
1860
|
+
metadata: JSON.stringify(next),
|
|
1861
|
+
expectedContentHash: latest.contentHash,
|
|
1862
|
+
expectedMetadata: latest.metadata
|
|
1863
|
+
})) throw new Error("profile-section-writer: pending profile row is missing");
|
|
1864
|
+
return;
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
if (!(error instanceof StorageError) || error.message !== `Cannot update changed memory '${latest.id}'`) throw error;
|
|
1867
|
+
}
|
|
1868
|
+
const refreshed = store.getById(latest.id);
|
|
1869
|
+
if (!refreshed) throw new Error("profile-section-writer: pending profile row is missing");
|
|
1870
|
+
if (!readRetireByNamePending(refreshed)) return;
|
|
1871
|
+
latest = refreshed;
|
|
1872
|
+
}
|
|
1873
|
+
throw new Error("profile-section-writer: pending profile row kept changing");
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Move a live profile row's `valid_from` forward to a repeat of the assertion it already
|
|
1877
|
+
* holds, so the out-of-order guard measures staleness from the LAST confirmation.
|
|
1878
|
+
*
|
|
1879
|
+
* Only forward, and only for a row that already carries the field: writing one where there
|
|
1880
|
+
* was none would arm a guard that has never been armed for that row.
|
|
1881
|
+
*
|
|
1882
|
+
* Returns the row as this call leaves it, so the caller's next step reads the metadata that is
|
|
1883
|
+
* now stored instead of the snapshot it came in with.
|
|
1884
|
+
*/
|
|
1885
|
+
async function advanceProfileConfirmation(current, currentValidFrom, params) {
|
|
1886
|
+
if (typeof currentValidFrom !== "number" || params.at <= currentValidFrom) return current;
|
|
1887
|
+
let latest = current;
|
|
1888
|
+
for (let attempt = 0; attempt < ADVANCE_PROFILE_CONFIRMATION_ATTEMPTS; attempt += 1) {
|
|
1889
|
+
const parsed = JSON.parse(latest.metadata);
|
|
1890
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("profile-section-writer: profile metadata must be an object");
|
|
1891
|
+
const next = {
|
|
1892
|
+
...parsed,
|
|
1893
|
+
valid_from: params.at
|
|
1894
|
+
};
|
|
1895
|
+
try {
|
|
1896
|
+
const updated = await params.store.update(latest.id, {
|
|
1897
|
+
writerAuthority: "profile-writer",
|
|
1898
|
+
metadata: JSON.stringify(next),
|
|
1899
|
+
expectedContentHash: latest.contentHash,
|
|
1900
|
+
expectedMetadata: latest.metadata
|
|
1901
|
+
});
|
|
1902
|
+
if (!updated) throw new Error("profile-section-writer: confirmed profile row is missing");
|
|
1903
|
+
return updated;
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
if (!(error instanceof StorageError) || error.message !== `Cannot update changed memory '${latest.id}'`) throw error;
|
|
1906
|
+
}
|
|
1907
|
+
const refreshed = params.store.getById(latest.id);
|
|
1908
|
+
if (!refreshed) throw new Error("profile-section-writer: confirmed profile row is missing");
|
|
1909
|
+
const refreshedMetadata = parseInsightMetadata(refreshed.metadata, refreshed);
|
|
1910
|
+
if (refreshedMetadata.invalidated_at !== void 0) return refreshed;
|
|
1911
|
+
const refreshedValidFrom = refreshedMetadata.valid_from;
|
|
1912
|
+
if (typeof refreshedValidFrom === "number" && refreshedValidFrom >= params.at) return refreshed;
|
|
1913
|
+
if (refreshed.contentHash !== latest.contentHash) return refreshed;
|
|
1914
|
+
latest = refreshed;
|
|
1915
|
+
}
|
|
1916
|
+
throw new Error("profile-section-writer: confirmed profile row kept changing");
|
|
1917
|
+
}
|
|
1918
|
+
async function resumeRetireByNamePending(current, params) {
|
|
1919
|
+
const pending = readRetireByNamePending(current);
|
|
1920
|
+
if (pending) await completeRetireByNamePending(current, params, pending);
|
|
1921
|
+
}
|
|
1922
|
+
async function listLiveSpecificSiblingSections(params) {
|
|
1923
|
+
return (await listLiveSpecificSiblings(params)).map((sibling) => sibling.name);
|
|
1924
|
+
}
|
|
1925
|
+
async function listLiveSpecificSiblings(params) {
|
|
1926
|
+
if (params.sectionName !== "preferences.general") return [];
|
|
1927
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1928
|
+
for (let offset = 0;; offset += MAX_LIST_LIMIT) {
|
|
1929
|
+
const page = await params.store.list({
|
|
1930
|
+
projectId: params.scope,
|
|
1931
|
+
category: "profile",
|
|
1932
|
+
limit: MAX_LIST_LIMIT,
|
|
1933
|
+
offset
|
|
1934
|
+
});
|
|
1935
|
+
for (const row of page) {
|
|
1936
|
+
const metadata = parseInsightMetadata(row.metadata, row);
|
|
1937
|
+
const section = metadata.section_name;
|
|
1938
|
+
const content = metadata.l2_content ?? row.text;
|
|
1939
|
+
if (metadata.invalidated_at !== void 0 || typeof section !== "string" || normalizeExactAssertionText(content) === normalizeExactAssertionText(retiredProfileSectionMarker(section))) continue;
|
|
1940
|
+
if (section.startsWith("preferences.") && section !== "preferences.general") sections.set(section, content);
|
|
1941
|
+
}
|
|
1942
|
+
if (page.length < 2048) break;
|
|
1943
|
+
}
|
|
1944
|
+
return [...sections].sort(([left], [right]) => left.localeCompare(right)).map(([name, content]) => ({
|
|
1945
|
+
name,
|
|
1946
|
+
content
|
|
1947
|
+
}));
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* True when a profile-merge call has no model behind it: no client at all, or a route that
|
|
1951
|
+
* resolves OFF. Both must be decided BEFORE the call, because an OFF route returns null rather
|
|
1952
|
+
* than throwing, which reads downstream as a malformed answer and defers the work forever. That
|
|
1953
|
+
* is what the 2026-09-01 run did all night: the lifecycle label was unregistered, so every
|
|
1954
|
+
* general-section classification deferred and retire-by-name never had a position to judge.
|
|
1955
|
+
*/
|
|
1956
|
+
function profileMergeCallHasNoModel(params, callId) {
|
|
1957
|
+
if (!params.llm) return true;
|
|
1958
|
+
if (!params.routing) return false;
|
|
1959
|
+
return "off" in resolveLlmRoute({
|
|
1960
|
+
callId,
|
|
1961
|
+
config: params.routing
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* True when conflict adjudication resolves OFF. This is Local First (or an
|
|
1966
|
+
* invalid/retired route); live agent and Sno routes adjudicate inline.
|
|
1967
|
+
*/
|
|
1968
|
+
function conflictAdjudicationRoutedOff(params) {
|
|
1969
|
+
if (!params.routing) return false;
|
|
1970
|
+
return "off" in resolveLlmRoute({
|
|
1971
|
+
callId: "P1",
|
|
1972
|
+
config: params.routing
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
async function planProfileConflicts(params, content, excludedIds) {
|
|
1976
|
+
if (conflictAdjudicationRoutedOff(params)) {
|
|
1977
|
+
log.debug("profile conflict scan routed off; conflicting rows both persist", { mode: params.routing?.mode }, {
|
|
1978
|
+
event_name: "sno_station_mem.profile-section-writer.profile.conflict.scan.routed.off.conflicting.rows.both.persist",
|
|
1979
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
1980
|
+
function: "planProfileConflicts",
|
|
1981
|
+
site_id: "profile-section-writer.planProfileConflicts.e5bcea94ce"
|
|
1982
|
+
});
|
|
1983
|
+
return { closes: [] };
|
|
1984
|
+
}
|
|
1985
|
+
try {
|
|
1986
|
+
const vector = await params.store.embedder.embed(content);
|
|
1987
|
+
const matches = await params.store.searchSemantic(vector, {
|
|
1988
|
+
projectIdFilter: [params.scope],
|
|
1989
|
+
category: "profile",
|
|
1990
|
+
excludeInvalidatedBefore: Number.MAX_SAFE_INTEGER,
|
|
1991
|
+
limit: PROFILE_CONFLICT_TOP_K + excludedIds.length
|
|
1992
|
+
});
|
|
1993
|
+
const excluded = new Set(excludedIds);
|
|
1994
|
+
const candidates = matches.map((match) => match.entry).filter((entry) => !excluded.has(entry.id)).slice(0, PROFILE_CONFLICT_TOP_K);
|
|
1995
|
+
const closes = [];
|
|
1996
|
+
for (const existing of candidates) {
|
|
1997
|
+
let existingMetadata;
|
|
1998
|
+
try {
|
|
1999
|
+
existingMetadata = parseInsightMetadata(existing.metadata, existing);
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
logProfileConflictFenceRejection(params, existing, {
|
|
2002
|
+
existingSection: null,
|
|
2003
|
+
existingFactKey: null,
|
|
2004
|
+
existingSessionKey: null,
|
|
2005
|
+
reason: "metadata-invalid",
|
|
2006
|
+
error
|
|
2007
|
+
});
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
const existingSection = existingMetadata.kind === "profile" ? existingMetadata.section_name : null;
|
|
2011
|
+
const existingContent = existingMetadata.l2_content ?? existing.text;
|
|
2012
|
+
if (typeof existingSection === "string" && normalizeExactAssertionText(existingContent) === normalizeExactAssertionText(retiredProfileSectionMarker(existingSection))) continue;
|
|
2013
|
+
if (existingSection !== params.sectionName) {
|
|
2014
|
+
logProfileConflictFenceRejection(params, existing, {
|
|
2015
|
+
existingSection,
|
|
2016
|
+
existingFactKey: existingMetadata.fact_key ?? null,
|
|
2017
|
+
existingSessionKey: existingMetadata.source_session ?? null,
|
|
2018
|
+
reason: "section-mismatch"
|
|
2019
|
+
});
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
const pair = orderAdapterAPair(adapterAViewFromEntry(existing), prospectiveProfileView(content, params.at));
|
|
2023
|
+
const verdict = await adjudicateSafely(pair, params, {
|
|
2024
|
+
purpose: "cross-row-profile-conflict",
|
|
2025
|
+
existingId: existing.id
|
|
2026
|
+
});
|
|
2027
|
+
if (verdict === "uncertain") {
|
|
2028
|
+
log.warn("profile conflict adjudication uncertain; keeping both rows", {
|
|
2029
|
+
memory_id: existing.id,
|
|
2030
|
+
candidate_size: content.length
|
|
2031
|
+
}, {
|
|
2032
|
+
event_name: "sno_station_mem.profile-section-writer.profile.conflict.adjudication.uncertain.keeping.both.rows",
|
|
2033
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
2034
|
+
function: "planProfileConflicts",
|
|
2035
|
+
site_id: "profile-section-writer.planProfileConflicts.97b6a2e824"
|
|
2036
|
+
});
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
if (verdict !== "replacement") continue;
|
|
2040
|
+
if (pair.candidateRole === "older") return {
|
|
2041
|
+
blockedBy: existing,
|
|
2042
|
+
closes: []
|
|
2043
|
+
};
|
|
2044
|
+
closes.push(existing);
|
|
2045
|
+
}
|
|
2046
|
+
return { closes };
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
rethrowCallerCancellation(error);
|
|
2049
|
+
log.error("profile conflict scan failed; proceeding without adjudication", {
|
|
2050
|
+
error,
|
|
2051
|
+
candidate_size: content.length
|
|
2052
|
+
}, {
|
|
2053
|
+
event_name: "sno_station_mem.profile-section-writer.profile.conflict.scan.failed.proceeding.without.adjudication",
|
|
2054
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
2055
|
+
function: "planProfileConflicts",
|
|
2056
|
+
site_id: "profile-section-writer.planProfileConflicts.b4962bc7f0"
|
|
2057
|
+
});
|
|
2058
|
+
return { closes: [] };
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
function logProfileConflictFenceRejection(params, existing, details) {
|
|
2062
|
+
log.warn("profile conflict candidate rejected by section fence", {
|
|
2063
|
+
incomingSection: params.sectionName,
|
|
2064
|
+
existingSection: details.existingSection,
|
|
2065
|
+
row_id: existing.id,
|
|
2066
|
+
incomingFactKey: profileFactKey(params.sectionName),
|
|
2067
|
+
existingFactKey: details.existingFactKey,
|
|
2068
|
+
incomingMessageId: params.source.messageId ?? null,
|
|
2069
|
+
incoming_session_reference: privateLogReference(params.source.sessionKey),
|
|
2070
|
+
existing_session_reference: privateLogReference(details.existingSessionKey ?? void 0),
|
|
2071
|
+
reason: details.reason,
|
|
2072
|
+
...details.error ? { error: details.error } : {}
|
|
2073
|
+
}, {
|
|
2074
|
+
event_name: "sno_station_mem.profile-section-writer.profile.conflict.candidate.rejected.by.section.fence",
|
|
2075
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
2076
|
+
function: "logProfileConflictFenceRejection",
|
|
2077
|
+
site_id: "profile-section-writer.logProfileConflictFenceRejection.b0669b40b3"
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
async function adjudicateSafely(pair, params, context) {
|
|
2081
|
+
try {
|
|
2082
|
+
const route = params.routing ? resolveLlmRoute({
|
|
2083
|
+
callId: "P1",
|
|
2084
|
+
config: params.routing
|
|
2085
|
+
}) : void 0;
|
|
2086
|
+
if (!params.llm) return "uncertain";
|
|
2087
|
+
const prompt = route && !("off" in route) && route.tier === "snoRemMem" ? renderAdapterAPrompt(pair.older, pair.newer) : renderAdapterAChatPrompt(pair);
|
|
2088
|
+
const response = await params.llm.completeText({
|
|
2089
|
+
prompt,
|
|
2090
|
+
hostPrompt: renderAdapterAChatPrompt(pair),
|
|
2091
|
+
callId: "P1",
|
|
2092
|
+
...params.timeoutMs ? { timeoutMs: params.timeoutMs } : {},
|
|
2093
|
+
...params.signal ? { signal: params.signal } : {}
|
|
2094
|
+
});
|
|
2095
|
+
return response ? parseAdapterAChatVerdict(response) : "uncertain";
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
rethrowCallerCancellation(error);
|
|
2098
|
+
log.error("Conflict adjudicator unavailable; keeping both rows", {
|
|
2099
|
+
...context,
|
|
2100
|
+
error
|
|
2101
|
+
}, {
|
|
2102
|
+
event_name: "sno_station_mem.profile-section-writer.conflict.adjudicator.unavailable.keeping.both.rows",
|
|
2103
|
+
file: "packages/memory/src/engine/extraction/profile-section-writer.ts",
|
|
2104
|
+
function: "adjudicateSafely",
|
|
2105
|
+
site_id: "profile-section-writer.adjudicateSafely.d3f7ab0ac5"
|
|
2106
|
+
});
|
|
2107
|
+
return "uncertain";
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
function uniqueRows(rows) {
|
|
2111
|
+
return [...new Map(rows.map((row) => [row.id, row])).values()];
|
|
2112
|
+
}
|
|
2113
|
+
function stableShortHash(value) {
|
|
2114
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 20);
|
|
2115
|
+
}
|
|
2116
|
+
function fallbackProfileMerge(existingText, newAssertion) {
|
|
2117
|
+
return combineUniqueLines(existingText, newAssertion);
|
|
2118
|
+
}
|
|
2119
|
+
function splitPreferenceClauses(text) {
|
|
2120
|
+
return text.split(/\n+|(?<=[.!?])\s+/).map((item) => item.trim()).filter(Boolean);
|
|
2121
|
+
}
|
|
2122
|
+
function combineUniqueLines(...texts) {
|
|
2123
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2124
|
+
const lines = [];
|
|
2125
|
+
for (const text of texts) for (const line of splitPreferenceClauses(text)) {
|
|
2126
|
+
const key = normalizeExactAssertionText(line);
|
|
2127
|
+
if (!key || seen.has(key)) continue;
|
|
2128
|
+
seen.add(key);
|
|
2129
|
+
lines.push(line);
|
|
2130
|
+
}
|
|
2131
|
+
return lines.join("\n");
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Whether the section already states everything the incoming assertion states,
|
|
2135
|
+
* measured with the same clause identity the deterministic merge uses: if
|
|
2136
|
+
* combining them adds no clause, there is nothing to write. This is the only
|
|
2137
|
+
* condition under which skipping the write loses no memory.
|
|
2138
|
+
*/
|
|
2139
|
+
function sectionAlreadyCovers(existingText, newAssertion) {
|
|
2140
|
+
return combineUniqueLines(existingText, newAssertion) === combineUniqueLines(existingText);
|
|
2141
|
+
}
|
|
2142
|
+
function normalizeExactAssertionText(text) {
|
|
2143
|
+
return text.trim().replace(/\s+/g, " ");
|
|
2144
|
+
}
|
|
2145
|
+
function firstSentence(text) {
|
|
2146
|
+
return text.split(/(?<=[.!?])\s+/)[0]?.trim() || text;
|
|
2147
|
+
}
|
|
2148
|
+
//#endregion
|
|
2149
|
+
export { prepareDeterministicTaskLifecycleWrite as i, createRetireByNameRunBudget as n, runProfileSectionUpdate as r, PROFILE_IDENTITY_SECTION as t };
|