@graphorin/memory 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -1
- package/README.md +31 -31
- package/dist/conflict/index.d.ts +3 -1
- package/dist/conflict/index.js +1 -1
- package/dist/conflict/locale-packs/en.d.ts +13 -0
- package/dist/conflict/locale-packs/en.d.ts.map +1 -0
- package/dist/conflict/locale-packs/en.js +1 -1
- package/dist/conflict/locale-packs/en.js.map +1 -1
- package/dist/conflict/pipeline.d.ts +2 -2
- package/dist/conflict/pipeline.js +3 -3
- package/dist/conflict/pipeline.js.map +1 -1
- package/dist/conflict/stages/helpers.js +1 -1
- package/dist/conflict/stages/helpers.js.map +1 -1
- package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -1
- package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
- package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -1
- package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -1
- package/dist/conflict/types.d.ts +7 -7
- package/dist/conflict/types.js +1 -1
- package/dist/conflict/types.js.map +1 -1
- package/dist/consolidator/budget.js +16 -3
- package/dist/consolidator/budget.js.map +1 -1
- package/dist/consolidator/decay.d.ts +13 -2
- package/dist/consolidator/decay.js +17 -5
- package/dist/consolidator/decay.js.map +1 -1
- package/dist/consolidator/dlq.js +1 -1
- package/dist/consolidator/dlq.js.map +1 -1
- package/dist/consolidator/errors.js +2 -2
- package/dist/consolidator/errors.js.map +1 -1
- package/dist/consolidator/index.js +8 -6
- package/dist/consolidator/index.js.map +1 -1
- package/dist/consolidator/lock.js +1 -1
- package/dist/consolidator/lock.js.map +1 -1
- package/dist/consolidator/noise-filter.js.map +1 -1
- package/dist/consolidator/phases/deep.js +6 -2
- package/dist/consolidator/phases/deep.js.map +1 -1
- package/dist/consolidator/phases/induce.d.ts +10 -10
- package/dist/consolidator/phases/induce.js +6 -6
- package/dist/consolidator/phases/induce.js.map +1 -1
- package/dist/consolidator/phases/learned-context.js +129 -0
- package/dist/consolidator/phases/learned-context.js.map +1 -0
- package/dist/consolidator/phases/light.js +2 -1
- package/dist/consolidator/phases/light.js.map +1 -1
- package/dist/consolidator/phases/reflect.js +5 -4
- package/dist/consolidator/phases/reflect.js.map +1 -1
- package/dist/consolidator/phases/standard.js +37 -8
- package/dist/consolidator/phases/standard.js.map +1 -1
- package/dist/consolidator/reconcile.js +1 -1
- package/dist/consolidator/reconcile.js.map +1 -1
- package/dist/consolidator/runtime.d.ts +4 -4
- package/dist/consolidator/runtime.d.ts.map +1 -1
- package/dist/consolidator/runtime.js +73 -16
- package/dist/consolidator/runtime.js.map +1 -1
- package/dist/consolidator/scheduler.d.ts +5 -5
- package/dist/consolidator/scheduler.js +1 -1
- package/dist/consolidator/scheduler.js.map +1 -1
- package/dist/consolidator/triggers.js +7 -7
- package/dist/consolidator/triggers.js.map +1 -1
- package/dist/consolidator/types.d.ts +68 -20
- package/dist/consolidator/types.d.ts.map +1 -1
- package/dist/consolidator/types.js +16 -6
- package/dist/consolidator/types.js.map +1 -1
- package/dist/context-engine/annotations.d.ts +9 -9
- package/dist/context-engine/annotations.js +3 -3
- package/dist/context-engine/annotations.js.map +1 -1
- package/dist/context-engine/auto-recall.js.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.d.ts +18 -3
- package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js +26 -5
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
- package/dist/context-engine/compaction/compactor.js +19 -6
- package/dist/context-engine/compaction/compactor.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +4 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +2 -1
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts +28 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.js +71 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.js.map +1 -0
- package/dist/context-engine/compaction/hooks/types.d.ts +15 -3
- package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -1
- package/dist/context-engine/compaction/index.d.ts +1 -0
- package/dist/context-engine/compaction/index.js +2 -1
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +10 -3
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.js +42 -5
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
- package/dist/context-engine/compaction/thresholds.js.map +1 -1
- package/dist/context-engine/compaction/types.d.ts +47 -7
- package/dist/context-engine/compaction/types.d.ts.map +1 -1
- package/dist/context-engine/engine.d.ts +23 -4
- package/dist/context-engine/engine.d.ts.map +1 -1
- package/dist/context-engine/engine.js +69 -15
- package/dist/context-engine/engine.js.map +1 -1
- package/dist/context-engine/index.d.ts +1 -0
- package/dist/context-engine/index.d.ts.map +1 -1
- package/dist/context-engine/index.js +2 -1
- package/dist/context-engine/locale-packs/en.js +14 -13
- package/dist/context-engine/locale-packs/en.js.map +1 -1
- package/dist/context-engine/locale-packs/types.d.ts +10 -10
- package/dist/context-engine/locale-packs/types.d.ts.map +1 -1
- package/dist/context-engine/locale-packs/types.js.map +1 -1
- package/dist/context-engine/metadata.d.ts +1 -1
- package/dist/context-engine/metadata.js +2 -2
- package/dist/context-engine/metadata.js.map +1 -1
- package/dist/context-engine/preambles/inbound-en.d.ts +1 -1
- package/dist/context-engine/preambles/inbound-en.js +1 -1
- package/dist/context-engine/preambles/inbound-en.js.map +1 -1
- package/dist/context-engine/privacy-filter.d.ts +2 -2
- package/dist/context-engine/privacy-filter.js +1 -1
- package/dist/context-engine/privacy-filter.js.map +1 -1
- package/dist/context-engine/templates/base-en.d.ts +1 -1
- package/dist/context-engine/templates/base-en.js +1 -1
- package/dist/context-engine/templates/base-en.js.map +1 -1
- package/dist/context-engine/templates/composer.d.ts +1 -1
- package/dist/context-engine/templates/composer.js.map +1 -1
- package/dist/context-engine/token-budget.d.ts +1 -1
- package/dist/context-engine/token-budget.js +1 -1
- package/dist/context-engine/token-budget.js.map +1 -1
- package/dist/context-engine/token-counter.d.ts +16 -7
- package/dist/context-engine/token-counter.d.ts.map +1 -1
- package/dist/context-engine/token-counter.js +31 -9
- package/dist/context-engine/token-counter.js.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +4 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/facade.d.ts +44 -15
- package/dist/facade.d.ts.map +1 -1
- package/dist/facade.js +16 -8
- package/dist/facade.js.map +1 -1
- package/dist/graph/entity-resolver.d.ts +2 -2
- package/dist/graph/entity-resolver.js +2 -2
- package/dist/graph/entity-resolver.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/internal/contextualize.js +2 -2
- package/dist/internal/contextualize.js.map +1 -1
- package/dist/internal/embedder-binding.js +1 -1
- package/dist/internal/embedder-binding.js.map +1 -1
- package/dist/internal/id.js +1 -1
- package/dist/internal/id.js.map +1 -1
- package/dist/internal/injection-heuristics.js +25 -2
- package/dist/internal/injection-heuristics.js.map +1 -1
- package/dist/internal/spans.js +1 -1
- package/dist/internal/spans.js.map +1 -1
- package/dist/internal/storage-adapter.d.ts +71 -22
- package/dist/internal/storage-adapter.d.ts.map +1 -1
- package/dist/migration/embedder-migration.d.ts +7 -7
- package/dist/migration/embedder-migration.js.map +1 -1
- package/dist/search/explain.d.ts +7 -4
- package/dist/search/explain.d.ts.map +1 -1
- package/dist/search/explain.js +1 -1
- package/dist/search/explain.js.map +1 -1
- package/dist/search/fit-weights.d.ts +48 -0
- package/dist/search/fit-weights.d.ts.map +1 -0
- package/dist/search/fit-weights.js +70 -0
- package/dist/search/fit-weights.js.map +1 -0
- package/dist/search/graph-ppr.d.ts +36 -0
- package/dist/search/graph-ppr.d.ts.map +1 -0
- package/dist/search/graph-ppr.js +38 -0
- package/dist/search/graph-ppr.js.map +1 -0
- package/dist/search/index.d.ts +4 -1
- package/dist/search/index.js +4 -1
- package/dist/search/iterative.d.ts +15 -8
- package/dist/search/iterative.d.ts.map +1 -1
- package/dist/search/iterative.js +10 -9
- package/dist/search/iterative.js.map +1 -1
- package/dist/search/query-transform.d.ts +6 -6
- package/dist/search/query-transform.js +6 -6
- package/dist/search/query-transform.js.map +1 -1
- package/dist/search/rrf.d.ts +4 -4
- package/dist/search/rrf.js +4 -4
- package/dist/search/rrf.js.map +1 -1
- package/dist/search/trust.d.ts +30 -0
- package/dist/search/trust.d.ts.map +1 -0
- package/dist/search/trust.js +50 -0
- package/dist/search/trust.js.map +1 -0
- package/dist/search/types.d.ts +1 -1
- package/dist/tiers/episodic-memory.d.ts +6 -4
- package/dist/tiers/episodic-memory.d.ts.map +1 -1
- package/dist/tiers/episodic-memory.js +3 -2
- package/dist/tiers/episodic-memory.js.map +1 -1
- package/dist/tiers/index.d.ts +1 -1
- package/dist/tiers/index.js +1 -1
- package/dist/tiers/insight-memory.d.ts +4 -4
- package/dist/tiers/insight-memory.js +3 -3
- package/dist/tiers/insight-memory.js.map +1 -1
- package/dist/tiers/procedural-memory.d.ts +29 -7
- package/dist/tiers/procedural-memory.d.ts.map +1 -1
- package/dist/tiers/procedural-memory.js +66 -6
- package/dist/tiers/procedural-memory.js.map +1 -1
- package/dist/tiers/semantic-memory.d.ts +111 -30
- package/dist/tiers/semantic-memory.d.ts.map +1 -1
- package/dist/tiers/semantic-memory.js +120 -27
- package/dist/tiers/semantic-memory.js.map +1 -1
- package/dist/tiers/session-memory.d.ts +7 -7
- package/dist/tiers/session-memory.js +7 -7
- package/dist/tiers/session-memory.js.map +1 -1
- package/dist/tiers/shared-memory.d.ts +1 -1
- package/dist/tiers/shared-memory.js +1 -1
- package/dist/tiers/shared-memory.js.map +1 -1
- package/dist/tiers/working-memory.d.ts +3 -3
- package/dist/tiers/working-memory.js +2 -2
- package/dist/tiers/working-memory.js.map +1 -1
- package/dist/tools/block-tools.d.ts +9 -9
- package/dist/tools/block-tools.d.ts.map +1 -1
- package/dist/tools/block-tools.js +3 -3
- package/dist/tools/block-tools.js.map +1 -1
- package/dist/tools/fact-tools.d.ts +48 -31
- package/dist/tools/fact-tools.d.ts.map +1 -1
- package/dist/tools/fact-tools.js +30 -18
- package/dist/tools/fact-tools.js.map +1 -1
- package/dist/tools/index.d.ts +9 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/recall-tools.d.ts +40 -20
- package/dist/tools/recall-tools.d.ts.map +1 -1
- package/dist/tools/recall-tools.js +29 -10
- package/dist/tools/recall-tools.js.map +1 -1
- package/dist/tools/runbook-tools.d.ts +76 -0
- package/dist/tools/runbook-tools.d.ts.map +1 -0
- package/dist/tools/runbook-tools.js +53 -0
- package/dist/tools/runbook-tools.js.map +1 -0
- package/dist/tools/taint.js +33 -0
- package/dist/tools/taint.js.map +1 -0
- package/dist/tools/types.d.ts +1 -1
- package/package.json +8 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage2-embedding-three-zone.js","names":["stage2EmbeddingThreeZone: PipelineStage"],"sources":["../../../src/conflict/stages/stage2-embedding-three-zone.ts"],"sourcesContent":["/**\n * Stage 2
|
|
1
|
+
{"version":3,"file":"stage2-embedding-three-zone.js","names":["stage2EmbeddingThreeZone: PipelineStage"],"sources":["../../../src/conflict/stages/stage2-embedding-three-zone.ts"],"sourcesContent":["/**\n * Stage 2 - Embedding three-zone classifier (RB-02 §8.1).\n *\n * Inspects the candidate similarity against the top-K nearest\n * neighbours surfaced by the semantic vector store and routes the\n * decision into one of four zones:\n *\n * - HOT (`sim ≥ thresholds.hot`): silent dedup - the existing fact is\n * semantically identical, no audit-worthy conflict.\n * - NEAR-DUP (`thresholds.nearDup ≤ sim < thresholds.hot`): same\n * fact phrased differently - dedup with `near-dup` zone label so\n * operators can spot the case in `fact_conflicts`.\n * - CONFLICT-CHECK (`thresholds.cold < sim < thresholds.nearDup`):\n * candidate sounds related; defer decision to Stages 3 → 4 → 5.\n * - COLD (`sim ≤ thresholds.cold`): no conflict - admit the candidate.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage, StageOutcome } from '../types.js';\n\nexport const stage2EmbeddingThreeZone: PipelineStage = {\n id: 'embedding-three-zone',\n async evaluate(ctx): Promise<StageOutcome> {\n if (ctx.existing.length === 0) {\n return { kind: 'admit', reason: 'no-existing-candidates' };\n }\n const top = ctx.existing[0];\n if (top === undefined) {\n return { kind: 'admit', reason: 'no-existing-candidates' };\n }\n const similarity = top.score;\n if (similarity >= ctx.thresholds.hot) {\n return {\n kind: 'dedup',\n existingId: top.record.id,\n similarity,\n reason: 'embedding-hot-zone',\n };\n }\n if (similarity >= ctx.thresholds.nearDup) {\n return {\n kind: 'dedup',\n existingId: top.record.id,\n similarity,\n reason: 'embedding-near-dup-zone',\n };\n }\n if (similarity > ctx.thresholds.cold) {\n return { kind: 'continue' };\n }\n return { kind: 'admit', reason: 'embedding-cold-zone' };\n },\n};\n"],"mappings":";AAqBA,MAAaA,2BAA0C;CACrD,IAAI;CACJ,MAAM,SAAS,KAA4B;AACzC,MAAI,IAAI,SAAS,WAAW,EAC1B,QAAO;GAAE,MAAM;GAAS,QAAQ;GAA0B;EAE5D,MAAM,MAAM,IAAI,SAAS;AACzB,MAAI,QAAQ,OACV,QAAO;GAAE,MAAM;GAAS,QAAQ;GAA0B;EAE5D,MAAM,aAAa,IAAI;AACvB,MAAI,cAAc,IAAI,WAAW,IAC/B,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB;GACA,QAAQ;GACT;AAEH,MAAI,cAAc,IAAI,WAAW,QAC/B,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB;GACA,QAAQ;GACT;AAEH,MAAI,aAAa,IAAI,WAAW,KAC9B,QAAO,EAAE,MAAM,YAAY;AAE7B,SAAO;GAAE,MAAM;GAAS,QAAQ;GAAuB;;CAE1D"}
|
|
@@ -3,7 +3,7 @@ import "../locale-packs/index.js";
|
|
|
3
3
|
|
|
4
4
|
//#region src/conflict/stages/stage3-heuristic-regex.ts
|
|
5
5
|
/**
|
|
6
|
-
* Stage 3
|
|
6
|
+
* Stage 3 - Heuristic regex (per-locale, pluggable).
|
|
7
7
|
*
|
|
8
8
|
* Inspects the candidate fact body for explicit supersede or negation
|
|
9
9
|
* markers from the active locale pack (English by default). When a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage3-heuristic-regex.js","names":["stage3HeuristicRegex: PipelineStage"],"sources":["../../../src/conflict/stages/stage3-heuristic-regex.ts"],"sourcesContent":["/**\n * Stage 3
|
|
1
|
+
{"version":3,"file":"stage3-heuristic-regex.js","names":["stage3HeuristicRegex: PipelineStage"],"sources":["../../../src/conflict/stages/stage3-heuristic-regex.ts"],"sourcesContent":["/**\n * Stage 3 - Heuristic regex (per-locale, pluggable).\n *\n * Inspects the candidate fact body for explicit supersede or negation\n * markers from the active locale pack (English by default). When a\n * marker fires the stage emits a `supersede` outcome against the\n * highest-scoring CONFLICT-CHECK candidate from Stage 2.\n *\n * @packageDocumentation\n */\n\nimport { evaluateMarkers } from '../locale-packs/index.js';\nimport type { PipelineStage, StageOutcome } from '../types.js';\n\nexport const stage3HeuristicRegex: PipelineStage = {\n id: 'heuristic-regex',\n async evaluate(ctx): Promise<StageOutcome> {\n const top = ctx.existing[0];\n if (top === undefined) {\n return { kind: 'continue' };\n }\n const candidateText = ctx.candidate.text;\n const supersede = evaluateMarkers(candidateText, ctx.localePack.supersedeMarkers);\n if (supersede.matched) {\n return {\n kind: 'supersede',\n existingId: top.record.id,\n reason: formatReason('regex-supersede-marker', supersede),\n };\n }\n const negation = evaluateMarkers(candidateText, ctx.localePack.negationMarkers);\n if (negation.matched) {\n return {\n kind: 'supersede',\n existingId: top.record.id,\n reason: formatReason('regex-negation', negation),\n };\n }\n return { kind: 'continue' };\n },\n};\n\nfunction formatReason(\n base: string,\n match: { kind?: string | undefined; excerpt?: string | undefined },\n): string {\n const parts = [base];\n if (match.kind !== undefined) parts.push(`kind=${match.kind}`);\n if (match.excerpt !== undefined) parts.push(`marker=${match.excerpt}`);\n return parts.join('; ');\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAaA,uBAAsC;CACjD,IAAI;CACJ,MAAM,SAAS,KAA4B;EACzC,MAAM,MAAM,IAAI,SAAS;AACzB,MAAI,QAAQ,OACV,QAAO,EAAE,MAAM,YAAY;EAE7B,MAAM,gBAAgB,IAAI,UAAU;EACpC,MAAM,YAAY,gBAAgB,eAAe,IAAI,WAAW,iBAAiB;AACjF,MAAI,UAAU,QACZ,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB,QAAQ,aAAa,0BAA0B,UAAU;GAC1D;EAEH,MAAM,WAAW,gBAAgB,eAAe,IAAI,WAAW,gBAAgB;AAC/E,MAAI,SAAS,QACX,QAAO;GACL,MAAM;GACN,YAAY,IAAI,OAAO;GACvB,QAAQ,aAAa,kBAAkB,SAAS;GACjD;AAEH,SAAO,EAAE,MAAM,YAAY;;CAE9B;AAED,SAAS,aACP,MACA,OACQ;CACR,MAAM,QAAQ,CAAC,KAAK;AACpB,KAAI,MAAM,SAAS,OAAW,OAAM,KAAK,QAAQ,MAAM,OAAO;AAC9D,KAAI,MAAM,YAAY,OAAW,OAAM,KAAK,UAAU,MAAM,UAAU;AACtE,QAAO,MAAM,KAAK,KAAK"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage4-subject-predicate.js","names":["stage4SubjectPredicate: PipelineStage"],"sources":["../../../src/conflict/stages/stage4-subject-predicate.ts"],"sourcesContent":["/**\n * Stage 4
|
|
1
|
+
{"version":3,"file":"stage4-subject-predicate.js","names":["stage4SubjectPredicate: PipelineStage"],"sources":["../../../src/conflict/stages/stage4-subject-predicate.ts"],"sourcesContent":["/**\n * Stage 4 - Subject / predicate match.\n *\n * Naively splits each fact body into `(subject, predicate, object)`\n * triples using the locale pack's predicate normalisers. When the\n * candidate and an existing CONFLICT-CHECK fact share the same\n * subject + predicate but differ on the object, Stage 4 marks the\n * existing fact as superseded.\n *\n * The split is intentionally simple: anything more sophisticated\n * (full entity linking) lives in the post-MVP knowledge-graph layer.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage, StageOutcome } from '../types.js';\nimport { splitSubjectPredicateObject } from './helpers.js';\n\nexport const stage4SubjectPredicate: PipelineStage = {\n id: 'subject-predicate',\n async evaluate(ctx): Promise<StageOutcome> {\n const candidateTriple = splitSubjectPredicateObject(ctx.candidate.text, ctx.localePack);\n if (candidateTriple === null) {\n return { kind: 'continue' };\n }\n for (const hit of ctx.existing) {\n const existingTriple = splitSubjectPredicateObject(hit.record.text, ctx.localePack);\n if (existingTriple === null) continue;\n if (candidateTriple.subject !== existingTriple.subject) continue;\n if (candidateTriple.predicate !== existingTriple.predicate) continue;\n if (candidateTriple.object === existingTriple.object) {\n return {\n kind: 'dedup',\n existingId: hit.record.id,\n reason: 'subject-predicate-match-same-object',\n };\n }\n return {\n kind: 'supersede',\n existingId: hit.record.id,\n reason: `subject-predicate-match; subject=${candidateTriple.subject}; predicate=${candidateTriple.predicate}`,\n };\n }\n return { kind: 'continue' };\n },\n};\n"],"mappings":";;;AAkBA,MAAaA,yBAAwC;CACnD,IAAI;CACJ,MAAM,SAAS,KAA4B;EACzC,MAAM,kBAAkB,4BAA4B,IAAI,UAAU,MAAM,IAAI,WAAW;AACvF,MAAI,oBAAoB,KACtB,QAAO,EAAE,MAAM,YAAY;AAE7B,OAAK,MAAM,OAAO,IAAI,UAAU;GAC9B,MAAM,iBAAiB,4BAA4B,IAAI,OAAO,MAAM,IAAI,WAAW;AACnF,OAAI,mBAAmB,KAAM;AAC7B,OAAI,gBAAgB,YAAY,eAAe,QAAS;AACxD,OAAI,gBAAgB,cAAc,eAAe,UAAW;AAC5D,OAAI,gBAAgB,WAAW,eAAe,OAC5C,QAAO;IACL,MAAM;IACN,YAAY,IAAI,OAAO;IACvB,QAAQ;IACT;AAEH,UAAO;IACL,MAAM;IACN,YAAY,IAAI,OAAO;IACvB,QAAQ,oCAAoC,gBAAgB,QAAQ,cAAc,gBAAgB;IACnG;;AAEH,SAAO,EAAE,MAAM,YAAY;;CAE9B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage5-defer-to-deep.js","names":["stage5DeferToDeep: PipelineStage"],"sources":["../../../src/conflict/stages/stage5-defer-to-deep.ts"],"sourcesContent":["/**\n * Stage 5
|
|
1
|
+
{"version":3,"file":"stage5-defer-to-deep.js","names":["stage5DeferToDeep: PipelineStage"],"sources":["../../../src/conflict/stages/stage5-defer-to-deep.ts"],"sourcesContent":["/**\n * Stage 5 - Defer to deep LLM judge.\n *\n * Reached only when Stages 1-4 left the decision unresolved. The\n * stage records every CONFLICT-CHECK candidate id surfaced by Stage 2\n * so the orchestrator can hand the queue write to the storage layer\n * (the actual `conflict_check_pending` insert is the orchestrator's\n * responsibility - Stage 5 only computes the payload).\n *\n * The candidate fact itself is admitted with a `pending` flag so it\n * remains searchable; the deep phase (Phase 10c) drains the queue and\n * applies the resolution later.\n *\n * @packageDocumentation\n */\n\nimport type { PipelineStage, StageOutcome } from '../types.js';\n\nexport const stage5DeferToDeep: PipelineStage = {\n id: 'defer-to-deep',\n async evaluate(ctx): Promise<StageOutcome> {\n if (ctx.existing.length === 0) {\n return { kind: 'admit', reason: 'stage5-no-candidates' };\n }\n const conflictingIds = ctx.existing.map((hit) => hit.record.id);\n const top = ctx.existing[0];\n return {\n kind: 'pending',\n conflictingIds,\n ...(top !== undefined ? { similarity: top.score } : {}),\n reason: 'awaiting-deep-llm-judge',\n };\n },\n};\n"],"mappings":";AAkBA,MAAaA,oBAAmC;CAC9C,IAAI;CACJ,MAAM,SAAS,KAA4B;AACzC,MAAI,IAAI,SAAS,WAAW,EAC1B,QAAO;GAAE,MAAM;GAAS,QAAQ;GAAwB;EAE1D,MAAM,iBAAiB,IAAI,SAAS,KAAK,QAAQ,IAAI,OAAO,GAAG;EAC/D,MAAM,MAAM,IAAI,SAAS;AACzB,SAAO;GACL,MAAM;GACN;GACA,GAAI,QAAQ,SAAY,EAAE,YAAY,IAAI,OAAO,GAAG,EAAE;GACtD,QAAQ;GACT;;CAEJ"}
|
package/dist/conflict/types.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { EmbedderProvider, Fact, MemoryHit, Tracer } from "@graphorin/core";
|
|
|
15
15
|
type ConflictStage = 'exact-dedup' | 'embedding-three-zone' | 'heuristic-regex' | 'subject-predicate' | 'defer-to-deep';
|
|
16
16
|
/**
|
|
17
17
|
* Configurable similarity thresholds for Stage 2 (embedding three-
|
|
18
|
-
* zone). Defaults pinned at `0.95 / 0.85 / 0.4` per RB-02 §8
|
|
18
|
+
* zone). Defaults pinned at `0.95 / 0.85 / 0.4` per RB-02 §8 - the
|
|
19
19
|
* production values for the default `Xenova/multilingual-e5-base`
|
|
20
20
|
* embedder (DEC-130).
|
|
21
21
|
*
|
|
@@ -34,8 +34,8 @@ interface ConflictThresholds {
|
|
|
34
34
|
*/
|
|
35
35
|
declare const DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds;
|
|
36
36
|
/**
|
|
37
|
-
* Final pipeline outcome
|
|
38
|
-
* {@link runConflictPipeline}. Mirrors RB-02 §8.1 / DEC-117
|
|
37
|
+
* Final pipeline outcome - discriminated union returned by
|
|
38
|
+
* {@link runConflictPipeline}. Mirrors RB-02 §8.1 / DEC-117 - every
|
|
39
39
|
* variant carries the originating `stage` so audit + replay tooling
|
|
40
40
|
* can pattern-match without inspecting the message.
|
|
41
41
|
*
|
|
@@ -72,9 +72,9 @@ type ConflictDecision = {
|
|
|
72
72
|
*
|
|
73
73
|
* The `mode` field is the master switch:
|
|
74
74
|
*
|
|
75
|
-
* - `'on'` (default)
|
|
75
|
+
* - `'on'` (default) - the multi-stage pipeline runs on every
|
|
76
76
|
* `SemanticMemory.remember(...)` call.
|
|
77
|
-
* - `'off'`
|
|
77
|
+
* - `'off'` - bypass the pipeline and fall back to 10a's straight-
|
|
78
78
|
* through write. Emits a one-shot WARN (per process) so operators
|
|
79
79
|
* notice the regression risk.
|
|
80
80
|
*
|
|
@@ -107,7 +107,7 @@ interface StageContext {
|
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
109
|
* Per-stage outcome surfaced to the orchestrator. `'admit'` means the
|
|
110
|
-
* stage decided not to short-circuit
|
|
110
|
+
* stage decided not to short-circuit - the pipeline continues to the
|
|
111
111
|
* next stage. Every other variant terminates the pipeline.
|
|
112
112
|
*
|
|
113
113
|
* @stable
|
|
@@ -210,7 +210,7 @@ type ReconcileDecision = {
|
|
|
210
210
|
* so reconcile outcomes land in `fact_conflicts` through the same audit
|
|
211
211
|
* path as the inline pipeline (no new stage / schema): `add` → `admit`,
|
|
212
212
|
* `noop` → `dedup`, `update` / `conflict` → `supersede`. All reconcile
|
|
213
|
-
* decisions are stamped with the `defer-to-deep` stage
|
|
213
|
+
* decisions are stamped with the `defer-to-deep` stage - the reconcile
|
|
214
214
|
* loop is the consolidator's replacement for the deferred deep-judge
|
|
215
215
|
* step it supersedes.
|
|
216
216
|
*
|
package/dist/conflict/types.js
CHANGED
|
@@ -15,7 +15,7 @@ const DEFAULT_CONFLICT_THRESHOLDS = Object.freeze({
|
|
|
15
15
|
* so reconcile outcomes land in `fact_conflicts` through the same audit
|
|
16
16
|
* path as the inline pipeline (no new stage / schema): `add` → `admit`,
|
|
17
17
|
* `noop` → `dedup`, `update` / `conflict` → `supersede`. All reconcile
|
|
18
|
-
* decisions are stamped with the `defer-to-deep` stage
|
|
18
|
+
* decisions are stamped with the `defer-to-deep` stage - the reconcile
|
|
19
19
|
* loop is the consolidator's replacement for the deferred deep-judge
|
|
20
20
|
* step it supersedes.
|
|
21
21
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":["DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds","stage: ConflictStage"],"sources":["../../src/conflict/types.ts"],"sourcesContent":["/**\n * Public types for the multi-stage conflict resolution pipeline shipped\n * in `@graphorin/memory` Phase 10b (DEC-117 / ADR-018 ext / RB-02).\n *\n * @packageDocumentation\n */\n\nimport type { EmbedderProvider, Fact, MemoryHit, Tracer } from '@graphorin/core';\nimport type { ConflictMemoryStoreExt, MemoryStoreAdapter } from '../internal/storage-adapter.js';\nimport type { LocalePack } from './locale-packs/index.js';\n\n/**\n * Stable lowercase identifier of the pipeline stage. Mirrored\n * byte-for-byte by `@graphorin/store-sqlite`'s `ConflictPipelineStage`\n * so `fact_conflicts` / `conflict_check_pending` rows can be grouped\n * deterministically.\n *\n * @stable\n */\nexport type ConflictStage =\n | 'exact-dedup'\n | 'embedding-three-zone'\n | 'heuristic-regex'\n | 'subject-predicate'\n | 'defer-to-deep';\n\n/**\n * Configurable similarity thresholds for Stage 2 (embedding three-\n * zone). Defaults pinned at `0.95 / 0.85 / 0.4` per RB-02 §8
|
|
1
|
+
{"version":3,"file":"types.js","names":["DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds","stage: ConflictStage"],"sources":["../../src/conflict/types.ts"],"sourcesContent":["/**\n * Public types for the multi-stage conflict resolution pipeline shipped\n * in `@graphorin/memory` Phase 10b (DEC-117 / ADR-018 ext / RB-02).\n *\n * @packageDocumentation\n */\n\nimport type { EmbedderProvider, Fact, MemoryHit, Tracer } from '@graphorin/core';\nimport type { ConflictMemoryStoreExt, MemoryStoreAdapter } from '../internal/storage-adapter.js';\nimport type { LocalePack } from './locale-packs/index.js';\n\n/**\n * Stable lowercase identifier of the pipeline stage. Mirrored\n * byte-for-byte by `@graphorin/store-sqlite`'s `ConflictPipelineStage`\n * so `fact_conflicts` / `conflict_check_pending` rows can be grouped\n * deterministically.\n *\n * @stable\n */\nexport type ConflictStage =\n | 'exact-dedup'\n | 'embedding-three-zone'\n | 'heuristic-regex'\n | 'subject-predicate'\n | 'defer-to-deep';\n\n/**\n * Configurable similarity thresholds for Stage 2 (embedding three-\n * zone). Defaults pinned at `0.95 / 0.85 / 0.4` per RB-02 §8 - the\n * production values for the default `Xenova/multilingual-e5-base`\n * embedder (DEC-130).\n *\n * @stable\n */\nexport interface ConflictThresholds {\n readonly hot: number;\n readonly nearDup: number;\n readonly cold: number;\n}\n\n/**\n * Hard-coded defaults for the three-zone thresholds. Imported by the\n * pipeline + the test suite so call sites stay aligned.\n *\n * @stable\n */\nexport const DEFAULT_CONFLICT_THRESHOLDS: ConflictThresholds = Object.freeze({\n hot: 0.95,\n nearDup: 0.85,\n cold: 0.4,\n});\n\n/**\n * Final pipeline outcome - discriminated union returned by\n * {@link runConflictPipeline}. Mirrors RB-02 §8.1 / DEC-117 - every\n * variant carries the originating `stage` so audit + replay tooling\n * can pattern-match without inspecting the message.\n *\n * @stable\n */\nexport type ConflictDecision =\n | {\n readonly kind: 'admit';\n readonly stage: ConflictStage;\n readonly reason?: string;\n }\n | {\n readonly kind: 'dedup';\n readonly stage: ConflictStage;\n readonly existingId: string;\n readonly similarity?: number;\n readonly reason?: string;\n }\n | {\n readonly kind: 'supersede';\n readonly stage: ConflictStage;\n readonly existingId: string;\n readonly reason: string;\n }\n | {\n readonly kind: 'pending';\n readonly stage: ConflictStage;\n /** The candidate id that was admitted as `pending`. */\n readonly candidateId: string;\n /** Top-K conflicting existing fact ids from Stage 2's vector search. */\n readonly conflictingIds: ReadonlyArray<string>;\n readonly similarity?: number;\n readonly reason?: string;\n };\n\n/**\n * Pipeline configuration accepted by `createMemory({ conflictPipeline:\n * ... })` and surfaced through {@link createConflictPipeline}.\n *\n * The `mode` field is the master switch:\n *\n * - `'on'` (default) - the multi-stage pipeline runs on every\n * `SemanticMemory.remember(...)` call.\n * - `'off'` - bypass the pipeline and fall back to 10a's straight-\n * through write. Emits a one-shot WARN (per process) so operators\n * notice the regression risk.\n *\n * @stable\n */\nexport interface ConflictPipelineOptions {\n readonly mode?: 'on' | 'off';\n readonly thresholds?: Partial<ConflictThresholds>;\n readonly localePack?: LocalePack;\n /** Per-list candidate count fed into Stage 2. Default `5` (RB-02 §8.1). */\n readonly stage2TopK?: number;\n /** Override the audit / pending sink. Defaults to `store.conflicts` when present. */\n readonly conflictStore?: ConflictMemoryStoreExt;\n /** Inject a deterministic clock. Defaults to `() => new Date().toISOString()`. */\n readonly now?: () => string;\n}\n\n/**\n * Inputs the orchestrator hands every stage. The `existing` array is\n * populated during Stage 2 (vector search top-K); Stage 1 receives an\n * empty array because the dedup hash is computed off the candidate\n * alone.\n *\n * @stable\n */\nexport interface StageContext {\n readonly candidate: Fact;\n readonly existing: ReadonlyArray<MemoryHit<Fact>>;\n readonly localePack: LocalePack;\n readonly thresholds: ConflictThresholds;\n}\n\n/**\n * Per-stage outcome surfaced to the orchestrator. `'admit'` means the\n * stage decided not to short-circuit - the pipeline continues to the\n * next stage. Every other variant terminates the pipeline.\n *\n * @stable\n */\nexport type StageOutcome =\n | { readonly kind: 'continue' }\n | { readonly kind: 'admit'; readonly reason?: string }\n | {\n readonly kind: 'dedup';\n readonly existingId: string;\n readonly similarity?: number;\n readonly reason?: string;\n }\n | {\n readonly kind: 'supersede';\n readonly existingId: string;\n readonly reason: string;\n }\n | {\n readonly kind: 'pending';\n readonly conflictingIds: ReadonlyArray<string>;\n readonly similarity?: number;\n readonly reason?: string;\n };\n\n/**\n * Stage interface every step implements. The orchestrator visits\n * stages in declaration order and stops at the first non-`'continue'`\n * outcome.\n *\n * @stable\n */\nexport interface PipelineStage {\n readonly id: ConflictStage;\n evaluate(ctx: StageContext): Promise<StageOutcome>;\n}\n\n/**\n * Pre-built pipeline returned by {@link createConflictPipeline}. The\n * `run` method is what `SemanticMemory.remember(...)` calls to make\n * the conflict decision.\n *\n * @stable\n */\nexport interface ConflictPipeline {\n readonly mode: 'on' | 'off';\n readonly localePack: LocalePack;\n readonly thresholds: ConflictThresholds;\n run(deps: ConflictPipelineDeps, candidate: Fact): Promise<ConflictDecision>;\n}\n\n/**\n * Per-call dependency bag handed to the pipeline by `SemanticMemory`.\n *\n * @stable\n */\nexport interface ConflictPipelineDeps {\n readonly store: MemoryStoreAdapter;\n readonly tracer: Tracer;\n readonly embedder: EmbedderProvider | null;\n readonly embedderId: string | null;\n /** Optional cancellation signal forwarded to embedder + searchVector. */\n readonly signal?: AbortSignal;\n}\n\n/**\n * The four actions the neighbour-aware reconcile loop (P0-3) may choose\n * for a candidate fact once the LLM has the most-similar existing\n * memories in view. The de-facto-standard memory write loop\n * (Mem0 / LangMem / Letta), with Graphorin's twist: `update` and\n * `conflict` route through a **bi-temporal supersede** (close the old\n * interval, insert the new) rather than a destructive delete.\n *\n * @stable\n */\nexport type ReconcileAction = 'add' | 'update' | 'noop' | 'conflict';\n\n/**\n * Outcome of {@link reconcileCandidate}. `add` is independent / unsure\n * (a fresh fact); `noop` is a duplicate that adds nothing; `update` is\n * a newer version of `targetId`; `conflict` contradicts `targetId` and\n * closes it. The three neighbour-referencing variants carry the\n * `targetId` of the existing fact they resolve against.\n *\n * @stable\n */\nexport type ReconcileDecision =\n | { readonly action: 'add'; readonly reason?: string }\n | { readonly action: 'update'; readonly targetId: string; readonly reason: string }\n | { readonly action: 'noop'; readonly targetId: string; readonly reason?: string }\n | { readonly action: 'conflict'; readonly targetId: string; readonly reason: string };\n\n/**\n * Map a {@link ReconcileDecision} onto the existing {@link ConflictDecision}\n * so reconcile outcomes land in `fact_conflicts` through the same audit\n * path as the inline pipeline (no new stage / schema): `add` → `admit`,\n * `noop` → `dedup`, `update` / `conflict` → `supersede`. All reconcile\n * decisions are stamped with the `defer-to-deep` stage - the reconcile\n * loop is the consolidator's replacement for the deferred deep-judge\n * step it supersedes.\n *\n * @stable\n */\nexport function reconcileToConflictDecision(decision: ReconcileDecision): ConflictDecision {\n const stage: ConflictStage = 'defer-to-deep';\n switch (decision.action) {\n case 'add':\n return {\n kind: 'admit',\n stage,\n ...(decision.reason !== undefined ? { reason: decision.reason } : {}),\n };\n case 'noop':\n return {\n kind: 'dedup',\n stage,\n existingId: decision.targetId,\n ...(decision.reason !== undefined ? { reason: decision.reason } : {}),\n };\n case 'update':\n case 'conflict':\n return {\n kind: 'supersede',\n stage,\n existingId: decision.targetId,\n reason: decision.reason,\n };\n }\n}\n"],"mappings":";;;;;;;AA8CA,MAAaA,8BAAkD,OAAO,OAAO;CAC3E,KAAK;CACL,SAAS;CACT,MAAM;CACP,CAAC;;;;;;;;;;;;AA2LF,SAAgB,4BAA4B,UAA+C;CACzF,MAAMC,QAAuB;AAC7B,SAAQ,SAAS,QAAjB;EACE,KAAK,MACH,QAAO;GACL,MAAM;GACN;GACA,GAAI,SAAS,WAAW,SAAY,EAAE,QAAQ,SAAS,QAAQ,GAAG,EAAE;GACrE;EACH,KAAK,OACH,QAAO;GACL,MAAM;GACN;GACA,YAAY,SAAS;GACrB,GAAI,SAAS,WAAW,SAAY,EAAE,QAAQ,SAAS,QAAQ,GAAG,EAAE;GACrE;EACH,KAAK;EACL,KAAK,WACH,QAAO;GACL,MAAM;GACN;GACA,YAAY,SAAS;GACrB,QAAQ,SAAS;GAClB"}
|
|
@@ -7,7 +7,7 @@ import { BudgetExceededError } from "./errors.js";
|
|
|
7
7
|
* (token + cost), the UTC reset semantics, and the `onExceed`
|
|
8
8
|
* dispatch.
|
|
9
9
|
*
|
|
10
|
-
* The tracker does not perform any I/O
|
|
10
|
+
* The tracker does not perform any I/O - operators surface the live
|
|
11
11
|
* counters via `Consolidator.status()` and the persisted snapshot is
|
|
12
12
|
* derived from `consolidator_runs` rows.
|
|
13
13
|
*
|
|
@@ -24,6 +24,7 @@ var BudgetTracker = class {
|
|
|
24
24
|
#now;
|
|
25
25
|
#resetSemantics;
|
|
26
26
|
#onExceed;
|
|
27
|
+
#logger;
|
|
27
28
|
#maxTokensPerDay;
|
|
28
29
|
#maxCostPerDay;
|
|
29
30
|
#bucketStart;
|
|
@@ -31,17 +32,22 @@ var BudgetTracker = class {
|
|
|
31
32
|
#cost = 0;
|
|
32
33
|
#paused = false;
|
|
33
34
|
#pausedReason = null;
|
|
35
|
+
/** Resources already WARNed this window under `onExceed: 'log'` (memory-consolidation-02). */
|
|
36
|
+
#warnedThisWindow = /* @__PURE__ */ new Set();
|
|
34
37
|
/**
|
|
35
38
|
* Timestamped spend ledger for `sliding-24h` only. `#maybeReset` trims it to
|
|
36
39
|
* the trailing 24h window and recomputes `#tokens` / `#cost` from it, so the
|
|
37
40
|
* counters reflect a true rolling window instead of being zeroed on every
|
|
38
|
-
* check (MCON-3). Unused
|
|
41
|
+
* check (MCON-3). Unused - and never appended to - under `utc` / `local`.
|
|
39
42
|
*/
|
|
40
43
|
#ledger = [];
|
|
41
44
|
constructor(opts) {
|
|
42
45
|
this.#now = opts.now ?? Date.now;
|
|
43
46
|
this.#resetSemantics = opts.resetSemantics;
|
|
44
47
|
this.#onExceed = opts.onExceed;
|
|
48
|
+
this.#logger = opts.logger ?? ((message) => {
|
|
49
|
+
process.stderr.write(`${message}\n`);
|
|
50
|
+
});
|
|
45
51
|
this.#maxTokensPerDay = opts.maxTokensPerDay;
|
|
46
52
|
this.#maxCostPerDay = opts.maxCostPerDay;
|
|
47
53
|
this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);
|
|
@@ -86,7 +92,7 @@ var BudgetTracker = class {
|
|
|
86
92
|
return { allowed: true };
|
|
87
93
|
}
|
|
88
94
|
/**
|
|
89
|
-
* Record consumption. Returns the post-record state
|
|
95
|
+
* Record consumption. Returns the post-record state - `paused` is
|
|
90
96
|
* `true` when the spend pushed past a ceiling under
|
|
91
97
|
* `onExceed: 'pause'`.
|
|
92
98
|
*
|
|
@@ -138,6 +144,7 @@ var BudgetTracker = class {
|
|
|
138
144
|
this.#paused = false;
|
|
139
145
|
this.#pausedReason = null;
|
|
140
146
|
this.#ledger = [];
|
|
147
|
+
this.#warnedThisWindow.clear();
|
|
141
148
|
this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);
|
|
142
149
|
}
|
|
143
150
|
#handleBreach(phase, resource, actual, budget) {
|
|
@@ -151,6 +158,10 @@ var BudgetTracker = class {
|
|
|
151
158
|
this.#paused = true;
|
|
152
159
|
this.#pausedReason = resource === "tokens" ? "tokens-exceeded" : "cost-exceeded";
|
|
153
160
|
}
|
|
161
|
+
if (this.#onExceed === "log" && !this.#warnedThisWindow.has(resource)) {
|
|
162
|
+
this.#warnedThisWindow.add(resource);
|
|
163
|
+
this.#logger(`[graphorin/memory] consolidator ${resource} budget exceeded in phase '${phase}': ${resource === "cost" ? `$${actual.toFixed(4)} > $${budget.toFixed(4)}` : `${actual} > ${budget}`} (onExceed: 'log' - continuing; switch to 'pause'/'throw' to enforce).`);
|
|
164
|
+
}
|
|
154
165
|
}
|
|
155
166
|
#maybeReset() {
|
|
156
167
|
const now = this.#now();
|
|
@@ -163,6 +174,7 @@ var BudgetTracker = class {
|
|
|
163
174
|
this.#paused = false;
|
|
164
175
|
this.#pausedReason = null;
|
|
165
176
|
}
|
|
177
|
+
if (this.#tokens <= this.#maxTokensPerDay && this.#cost <= this.#maxCostPerDay) this.#warnedThisWindow.clear();
|
|
166
178
|
return;
|
|
167
179
|
}
|
|
168
180
|
const currentBucket = bucketStart(now, this.#resetSemantics);
|
|
@@ -172,6 +184,7 @@ var BudgetTracker = class {
|
|
|
172
184
|
this.#cost = 0;
|
|
173
185
|
this.#paused = false;
|
|
174
186
|
this.#pausedReason = null;
|
|
187
|
+
this.#warnedThisWindow.clear();
|
|
175
188
|
}
|
|
176
189
|
}
|
|
177
190
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"budget.js","names":["#now","#resetSemantics","#onExceed","#maxTokensPerDay","#maxCostPerDay","#bucketStart","#paused","#tokens","#cost","#pausedReason","#maybeReset","#ledger","#handleBreach"],"sources":["../../src/consolidator/budget.ts"],"sourcesContent":["/**\n * Cost-budget enforcement for the consolidator. The tracker is a\n * pure in-memory state machine: it owns the day-bucketed counters\n * (token + cost), the UTC reset semantics, and the `onExceed`\n * dispatch.\n *\n * The tracker does not perform any I/O — operators surface the live\n * counters via `Consolidator.status()` and the persisted snapshot is\n * derived from `consolidator_runs` rows.\n *\n * @packageDocumentation\n */\n\nimport { BudgetExceededError } from './errors.js';\nimport type { ConsolidatorPhase, OnBudgetExceed } from './types.js';\n\n/**\n * @stable\n */\nexport interface BudgetSnapshot {\n readonly tokensUsedToday: number;\n readonly costUsedToday: number;\n readonly tokensRemaining: number;\n readonly costRemaining: number;\n readonly resetAt: string;\n readonly paused: boolean;\n}\n\n/**\n * @stable\n */\nexport interface BudgetCheck {\n readonly allowed: boolean;\n readonly reason?: 'paused' | 'tokens-exceeded' | 'cost-exceeded';\n}\n\n/**\n * @stable\n */\nexport interface BudgetTrackerOptions {\n readonly maxTokensPerDay: number;\n readonly maxCostPerDay: number;\n readonly onExceed: OnBudgetExceed;\n readonly resetSemantics: 'utc' | 'local' | 'sliding-24h';\n readonly now?: () => number;\n}\n\n/**\n * Per-instance budget tracker. The runtime creates one tracker per\n * consolidator and resets the day counters lazily on the next phase\n * invocation.\n *\n * @stable\n */\nexport class BudgetTracker {\n readonly #now: () => number;\n readonly #resetSemantics: 'utc' | 'local' | 'sliding-24h';\n readonly #onExceed: OnBudgetExceed;\n #maxTokensPerDay: number;\n #maxCostPerDay: number;\n #bucketStart: number;\n #tokens = 0;\n #cost = 0;\n #paused = false;\n #pausedReason: 'tokens-exceeded' | 'cost-exceeded' | null = null;\n /**\n * Timestamped spend ledger for `sliding-24h` only. `#maybeReset` trims it to\n * the trailing 24h window and recomputes `#tokens` / `#cost` from it, so the\n * counters reflect a true rolling window instead of being zeroed on every\n * check (MCON-3). Unused — and never appended to — under `utc` / `local`.\n */\n #ledger: Array<{ at: number; tokens: number; cost: number }> = [];\n\n constructor(opts: BudgetTrackerOptions) {\n this.#now = opts.now ?? Date.now;\n this.#resetSemantics = opts.resetSemantics;\n this.#onExceed = opts.onExceed;\n this.#maxTokensPerDay = opts.maxTokensPerDay;\n this.#maxCostPerDay = opts.maxCostPerDay;\n this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);\n }\n\n /**\n * Replace the active ceilings. Used by `Consolidator.setTier(...)`.\n *\n * @stable\n */\n reconfigure(opts: { maxTokensPerDay: number; maxCostPerDay: number }): void {\n this.#maxTokensPerDay = opts.maxTokensPerDay;\n this.#maxCostPerDay = opts.maxCostPerDay;\n if (this.#paused) {\n const tokensOk = this.#tokens <= this.#maxTokensPerDay;\n const costOk = this.#cost <= this.#maxCostPerDay;\n if (tokensOk && costOk) {\n this.#paused = false;\n this.#pausedReason = null;\n }\n }\n }\n\n /**\n * Return whether the supplied phase may run right now.\n *\n * @stable\n */\n precheck(phase: ConsolidatorPhase): BudgetCheck {\n this.#maybeReset();\n if (phase === 'light') return { allowed: true };\n if (this.#paused) {\n const reason = this.#pausedReason ?? 'cost-exceeded';\n return { allowed: false, reason };\n }\n if (this.#maxTokensPerDay <= 0) {\n return { allowed: false, reason: 'tokens-exceeded' };\n }\n if (this.#maxCostPerDay <= 0) {\n return { allowed: false, reason: 'cost-exceeded' };\n }\n return { allowed: true };\n }\n\n /**\n * Record consumption. Returns the post-record state — `paused` is\n * `true` when the spend pushed past a ceiling under\n * `onExceed: 'pause'`.\n *\n * Throws {@link BudgetExceededError} when the configured behaviour\n * is `'throw'`.\n *\n * @stable\n */\n record(args: { phase: ConsolidatorPhase; tokens: number; costUsd: number }): BudgetSnapshot {\n this.#maybeReset();\n const tokens = Math.max(0, args.tokens);\n const cost = Math.max(0, args.costUsd);\n if (this.#resetSemantics === 'sliding-24h') {\n this.#ledger.push({ at: this.#now(), tokens, cost });\n }\n this.#tokens += tokens;\n this.#cost += cost;\n if (this.#tokens > this.#maxTokensPerDay) {\n this.#handleBreach(args.phase, 'tokens', this.#tokens, this.#maxTokensPerDay);\n }\n if (this.#cost > this.#maxCostPerDay) {\n this.#handleBreach(args.phase, 'cost', this.#cost, this.#maxCostPerDay);\n }\n return this.snapshot();\n }\n\n /**\n * Read-only snapshot. Surfaced through `Consolidator.status()`.\n *\n * @stable\n */\n snapshot(): BudgetSnapshot {\n this.#maybeReset();\n // For the rolling window the \"reset\" is continuous — the boundary is when\n // the oldest in-window spend ages out (24h after it landed).\n const resetAt =\n this.#resetSemantics === 'sliding-24h'\n ? (this.#ledger[0]?.at ?? this.#now()) + DAY_MS\n : nextBucketStart(this.#bucketStart, this.#resetSemantics);\n return Object.freeze({\n tokensUsedToday: this.#tokens,\n costUsedToday: this.#cost,\n tokensRemaining: Math.max(0, this.#maxTokensPerDay - this.#tokens),\n costRemaining: Math.max(0, this.#maxCostPerDay - this.#cost),\n resetAt: new Date(resetAt).toISOString(),\n paused: this.#paused,\n });\n }\n\n /**\n * Force a reset. Used by tests + manual operator action.\n *\n * @stable\n */\n reset(): void {\n this.#tokens = 0;\n this.#cost = 0;\n this.#paused = false;\n this.#pausedReason = null;\n this.#ledger = [];\n this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);\n }\n\n #handleBreach(\n phase: ConsolidatorPhase,\n resource: 'tokens' | 'cost',\n actual: number,\n budget: number,\n ): void {\n if (this.#onExceed === 'throw') {\n throw new BudgetExceededError({ phase, resource, actual, budget });\n }\n if (this.#onExceed === 'pause') {\n this.#paused = true;\n this.#pausedReason = resource === 'tokens' ? 'tokens-exceeded' : 'cost-exceeded';\n }\n }\n\n #maybeReset(): void {\n const now = this.#now();\n if (this.#resetSemantics === 'sliding-24h') {\n // Rolling window: drop spends older than the trailing 24h and recompute\n // the totals from what remains, rather than zeroing on every check.\n const cutoff = now - DAY_MS;\n if (\n this.#ledger.length > 0 &&\n this.#ledger[0] !== undefined &&\n this.#ledger[0].at <= cutoff\n ) {\n this.#ledger = this.#ledger.filter((entry) => entry.at > cutoff);\n }\n this.#tokens = this.#ledger.reduce((sum, entry) => sum + entry.tokens, 0);\n this.#cost = this.#ledger.reduce((sum, entry) => sum + entry.cost, 0);\n // Auto-unpause once the window has dropped back under both ceilings.\n if (\n this.#paused &&\n this.#tokens <= this.#maxTokensPerDay &&\n this.#cost <= this.#maxCostPerDay\n ) {\n this.#paused = false;\n this.#pausedReason = null;\n }\n return;\n }\n // utc / local: zero the counters when the calendar bucket rolls over.\n const currentBucket = bucketStart(now, this.#resetSemantics);\n if (currentBucket !== this.#bucketStart) {\n this.#bucketStart = currentBucket;\n this.#tokens = 0;\n this.#cost = 0;\n this.#paused = false;\n this.#pausedReason = null;\n }\n }\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Compute the start of the current bucket (ms epoch) for the\n * supplied reset semantics. UTC midnight is the production default\n * per ADR-038.\n *\n * @internal\n */\nexport function bucketStart(now: number, semantics: 'utc' | 'local' | 'sliding-24h'): number {\n switch (semantics) {\n case 'utc': {\n const d = new Date(now);\n return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());\n }\n case 'local': {\n const d = new Date(now);\n return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();\n }\n case 'sliding-24h':\n return now;\n }\n}\n\n/**\n * Compute the next reset boundary (ms epoch).\n *\n * @internal\n */\nexport function nextBucketStart(\n bucket: number,\n semantics: 'utc' | 'local' | 'sliding-24h',\n): number {\n switch (semantics) {\n case 'utc':\n case 'local':\n return bucket + DAY_MS;\n case 'sliding-24h':\n return bucket + DAY_MS;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"budget.js","names":["#now","#resetSemantics","#onExceed","#logger","#maxTokensPerDay","#maxCostPerDay","#bucketStart","#paused","#tokens","#cost","#pausedReason","#maybeReset","#ledger","#handleBreach","#warnedThisWindow"],"sources":["../../src/consolidator/budget.ts"],"sourcesContent":["/**\n * Cost-budget enforcement for the consolidator. The tracker is a\n * pure in-memory state machine: it owns the day-bucketed counters\n * (token + cost), the UTC reset semantics, and the `onExceed`\n * dispatch.\n *\n * The tracker does not perform any I/O - operators surface the live\n * counters via `Consolidator.status()` and the persisted snapshot is\n * derived from `consolidator_runs` rows.\n *\n * @packageDocumentation\n */\n\nimport { BudgetExceededError } from './errors.js';\nimport type { ConsolidatorPhase, OnBudgetExceed } from './types.js';\n\n/**\n * @stable\n */\nexport interface BudgetSnapshot {\n readonly tokensUsedToday: number;\n readonly costUsedToday: number;\n readonly tokensRemaining: number;\n readonly costRemaining: number;\n readonly resetAt: string;\n readonly paused: boolean;\n}\n\n/**\n * @stable\n */\nexport interface BudgetCheck {\n readonly allowed: boolean;\n readonly reason?: 'paused' | 'tokens-exceeded' | 'cost-exceeded';\n}\n\n/**\n * @stable\n */\nexport interface BudgetTrackerOptions {\n readonly maxTokensPerDay: number;\n readonly maxCostPerDay: number;\n readonly onExceed: OnBudgetExceed;\n readonly resetSemantics: 'utc' | 'local' | 'sliding-24h';\n readonly now?: () => number;\n /**\n * Sink for the `onExceed: 'log'` WARN (memory-consolidation-02).\n * Defaults to `process.stderr`. One WARN per resource per budget\n * window - the shipped standard/full presets use `'log'`, so without\n * it a breached ceiling was completely silent.\n */\n readonly logger?: (message: string) => void;\n}\n\n/**\n * Per-instance budget tracker. The runtime creates one tracker per\n * consolidator and resets the day counters lazily on the next phase\n * invocation.\n *\n * @stable\n */\nexport class BudgetTracker {\n readonly #now: () => number;\n readonly #resetSemantics: 'utc' | 'local' | 'sliding-24h';\n readonly #onExceed: OnBudgetExceed;\n readonly #logger: (message: string) => void;\n #maxTokensPerDay: number;\n #maxCostPerDay: number;\n #bucketStart: number;\n #tokens = 0;\n #cost = 0;\n #paused = false;\n #pausedReason: 'tokens-exceeded' | 'cost-exceeded' | null = null;\n /** Resources already WARNed this window under `onExceed: 'log'` (memory-consolidation-02). */\n #warnedThisWindow = new Set<'tokens' | 'cost'>();\n /**\n * Timestamped spend ledger for `sliding-24h` only. `#maybeReset` trims it to\n * the trailing 24h window and recomputes `#tokens` / `#cost` from it, so the\n * counters reflect a true rolling window instead of being zeroed on every\n * check (MCON-3). Unused - and never appended to - under `utc` / `local`.\n */\n #ledger: Array<{ at: number; tokens: number; cost: number }> = [];\n\n constructor(opts: BudgetTrackerOptions) {\n this.#now = opts.now ?? Date.now;\n this.#resetSemantics = opts.resetSemantics;\n this.#onExceed = opts.onExceed;\n this.#logger =\n opts.logger ??\n ((message: string): void => {\n process.stderr.write(`${message}\\n`);\n });\n this.#maxTokensPerDay = opts.maxTokensPerDay;\n this.#maxCostPerDay = opts.maxCostPerDay;\n this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);\n }\n\n /**\n * Replace the active ceilings. Used by `Consolidator.setTier(...)`.\n *\n * @stable\n */\n reconfigure(opts: { maxTokensPerDay: number; maxCostPerDay: number }): void {\n this.#maxTokensPerDay = opts.maxTokensPerDay;\n this.#maxCostPerDay = opts.maxCostPerDay;\n if (this.#paused) {\n const tokensOk = this.#tokens <= this.#maxTokensPerDay;\n const costOk = this.#cost <= this.#maxCostPerDay;\n if (tokensOk && costOk) {\n this.#paused = false;\n this.#pausedReason = null;\n }\n }\n }\n\n /**\n * Return whether the supplied phase may run right now.\n *\n * @stable\n */\n precheck(phase: ConsolidatorPhase): BudgetCheck {\n this.#maybeReset();\n if (phase === 'light') return { allowed: true };\n if (this.#paused) {\n const reason = this.#pausedReason ?? 'cost-exceeded';\n return { allowed: false, reason };\n }\n if (this.#maxTokensPerDay <= 0) {\n return { allowed: false, reason: 'tokens-exceeded' };\n }\n if (this.#maxCostPerDay <= 0) {\n return { allowed: false, reason: 'cost-exceeded' };\n }\n return { allowed: true };\n }\n\n /**\n * Record consumption. Returns the post-record state - `paused` is\n * `true` when the spend pushed past a ceiling under\n * `onExceed: 'pause'`.\n *\n * Throws {@link BudgetExceededError} when the configured behaviour\n * is `'throw'`.\n *\n * @stable\n */\n record(args: { phase: ConsolidatorPhase; tokens: number; costUsd: number }): BudgetSnapshot {\n this.#maybeReset();\n const tokens = Math.max(0, args.tokens);\n const cost = Math.max(0, args.costUsd);\n if (this.#resetSemantics === 'sliding-24h') {\n this.#ledger.push({ at: this.#now(), tokens, cost });\n }\n this.#tokens += tokens;\n this.#cost += cost;\n if (this.#tokens > this.#maxTokensPerDay) {\n this.#handleBreach(args.phase, 'tokens', this.#tokens, this.#maxTokensPerDay);\n }\n if (this.#cost > this.#maxCostPerDay) {\n this.#handleBreach(args.phase, 'cost', this.#cost, this.#maxCostPerDay);\n }\n return this.snapshot();\n }\n\n /**\n * Read-only snapshot. Surfaced through `Consolidator.status()`.\n *\n * @stable\n */\n snapshot(): BudgetSnapshot {\n this.#maybeReset();\n // For the rolling window the \"reset\" is continuous - the boundary is when\n // the oldest in-window spend ages out (24h after it landed).\n const resetAt =\n this.#resetSemantics === 'sliding-24h'\n ? (this.#ledger[0]?.at ?? this.#now()) + DAY_MS\n : nextBucketStart(this.#bucketStart, this.#resetSemantics);\n return Object.freeze({\n tokensUsedToday: this.#tokens,\n costUsedToday: this.#cost,\n tokensRemaining: Math.max(0, this.#maxTokensPerDay - this.#tokens),\n costRemaining: Math.max(0, this.#maxCostPerDay - this.#cost),\n resetAt: new Date(resetAt).toISOString(),\n paused: this.#paused,\n });\n }\n\n /**\n * Force a reset. Used by tests + manual operator action.\n *\n * @stable\n */\n reset(): void {\n this.#tokens = 0;\n this.#cost = 0;\n this.#paused = false;\n this.#pausedReason = null;\n this.#ledger = [];\n this.#warnedThisWindow.clear();\n this.#bucketStart = bucketStart(this.#now(), this.#resetSemantics);\n }\n\n #handleBreach(\n phase: ConsolidatorPhase,\n resource: 'tokens' | 'cost',\n actual: number,\n budget: number,\n ): void {\n if (this.#onExceed === 'throw') {\n throw new BudgetExceededError({ phase, resource, actual, budget });\n }\n if (this.#onExceed === 'pause') {\n this.#paused = true;\n this.#pausedReason = resource === 'tokens' ? 'tokens-exceeded' : 'cost-exceeded';\n }\n if (this.#onExceed === 'log' && !this.#warnedThisWindow.has(resource)) {\n // memory-consolidation-02: the type doc always promised \"'log'\n // keeps running with a WARN\" - the WARN finally exists. Once per\n // resource per budget window, so a breached daily ceiling is\n // visible without flooding.\n this.#warnedThisWindow.add(resource);\n this.#logger(\n `[graphorin/memory] consolidator ${resource} budget exceeded in phase '${phase}': ` +\n `${resource === 'cost' ? `$${actual.toFixed(4)} > $${budget.toFixed(4)}` : `${actual} > ${budget}`} ` +\n `(onExceed: 'log' - continuing; switch to 'pause'/'throw' to enforce).`,\n );\n }\n }\n\n #maybeReset(): void {\n const now = this.#now();\n if (this.#resetSemantics === 'sliding-24h') {\n // Rolling window: drop spends older than the trailing 24h and recompute\n // the totals from what remains, rather than zeroing on every check.\n const cutoff = now - DAY_MS;\n if (\n this.#ledger.length > 0 &&\n this.#ledger[0] !== undefined &&\n this.#ledger[0].at <= cutoff\n ) {\n this.#ledger = this.#ledger.filter((entry) => entry.at > cutoff);\n }\n this.#tokens = this.#ledger.reduce((sum, entry) => sum + entry.tokens, 0);\n this.#cost = this.#ledger.reduce((sum, entry) => sum + entry.cost, 0);\n // Auto-unpause once the window has dropped back under both ceilings.\n if (\n this.#paused &&\n this.#tokens <= this.#maxTokensPerDay &&\n this.#cost <= this.#maxCostPerDay\n ) {\n this.#paused = false;\n this.#pausedReason = null;\n }\n if (this.#tokens <= this.#maxTokensPerDay && this.#cost <= this.#maxCostPerDay) {\n this.#warnedThisWindow.clear();\n }\n return;\n }\n // utc / local: zero the counters when the calendar bucket rolls over.\n const currentBucket = bucketStart(now, this.#resetSemantics);\n if (currentBucket !== this.#bucketStart) {\n this.#bucketStart = currentBucket;\n this.#tokens = 0;\n this.#cost = 0;\n this.#paused = false;\n this.#pausedReason = null;\n this.#warnedThisWindow.clear();\n }\n }\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Compute the start of the current bucket (ms epoch) for the\n * supplied reset semantics. UTC midnight is the production default\n * per ADR-038.\n *\n * @internal\n */\nexport function bucketStart(now: number, semantics: 'utc' | 'local' | 'sliding-24h'): number {\n switch (semantics) {\n case 'utc': {\n const d = new Date(now);\n return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());\n }\n case 'local': {\n const d = new Date(now);\n return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();\n }\n case 'sliding-24h':\n return now;\n }\n}\n\n/**\n * Compute the next reset boundary (ms epoch).\n *\n * @internal\n */\nexport function nextBucketStart(\n bucket: number,\n semantics: 'utc' | 'local' | 'sliding-24h',\n): number {\n switch (semantics) {\n case 'utc':\n case 'local':\n return bucket + DAY_MS;\n case 'sliding-24h':\n return bucket + DAY_MS;\n }\n}\n"],"mappings":"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"}
|
|
@@ -16,7 +16,7 @@ interface SalienceWeights {
|
|
|
16
16
|
*/
|
|
17
17
|
readonly importance: number;
|
|
18
18
|
/**
|
|
19
|
-
* Penalty applied to a **quarantined** fact (P1-4)
|
|
19
|
+
* Penalty applied to a **quarantined** fact (P1-4) - the explicit
|
|
20
20
|
* security-risk negative term. At the default `0.7`, a quarantined
|
|
21
21
|
* fact keeps only `0.3` of its retention, so it is evicted first under
|
|
22
22
|
* capacity pressure. Never a hard delete: the fact is archived,
|
|
@@ -24,11 +24,22 @@ interface SalienceWeights {
|
|
|
24
24
|
*/
|
|
25
25
|
readonly quarantine: number;
|
|
26
26
|
/**
|
|
27
|
-
* Mild penalty for a fact with non-first-party provenance (P1-4)
|
|
27
|
+
* Mild penalty for a fact with non-first-party provenance (P1-4) -
|
|
28
28
|
* e.g. `'tool'` / `'imported'` content that did not originate with the
|
|
29
29
|
* user. At the default `0.2` such a fact keeps `0.8` of its retention.
|
|
30
30
|
*/
|
|
31
31
|
readonly foreignProvenance: number;
|
|
32
|
+
/**
|
|
33
|
+
* Retrieval-frequency reinforcement (D3) - the use-it-or-lose-it
|
|
34
|
+
* signal. How strongly the monotonic access counter stretches
|
|
35
|
+
* retention: the factor is
|
|
36
|
+
* `1 + weight * min(1, log1p(count) / log1p(saturation))`, saturating
|
|
37
|
+
* at {@link ACCESS_REINFORCEMENT_SATURATION} accesses. At the default
|
|
38
|
+
* `0` the factor is exactly `1` - behaviour is byte-identical until an
|
|
39
|
+
* operator opts in (e.g. `0.3` ⇒ a heavily-used fact keeps up to 1.3x
|
|
40
|
+
* its retention). Optional so existing weight literals stay valid.
|
|
41
|
+
*/
|
|
42
|
+
readonly accessReinforcement?: number;
|
|
32
43
|
}
|
|
33
44
|
//#endregion
|
|
34
45
|
export { SalienceWeights };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Compute the retention score for a fact given its age, last-access
|
|
4
4
|
* recency, and accumulated `strength`. Larger `strength` means the
|
|
5
5
|
* fact has been accessed more often, so the retention curve flattens
|
|
6
|
-
*
|
|
6
|
+
* - `score = base * exp(-elapsedDays / (tau * strength))`.
|
|
7
7
|
*
|
|
8
8
|
* @stable
|
|
9
9
|
*/
|
|
@@ -14,7 +14,7 @@ function retention(args) {
|
|
|
14
14
|
return Math.exp(-elapsedDays / tau);
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Neutral importance used when a fact carries no importance hint
|
|
17
|
+
* Neutral importance used when a fact carries no importance hint -
|
|
18
18
|
* the midpoint of the `[0, 1]` range, so an unscored fact contributes
|
|
19
19
|
* an importance factor of exactly `1.0` (`salience === retention`).
|
|
20
20
|
*
|
|
@@ -22,6 +22,14 @@ function retention(args) {
|
|
|
22
22
|
*/
|
|
23
23
|
const NEUTRAL_IMPORTANCE = .5;
|
|
24
24
|
/**
|
|
25
|
+
* Access count at which retrieval-frequency reinforcement saturates
|
|
26
|
+
* (D3). `log1p`-scaled, so the first few accesses matter most and
|
|
27
|
+
* anything past this count contributes the full weight.
|
|
28
|
+
*
|
|
29
|
+
* @stable
|
|
30
|
+
*/
|
|
31
|
+
const ACCESS_REINFORCEMENT_SATURATION = 32;
|
|
32
|
+
/**
|
|
25
33
|
* Default {@link SalienceWeights}. Chosen so that an active,
|
|
26
34
|
* first-party, unscored fact has `salience === retention` (the X-1
|
|
27
35
|
* change is invisible until a fact carries an importance hint, is
|
|
@@ -32,7 +40,8 @@ const NEUTRAL_IMPORTANCE = .5;
|
|
|
32
40
|
const DEFAULT_SALIENCE_WEIGHTS = Object.freeze({
|
|
33
41
|
importance: .6,
|
|
34
42
|
quarantine: .7,
|
|
35
|
-
foreignProvenance: .2
|
|
43
|
+
foreignProvenance: .2,
|
|
44
|
+
accessReinforcement: 0
|
|
36
45
|
});
|
|
37
46
|
/**
|
|
38
47
|
* Multi-signal salience for capacity-bounded forgetting (X-1). Combines
|
|
@@ -45,7 +54,7 @@ const DEFAULT_SALIENCE_WEIGHTS = Object.freeze({
|
|
|
45
54
|
* ```
|
|
46
55
|
*
|
|
47
56
|
* Sold as **cost / staleness control, not accuracy**: it only orders
|
|
48
|
-
* what gets archived first when storage is bounded
|
|
57
|
+
* what gets archived first when storage is bounded - it never gates
|
|
49
58
|
* recall. With neutral importance, an active fact, and first-party
|
|
50
59
|
* provenance the factors are all `1`, so `salience === retention`.
|
|
51
60
|
*
|
|
@@ -63,7 +72,10 @@ function salience(args) {
|
|
|
63
72
|
const importance = clamp01(args.importance ?? NEUTRAL_IMPORTANCE);
|
|
64
73
|
const importanceFactor = Math.max(0, 1 + weights.importance * (importance - NEUTRAL_IMPORTANCE));
|
|
65
74
|
const securityFactor = args.quarantined ? Math.max(0, 1 - weights.quarantine) : args.foreignProvenance ? Math.max(0, 1 - weights.foreignProvenance) : 1;
|
|
66
|
-
|
|
75
|
+
const reinforcementWeight = weights.accessReinforcement ?? 0;
|
|
76
|
+
const count = Math.max(0, args.accessCount ?? 0);
|
|
77
|
+
const reinforcementFactor = reinforcementWeight === 0 ? 1 : 1 + reinforcementWeight * Math.min(1, Math.log1p(count) / Math.log1p(ACCESS_REINFORCEMENT_SATURATION));
|
|
78
|
+
return Math.max(0, base * importanceFactor * securityFactor * reinforcementFactor);
|
|
67
79
|
}
|
|
68
80
|
/**
|
|
69
81
|
* Capacity-bounded eviction selector (X-1). Given facts scored by
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decay.js","names":["DEFAULT_SALIENCE_WEIGHTS: SalienceWeights"],"sources":["../../src/consolidator/decay.ts"],"sourcesContent":["/**\n * Ebbinghaus retention curve for the light phase. Pure functions so\n * the formula is testable without touching storage.\n *\n * @packageDocumentation\n */\n\n/**\n * Default time constant for the retention curve. The plan calls for\n * `tau = 7` days; consumers may override per-consolidator via\n * `decayTauDays`.\n *\n * @stable\n */\nexport const DEFAULT_DECAY_TAU_DAYS = 7;\n\n/**\n * Default archive threshold — facts whose retention falls below this\n * value are soft-archived in the light phase. `0.05` matches the\n * documented forgetting policy for the memory system.\n *\n * @stable\n */\nexport const DEFAULT_DECAY_ARCHIVE_THRESHOLD = 0.05;\n\n/**\n * Compute the retention score for a fact given its age, last-access\n * recency, and accumulated `strength`. Larger `strength` means the\n * fact has been accessed more often, so the retention curve flattens\n * — `score = base * exp(-elapsedDays / (tau * strength))`.\n *\n * @stable\n */\nexport function retention(args: {\n /** Reference time (ms epoch). */\n readonly now: number;\n /** Last access timestamp (ms epoch); falls back to `createdAt` when null. */\n readonly lastAccessedAt: number | null;\n /** Creation timestamp (ms epoch). */\n readonly createdAt: number;\n /** Strength multiplier (default 1.0). */\n readonly strength: number;\n /** Time constant in days. */\n readonly tauDays: number;\n}): number {\n const reference = args.lastAccessedAt ?? args.createdAt;\n const elapsedMs = Math.max(0, args.now - reference);\n const elapsedDays = elapsedMs / (1000 * 60 * 60 * 24);\n const tau = Math.max(0.1, args.tauDays * Math.max(0.5, args.strength));\n return Math.exp(-elapsedDays / tau);\n}\n\n/**\n * `true` when the fact's retention curve has fallen below the\n * archive threshold.\n *\n * @stable\n */\nexport function shouldArchive(args: {\n readonly now: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n readonly strength: number;\n readonly tauDays: number;\n readonly archiveThreshold: number;\n}): boolean {\n const score = retention({\n now: args.now,\n lastAccessedAt: args.lastAccessedAt,\n createdAt: args.createdAt,\n strength: args.strength,\n tauDays: args.tauDays,\n });\n return score < args.archiveThreshold;\n}\n\n/**\n * Neutral importance used when a fact carries no importance hint —\n * the midpoint of the `[0, 1]` range, so an unscored fact contributes\n * an importance factor of exactly `1.0` (`salience === retention`).\n *\n * @stable\n */\nexport const NEUTRAL_IMPORTANCE = 0.5;\n\n/**\n * Tunable weights for the multi-signal {@link salience} score (X-1).\n * Each weight is the *magnitude* of the corresponding signal's pull on\n * the retention curve; all default to values chosen so the ordering is\n * sensible without ever inverting it.\n *\n * @stable\n */\nexport interface SalienceWeights {\n /**\n * How strongly importance (P1-2) stretches retention. At the default\n * `0.6`, importance `1.0` multiplies retention by `1.3` and importance\n * `0.0` by `0.7`; neutral importance leaves it unchanged.\n */\n readonly importance: number;\n /**\n * Penalty applied to a **quarantined** fact (P1-4) — the explicit\n * security-risk negative term. At the default `0.7`, a quarantined\n * fact keeps only `0.3` of its retention, so it is evicted first under\n * capacity pressure. Never a hard delete: the fact is archived,\n * recoverable, and quarantine still gates it out of recall meanwhile.\n */\n readonly quarantine: number;\n /**\n * Mild penalty for a fact with non-first-party provenance (P1-4) —\n * e.g. `'tool'` / `'imported'` content that did not originate with the\n * user. At the default `0.2` such a fact keeps `0.8` of its retention.\n */\n readonly foreignProvenance: number;\n}\n\n/**\n * Default {@link SalienceWeights}. Chosen so that an active,\n * first-party, unscored fact has `salience === retention` (the X-1\n * change is invisible until a fact carries an importance hint, is\n * quarantined, or has foreign provenance).\n *\n * @stable\n */\nexport const DEFAULT_SALIENCE_WEIGHTS: SalienceWeights = Object.freeze({\n importance: 0.6,\n quarantine: 0.7,\n foreignProvenance: 0.2,\n});\n\n/**\n * Multi-signal salience for capacity-bounded forgetting (X-1). Combines\n * the Ebbinghaus {@link retention} curve (temporal relevance + access\n * frequency via `strength`) with the P1-2 importance hint and a P1-4\n * security-risk negative term:\n *\n * ```\n * salience = retention × importanceFactor × securityFactor\n * ```\n *\n * Sold as **cost / staleness control, not accuracy**: it only orders\n * what gets archived first when storage is bounded — it never gates\n * recall. With neutral importance, an active fact, and first-party\n * provenance the factors are all `1`, so `salience === retention`.\n *\n * @stable\n */\nexport function salience(args: {\n readonly now: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n readonly strength: number;\n readonly tauDays: number;\n /** Importance hint in `[0, 1]`; `null` → {@link NEUTRAL_IMPORTANCE}. */\n readonly importance: number | null;\n /** P1-4: a quarantined fact gets the {@link SalienceWeights.quarantine} penalty. */\n readonly quarantined: boolean;\n /** P1-4: a non-first-party fact gets the {@link SalienceWeights.foreignProvenance} penalty. */\n readonly foreignProvenance: boolean;\n /** Defaults to {@link DEFAULT_SALIENCE_WEIGHTS}. */\n readonly weights?: SalienceWeights;\n}): number {\n const weights = args.weights ?? DEFAULT_SALIENCE_WEIGHTS;\n const base = retention({\n now: args.now,\n lastAccessedAt: args.lastAccessedAt,\n createdAt: args.createdAt,\n strength: args.strength,\n tauDays: args.tauDays,\n });\n const importance = clamp01(args.importance ?? NEUTRAL_IMPORTANCE);\n const importanceFactor = Math.max(0, 1 + weights.importance * (importance - NEUTRAL_IMPORTANCE));\n const securityFactor = args.quarantined\n ? Math.max(0, 1 - weights.quarantine)\n : args.foreignProvenance\n ? Math.max(0, 1 - weights.foreignProvenance)\n : 1;\n return Math.max(0, base * importanceFactor * securityFactor);\n}\n\n/**\n * Capacity-bounded eviction selector (X-1). Given facts scored by\n * {@link salience}, return the ids of the lowest-salience ones to\n * archive so that at most `capacity` remain. Pure and deterministic:\n * ties break by id so a given batch always evicts the same set.\n *\n * Returns `[]` when the batch already fits (`length <= capacity`).\n * `capacity <= 0` evicts the whole batch.\n *\n * @stable\n */\nexport function selectForCapacityEviction(\n scored: ReadonlyArray<{ readonly id: string; readonly salience: number }>,\n capacity: number,\n): ReadonlyArray<string> {\n const evictCount = scored.length - Math.max(0, capacity);\n if (evictCount <= 0) return [];\n return [...scored]\n .sort((a, b) => a.salience - b.salience || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))\n .slice(0, evictCount)\n .map((row) => row.id);\n}\n\nfunction clamp01(value: number): number {\n if (Number.isNaN(value)) return NEUTRAL_IMPORTANCE;\n return Math.min(1, Math.max(0, value));\n}\n"],"mappings":";;;;;;;;;AAiCA,SAAgB,UAAU,MAWf;CACT,MAAM,YAAY,KAAK,kBAAkB,KAAK;CAE9C,MAAM,cADY,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,IAClB,MAAO,KAAK,KAAK;CAClD,MAAM,MAAM,KAAK,IAAI,IAAK,KAAK,UAAU,KAAK,IAAI,IAAK,KAAK,SAAS,CAAC;AACtE,QAAO,KAAK,IAAI,CAAC,cAAc,IAAI;;;;;;;;;AAkCrC,MAAa,qBAAqB;;;;;;;;;AAyClC,MAAaA,2BAA4C,OAAO,OAAO;CACrE,YAAY;CACZ,YAAY;CACZ,mBAAmB;CACpB,CAAC;;;;;;;;;;;;;;;;;;AAmBF,SAAgB,SAAS,MAcd;CACT,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,OAAO,UAAU;EACrB,KAAK,KAAK;EACV,gBAAgB,KAAK;EACrB,WAAW,KAAK;EAChB,UAAU,KAAK;EACf,SAAS,KAAK;EACf,CAAC;CACF,MAAM,aAAa,QAAQ,KAAK,cAAc,mBAAmB;CACjE,MAAM,mBAAmB,KAAK,IAAI,GAAG,IAAI,QAAQ,cAAc,aAAa,oBAAoB;CAChG,MAAM,iBAAiB,KAAK,cACxB,KAAK,IAAI,GAAG,IAAI,QAAQ,WAAW,GACnC,KAAK,oBACH,KAAK,IAAI,GAAG,IAAI,QAAQ,kBAAkB,GAC1C;AACN,QAAO,KAAK,IAAI,GAAG,OAAO,mBAAmB,eAAe;;;;;;;;;;;;;AAc9D,SAAgB,0BACd,QACA,UACuB;CACvB,MAAM,aAAa,OAAO,SAAS,KAAK,IAAI,GAAG,SAAS;AACxD,KAAI,cAAc,EAAG,QAAO,EAAE;AAC9B,QAAO,CAAC,GAAG,OAAO,CACf,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,CACnF,MAAM,GAAG,WAAW,CACpB,KAAK,QAAQ,IAAI,GAAG;;AAGzB,SAAS,QAAQ,OAAuB;AACtC,KAAI,OAAO,MAAM,MAAM,CAAE,QAAO;AAChC,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC"}
|
|
1
|
+
{"version":3,"file":"decay.js","names":["DEFAULT_SALIENCE_WEIGHTS: SalienceWeights"],"sources":["../../src/consolidator/decay.ts"],"sourcesContent":["/**\n * Ebbinghaus retention curve for the light phase. Pure functions so\n * the formula is testable without touching storage.\n *\n * @packageDocumentation\n */\n\n/**\n * Default time constant for the retention curve. The plan calls for\n * `tau = 7` days; consumers may override per-consolidator via\n * `decayTauDays`.\n *\n * @stable\n */\nexport const DEFAULT_DECAY_TAU_DAYS = 7;\n\n/**\n * Default archive threshold - facts whose retention falls below this\n * value are soft-archived in the light phase. `0.05` matches the\n * documented forgetting policy for the memory system.\n *\n * @stable\n */\nexport const DEFAULT_DECAY_ARCHIVE_THRESHOLD = 0.05;\n\n/**\n * Compute the retention score for a fact given its age, last-access\n * recency, and accumulated `strength`. Larger `strength` means the\n * fact has been accessed more often, so the retention curve flattens\n * - `score = base * exp(-elapsedDays / (tau * strength))`.\n *\n * @stable\n */\nexport function retention(args: {\n /** Reference time (ms epoch). */\n readonly now: number;\n /** Last access timestamp (ms epoch); falls back to `createdAt` when null. */\n readonly lastAccessedAt: number | null;\n /** Creation timestamp (ms epoch). */\n readonly createdAt: number;\n /** Strength multiplier (default 1.0). */\n readonly strength: number;\n /** Time constant in days. */\n readonly tauDays: number;\n}): number {\n const reference = args.lastAccessedAt ?? args.createdAt;\n const elapsedMs = Math.max(0, args.now - reference);\n const elapsedDays = elapsedMs / (1000 * 60 * 60 * 24);\n const tau = Math.max(0.1, args.tauDays * Math.max(0.5, args.strength));\n return Math.exp(-elapsedDays / tau);\n}\n\n/**\n * `true` when the fact's retention curve has fallen below the\n * archive threshold.\n *\n * @stable\n */\nexport function shouldArchive(args: {\n readonly now: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n readonly strength: number;\n readonly tauDays: number;\n readonly archiveThreshold: number;\n}): boolean {\n const score = retention({\n now: args.now,\n lastAccessedAt: args.lastAccessedAt,\n createdAt: args.createdAt,\n strength: args.strength,\n tauDays: args.tauDays,\n });\n return score < args.archiveThreshold;\n}\n\n/**\n * Neutral importance used when a fact carries no importance hint -\n * the midpoint of the `[0, 1]` range, so an unscored fact contributes\n * an importance factor of exactly `1.0` (`salience === retention`).\n *\n * @stable\n */\nexport const NEUTRAL_IMPORTANCE = 0.5;\n\n/**\n * Tunable weights for the multi-signal {@link salience} score (X-1).\n * Each weight is the *magnitude* of the corresponding signal's pull on\n * the retention curve; all default to values chosen so the ordering is\n * sensible without ever inverting it.\n *\n * @stable\n */\nexport interface SalienceWeights {\n /**\n * How strongly importance (P1-2) stretches retention. At the default\n * `0.6`, importance `1.0` multiplies retention by `1.3` and importance\n * `0.0` by `0.7`; neutral importance leaves it unchanged.\n */\n readonly importance: number;\n /**\n * Penalty applied to a **quarantined** fact (P1-4) - the explicit\n * security-risk negative term. At the default `0.7`, a quarantined\n * fact keeps only `0.3` of its retention, so it is evicted first under\n * capacity pressure. Never a hard delete: the fact is archived,\n * recoverable, and quarantine still gates it out of recall meanwhile.\n */\n readonly quarantine: number;\n /**\n * Mild penalty for a fact with non-first-party provenance (P1-4) -\n * e.g. `'tool'` / `'imported'` content that did not originate with the\n * user. At the default `0.2` such a fact keeps `0.8` of its retention.\n */\n readonly foreignProvenance: number;\n /**\n * Retrieval-frequency reinforcement (D3) - the use-it-or-lose-it\n * signal. How strongly the monotonic access counter stretches\n * retention: the factor is\n * `1 + weight * min(1, log1p(count) / log1p(saturation))`, saturating\n * at {@link ACCESS_REINFORCEMENT_SATURATION} accesses. At the default\n * `0` the factor is exactly `1` - behaviour is byte-identical until an\n * operator opts in (e.g. `0.3` ⇒ a heavily-used fact keeps up to 1.3x\n * its retention). Optional so existing weight literals stay valid.\n */\n readonly accessReinforcement?: number;\n}\n\n/**\n * Access count at which retrieval-frequency reinforcement saturates\n * (D3). `log1p`-scaled, so the first few accesses matter most and\n * anything past this count contributes the full weight.\n *\n * @stable\n */\nexport const ACCESS_REINFORCEMENT_SATURATION = 32;\n\n/**\n * Default {@link SalienceWeights}. Chosen so that an active,\n * first-party, unscored fact has `salience === retention` (the X-1\n * change is invisible until a fact carries an importance hint, is\n * quarantined, or has foreign provenance).\n *\n * @stable\n */\nexport const DEFAULT_SALIENCE_WEIGHTS: SalienceWeights = Object.freeze({\n importance: 0.6,\n quarantine: 0.7,\n foreignProvenance: 0.2,\n // D3: reinforcement is opt-in - the default weight 0 keeps salience\n // byte-identical for every fact regardless of its access count.\n accessReinforcement: 0,\n});\n\n/**\n * Multi-signal salience for capacity-bounded forgetting (X-1). Combines\n * the Ebbinghaus {@link retention} curve (temporal relevance + access\n * frequency via `strength`) with the P1-2 importance hint and a P1-4\n * security-risk negative term:\n *\n * ```\n * salience = retention × importanceFactor × securityFactor\n * ```\n *\n * Sold as **cost / staleness control, not accuracy**: it only orders\n * what gets archived first when storage is bounded - it never gates\n * recall. With neutral importance, an active fact, and first-party\n * provenance the factors are all `1`, so `salience === retention`.\n *\n * @stable\n */\nexport function salience(args: {\n readonly now: number;\n readonly lastAccessedAt: number | null;\n readonly createdAt: number;\n readonly strength: number;\n readonly tauDays: number;\n /** Importance hint in `[0, 1]`; `null` → {@link NEUTRAL_IMPORTANCE}. */\n readonly importance: number | null;\n /** P1-4: a quarantined fact gets the {@link SalienceWeights.quarantine} penalty. */\n readonly quarantined: boolean;\n /** P1-4: a non-first-party fact gets the {@link SalienceWeights.foreignProvenance} penalty. */\n readonly foreignProvenance: boolean;\n /**\n * Monotonic retrieval-access count (D3); `null` / absent ⇒ `0`.\n * Contributes only when {@link SalienceWeights.accessReinforcement}\n * is non-zero (the default `0` keeps salience unchanged).\n */\n readonly accessCount?: number | null;\n /** Defaults to {@link DEFAULT_SALIENCE_WEIGHTS}. */\n readonly weights?: SalienceWeights;\n}): number {\n const weights = args.weights ?? DEFAULT_SALIENCE_WEIGHTS;\n const base = retention({\n now: args.now,\n lastAccessedAt: args.lastAccessedAt,\n createdAt: args.createdAt,\n strength: args.strength,\n tauDays: args.tauDays,\n });\n const importance = clamp01(args.importance ?? NEUTRAL_IMPORTANCE);\n const importanceFactor = Math.max(0, 1 + weights.importance * (importance - NEUTRAL_IMPORTANCE));\n const securityFactor = args.quarantined\n ? Math.max(0, 1 - weights.quarantine)\n : args.foreignProvenance\n ? Math.max(0, 1 - weights.foreignProvenance)\n : 1;\n // D3: use-it-or-lose-it reinforcement. log1p-saturating in the access\n // count so early accesses matter most; identity at the default weight 0.\n const reinforcementWeight = weights.accessReinforcement ?? 0;\n const count = Math.max(0, args.accessCount ?? 0);\n const reinforcementFactor =\n reinforcementWeight === 0\n ? 1\n : 1 +\n reinforcementWeight *\n Math.min(1, Math.log1p(count) / Math.log1p(ACCESS_REINFORCEMENT_SATURATION));\n return Math.max(0, base * importanceFactor * securityFactor * reinforcementFactor);\n}\n\n/**\n * Capacity-bounded eviction selector (X-1). Given facts scored by\n * {@link salience}, return the ids of the lowest-salience ones to\n * archive so that at most `capacity` remain. Pure and deterministic:\n * ties break by id so a given batch always evicts the same set.\n *\n * Returns `[]` when the batch already fits (`length <= capacity`).\n * `capacity <= 0` evicts the whole batch.\n *\n * @stable\n */\nexport function selectForCapacityEviction(\n scored: ReadonlyArray<{ readonly id: string; readonly salience: number }>,\n capacity: number,\n): ReadonlyArray<string> {\n const evictCount = scored.length - Math.max(0, capacity);\n if (evictCount <= 0) return [];\n return [...scored]\n .sort((a, b) => a.salience - b.salience || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))\n .slice(0, evictCount)\n .map((row) => row.id);\n}\n\nfunction clamp01(value: number): number {\n if (Number.isNaN(value)) return NEUTRAL_IMPORTANCE;\n return Math.min(1, Math.max(0, value));\n}\n"],"mappings":";;;;;;;;;AAiCA,SAAgB,UAAU,MAWf;CACT,MAAM,YAAY,KAAK,kBAAkB,KAAK;CAE9C,MAAM,cADY,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,IAClB,MAAO,KAAK,KAAK;CAClD,MAAM,MAAM,KAAK,IAAI,IAAK,KAAK,UAAU,KAAK,IAAI,IAAK,KAAK,SAAS,CAAC;AACtE,QAAO,KAAK,IAAI,CAAC,cAAc,IAAI;;;;;;;;;AAkCrC,MAAa,qBAAqB;;;;;;;;AAmDlC,MAAa,kCAAkC;;;;;;;;;AAU/C,MAAaA,2BAA4C,OAAO,OAAO;CACrE,YAAY;CACZ,YAAY;CACZ,mBAAmB;CAGnB,qBAAqB;CACtB,CAAC;;;;;;;;;;;;;;;;;;AAmBF,SAAgB,SAAS,MAoBd;CACT,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,OAAO,UAAU;EACrB,KAAK,KAAK;EACV,gBAAgB,KAAK;EACrB,WAAW,KAAK;EAChB,UAAU,KAAK;EACf,SAAS,KAAK;EACf,CAAC;CACF,MAAM,aAAa,QAAQ,KAAK,cAAc,mBAAmB;CACjE,MAAM,mBAAmB,KAAK,IAAI,GAAG,IAAI,QAAQ,cAAc,aAAa,oBAAoB;CAChG,MAAM,iBAAiB,KAAK,cACxB,KAAK,IAAI,GAAG,IAAI,QAAQ,WAAW,GACnC,KAAK,oBACH,KAAK,IAAI,GAAG,IAAI,QAAQ,kBAAkB,GAC1C;CAGN,MAAM,sBAAsB,QAAQ,uBAAuB;CAC3D,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,eAAe,EAAE;CAChD,MAAM,sBACJ,wBAAwB,IACpB,IACA,IACA,sBACE,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,GAAG,KAAK,MAAM,gCAAgC,CAAC;AACpF,QAAO,KAAK,IAAI,GAAG,OAAO,mBAAmB,iBAAiB,oBAAoB;;;;;;;;;;;;;AAcpF,SAAgB,0BACd,QACA,UACuB;CACvB,MAAM,aAAa,OAAO,SAAS,KAAK,IAAI,GAAG,SAAS;AACxD,KAAI,cAAc,EAAG,QAAO,EAAE;AAC9B,QAAO,CAAC,GAAG,OAAO,CACf,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,CACnF,MAAM,GAAG,WAAW,CACpB,KAAK,QAAQ,IAAI,GAAG;;AAGzB,SAAS,QAAQ,OAAuB;AACtC,KAAI,OAAO,MAAM,MAAM,CAAE,QAAO;AAChC,QAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC"}
|
package/dist/consolidator/dlq.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/consolidator/dlq.ts
|
|
2
2
|
/**
|
|
3
|
-
* Dead-letter queue helpers
|
|
3
|
+
* Dead-letter queue helpers - pure functions used by the standard
|
|
4
4
|
* + deep phases. The actual persistence lives in the storage
|
|
5
5
|
* adapter's `enqueueFailedBatch / claimReadyBatches / ...` surface.
|
|
6
6
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dlq.js","names":[],"sources":["../../src/consolidator/dlq.ts"],"sourcesContent":["/**\n * Dead-letter queue helpers
|
|
1
|
+
{"version":3,"file":"dlq.js","names":[],"sources":["../../src/consolidator/dlq.ts"],"sourcesContent":["/**\n * Dead-letter queue helpers - pure functions used by the standard\n * + deep phases. The actual persistence lives in the storage\n * adapter's `enqueueFailedBatch / claimReadyBatches / ...` surface.\n *\n * @packageDocumentation\n */\n\n/**\n * Compute the next retry delay given the failure count + retry\n * policy. Implements full-jitter exponential backoff capped by\n * `maxBackoffMs`.\n *\n * The formula is `delay = random(0, min(maxBackoffMs, baseMs * 2^retryCount))`\n * where `random` is the optional `jitter` callback (defaults to\n * `Math.random()`).\n *\n * @stable\n */\nexport function nextBackoffMs(args: {\n readonly retryCount: number;\n readonly baseMs: number;\n readonly maxMs: number;\n readonly jitter?: () => number;\n}): number {\n const cap = Math.min(args.maxMs, args.baseMs * 2 ** Math.max(0, args.retryCount));\n const jitter = args.jitter ?? Math.random;\n return Math.max(0, Math.floor(cap * jitter()));\n}\n\n/**\n * Classify a thrown error into the lowercase `error_kind` value\n * recorded against `consolidator_failed_batches`. Used by the phases\n * so the DLQ row carries a stable discriminator instead of free-form\n * prose.\n *\n * @stable\n */\nexport function classifyError(err: unknown): string {\n if (err === null || err === undefined) return 'unknown';\n const message = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n if (message.includes('rate limit') || message.includes('429')) return 'rate_limit';\n if (message.includes('5xx') || message.includes('500') || message.includes('502')) return '5xx';\n if (message.includes('timeout')) return 'timeout';\n if (message.includes('embedder') || message.includes('embedding')) return 'embedder_failed';\n if (message.includes('invalid') || message.includes('parse')) return 'invalid_response';\n if (message.includes('budget')) return 'budget';\n return 'unknown';\n}\n\n/**\n * Best-effort error message extraction.\n *\n * @internal\n */\nexport function describeError(err: unknown): string {\n if (err === null || err === undefined) return 'unknown error';\n if (err instanceof Error) return err.message;\n if (typeof err === 'string') return err;\n return String(err);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,cAAc,MAKnB;CACT,MAAM,MAAM,KAAK,IAAI,KAAK,OAAO,KAAK,SAAS,KAAK,KAAK,IAAI,GAAG,KAAK,WAAW,CAAC;CACjF,MAAM,SAAS,KAAK,UAAU,KAAK;AACnC,QAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,QAAQ,CAAC,CAAC;;;;;;;;;;AAWhD,SAAgB,cAAc,KAAsB;AAClD,KAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;CAC9C,MAAM,UAAU,eAAe,QAAQ,IAAI,QAAQ,aAAa,GAAG,OAAO,IAAI,CAAC,aAAa;AAC5F,KAAI,QAAQ,SAAS,aAAa,IAAI,QAAQ,SAAS,MAAM,CAAE,QAAO;AACtE,KAAI,QAAQ,SAAS,MAAM,IAAI,QAAQ,SAAS,MAAM,IAAI,QAAQ,SAAS,MAAM,CAAE,QAAO;AAC1F,KAAI,QAAQ,SAAS,UAAU,CAAE,QAAO;AACxC,KAAI,QAAQ,SAAS,WAAW,IAAI,QAAQ,SAAS,YAAY,CAAE,QAAO;AAC1E,KAAI,QAAQ,SAAS,UAAU,IAAI,QAAQ,SAAS,QAAQ,CAAE,QAAO;AACrE,KAAI,QAAQ,SAAS,SAAS,CAAE,QAAO;AACvC,QAAO;;;;;;;AAQT,SAAgB,cAAc,KAAsB;AAClD,KAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAC9C,KAAI,eAAe,MAAO,QAAO,IAAI;AACrC,KAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAO,OAAO,IAAI"}
|
|
@@ -22,7 +22,7 @@ var BudgetExceededError = class extends GraphorinMemoryError {
|
|
|
22
22
|
actual;
|
|
23
23
|
resource;
|
|
24
24
|
constructor(args) {
|
|
25
|
-
super(`[graphorin/memory] consolidator budget exceeded
|
|
25
|
+
super(`[graphorin/memory] consolidator budget exceeded - phase=${args.phase} resource=${args.resource} actual=${args.actual} budget=${args.budget}`, { hint: "Pass `onExceed: \"log\"` to keep running, or upgrade the tier ceilings." });
|
|
26
26
|
this.phase = args.phase;
|
|
27
27
|
this.budget = args.budget;
|
|
28
28
|
this.actual = args.actual;
|
|
@@ -41,7 +41,7 @@ var CustomTierMisconfiguredError = class extends GraphorinMemoryError {
|
|
|
41
41
|
kind = "custom-tier-misconfigured";
|
|
42
42
|
missing;
|
|
43
43
|
constructor(missing) {
|
|
44
|
-
super(`[graphorin/memory] tier='custom' requires explicit ceilings
|
|
44
|
+
super(`[graphorin/memory] tier='custom' requires explicit ceilings - missing: ${missing.join(", ")}`, { hint: "Set `ceilings.maxTokensPerDay` + `ceilings.maxCostPerDay` (and `cheapModel` / `deepModel` if those phases are enabled)." });
|
|
45
45
|
this.missing = Object.freeze([...missing]);
|
|
46
46
|
}
|
|
47
47
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../../src/consolidator/errors.ts"],"sourcesContent":["/**\n * Typed errors raised by the Phase 10c consolidator runtime. Every\n * error carries a stable lowercase `kind` discriminator so downstream\n * tooling can pattern-match without parsing the prose.\n *\n * @packageDocumentation\n */\n\nimport { GraphorinMemoryError } from '../errors/index.js';\nimport type { ConsolidatorPhase } from './types.js';\n\n/**\n * Raised when the consolidator's daily budget envelope is exhausted\n * and `onExceed: 'throw'` is in effect.\n *\n * @stable\n */\nexport class BudgetExceededError extends GraphorinMemoryError {\n override readonly name = 'BudgetExceededError';\n readonly kind = 'budget-exceeded' as const;\n readonly phase: ConsolidatorPhase;\n readonly budget: number;\n readonly actual: number;\n readonly resource: 'tokens' | 'cost';\n\n constructor(args: {\n phase: ConsolidatorPhase;\n budget: number;\n actual: number;\n resource: 'tokens' | 'cost';\n }) {\n super(\n `[graphorin/memory] consolidator budget exceeded
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../../src/consolidator/errors.ts"],"sourcesContent":["/**\n * Typed errors raised by the Phase 10c consolidator runtime. Every\n * error carries a stable lowercase `kind` discriminator so downstream\n * tooling can pattern-match without parsing the prose.\n *\n * @packageDocumentation\n */\n\nimport { GraphorinMemoryError } from '../errors/index.js';\nimport type { ConsolidatorPhase } from './types.js';\n\n/**\n * Raised when the consolidator's daily budget envelope is exhausted\n * and `onExceed: 'throw'` is in effect.\n *\n * @stable\n */\nexport class BudgetExceededError extends GraphorinMemoryError {\n override readonly name = 'BudgetExceededError';\n readonly kind = 'budget-exceeded' as const;\n readonly phase: ConsolidatorPhase;\n readonly budget: number;\n readonly actual: number;\n readonly resource: 'tokens' | 'cost';\n\n constructor(args: {\n phase: ConsolidatorPhase;\n budget: number;\n actual: number;\n resource: 'tokens' | 'cost';\n }) {\n super(\n `[graphorin/memory] consolidator budget exceeded - phase=${args.phase} resource=${args.resource} actual=${args.actual} budget=${args.budget}`,\n { hint: 'Pass `onExceed: \"log\"` to keep running, or upgrade the tier ceilings.' },\n );\n this.phase = args.phase;\n this.budget = args.budget;\n this.actual = args.actual;\n this.resource = args.resource;\n }\n}\n\n/**\n * Raised when `tier: 'custom'` is selected without explicit\n * `ceilings.maxTokensPerDay` / `ceilings.maxCostPerDay`. Custom is\n * an explicit-only escape hatch.\n *\n * @stable\n */\nexport class CustomTierMisconfiguredError extends GraphorinMemoryError {\n override readonly name = 'CustomTierMisconfiguredError';\n readonly kind = 'custom-tier-misconfigured' as const;\n readonly missing: ReadonlyArray<string>;\n\n constructor(missing: ReadonlyArray<string>) {\n super(\n `[graphorin/memory] tier='custom' requires explicit ceilings - missing: ${missing.join(', ')}`,\n {\n hint: 'Set `ceilings.maxTokensPerDay` + `ceilings.maxCostPerDay` (and `cheapModel` / `deepModel` if those phases are enabled).',\n },\n );\n this.missing = Object.freeze([...missing]);\n }\n}\n\n/**\n * Raised when the standard / deep phase attempts an LLM call without\n * a configured provider. Surfaces as a typed failure that the DLQ\n * pipeline can attribute correctly.\n *\n * @stable\n */\nexport class ProviderNotConfiguredError extends GraphorinMemoryError {\n override readonly name = 'ProviderNotConfiguredError';\n readonly kind = 'provider-not-configured' as const;\n readonly phase: ConsolidatorPhase;\n\n constructor(phase: ConsolidatorPhase) {\n super(\n `[graphorin/memory] consolidator phase '${phase}' requires a provider but none was supplied.`,\n {\n hint: 'Pass `provider` to createConsolidator({...}) or pin tier=\"free\" to skip LLM phases.',\n },\n );\n this.phase = phase;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,IAAa,sBAAb,cAAyC,qBAAqB;CAC5D,AAAkB,OAAO;CACzB,AAAS,OAAO;CAChB,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAS;CAET,YAAY,MAKT;AACD,QACE,2DAA2D,KAAK,MAAM,YAAY,KAAK,SAAS,UAAU,KAAK,OAAO,UAAU,KAAK,UACrI,EAAE,MAAM,2EAAyE,CAClF;AACD,OAAK,QAAQ,KAAK;AAClB,OAAK,SAAS,KAAK;AACnB,OAAK,SAAS,KAAK;AACnB,OAAK,WAAW,KAAK;;;;;;;;;;AAWzB,IAAa,+BAAb,cAAkD,qBAAqB;CACrE,AAAkB,OAAO;CACzB,AAAS,OAAO;CAChB,AAAS;CAET,YAAY,SAAgC;AAC1C,QACE,0EAA0E,QAAQ,KAAK,KAAK,IAC5F,EACE,MAAM,2HACP,CACF;AACD,OAAK,UAAU,OAAO,OAAO,CAAC,GAAG,QAAQ,CAAC;;;;;;;;;;AAW9C,IAAa,6BAAb,cAAgD,qBAAqB;CACnE,AAAkB,OAAO;CACzB,AAAS,OAAO;CAChB,AAAS;CAET,YAAY,OAA0B;AACpC,QACE,0CAA0C,MAAM,+CAChD,EACE,MAAM,yFACP,CACF;AACD,OAAK,QAAQ"}
|
|
@@ -16,23 +16,23 @@ import { createConsolidator } from "./runtime.js";
|
|
|
16
16
|
|
|
17
17
|
//#region src/consolidator/index.ts
|
|
18
18
|
/**
|
|
19
|
-
* `@graphorin/memory/consolidator`
|
|
19
|
+
* `@graphorin/memory/consolidator` - Phase 10c runtime that turns
|
|
20
20
|
* raw conversation turns into long-lived facts and episodes. The
|
|
21
21
|
* module ships:
|
|
22
22
|
*
|
|
23
|
-
* - {@link createConsolidator}
|
|
23
|
+
* - {@link createConsolidator} - the production factory that wires
|
|
24
24
|
* triggers, the budget envelope, the wait-then-defer lock, the
|
|
25
25
|
* idempotent cursor, the DLQ, and the three phases (light /
|
|
26
26
|
* standard / deep) into a single `Consolidator` handle.
|
|
27
|
-
* - {@link createConsolidatorPlaceholder}
|
|
27
|
+
* - {@link createConsolidatorPlaceholder} - kept for back-compat
|
|
28
28
|
* with consumers that wired the no-op stub during Phase 10a; it
|
|
29
29
|
* honours the same shape but does no background work.
|
|
30
30
|
* - Pure helpers (`noise-filter`, `decay`, `budget`, `lock`, `dlq`,
|
|
31
|
-
* `idempotency`, `triggers`) and the per-phase orchestrators
|
|
31
|
+
* `idempotency`, `triggers`) and the per-phase orchestrators -
|
|
32
32
|
* exported so tests + the standalone server (Phase 14) can compose
|
|
33
33
|
* them without re-implementing the algorithm.
|
|
34
34
|
*
|
|
35
|
-
* The `Consolidator` interface is **stable**
|
|
35
|
+
* The `Consolidator` interface is **stable** - Phase 14 wires the
|
|
36
36
|
* runtime into the lifecycle hooks, and Phase 15 surfaces every
|
|
37
37
|
* method through `graphorin consolidator …` CLI commands.
|
|
38
38
|
*
|
|
@@ -86,7 +86,9 @@ function createConsolidatorPlaceholder(options = {}) {
|
|
|
86
86
|
reflection: false,
|
|
87
87
|
importanceThreshold: 3,
|
|
88
88
|
reflectionMaxQuestions: 3,
|
|
89
|
-
contextualRetrieval: "late-chunk"
|
|
89
|
+
contextualRetrieval: "late-chunk",
|
|
90
|
+
learnedContext: false,
|
|
91
|
+
learnedContextMaxChars: 1200
|
|
90
92
|
});
|
|
91
93
|
const status = async () => Object.freeze({
|
|
92
94
|
tier,
|