@graphorin/memory 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -1
- package/README.md +54 -37
- 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 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- 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/package.js +6 -0
- package/dist/package.js.map +1 -0
- 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 +3 -3
- 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":"standard.js","names":["rawBatch: ReadonlyArray<SessionMessageRecord>","decision: ReconcileDecision","audited: ConflictDecision","candidateId: string","indexText: string | undefined","startedAt","parsed: unknown","out: ExtractedFact[]","text","fact: ExtractedFact"],"sources":["../../../src/consolidator/phases/standard.ts"],"sourcesContent":["/**\n * Standard phase — extracts new facts from session messages via the\n * configured cheap-tier provider. The phase is gated by the active\n * tier (DEC-144 / ADR-038) and the budget tracker; it never runs\n * under `tier: 'free'`.\n *\n * The extraction prompt asks the model to return a JSON array of\n * `{ text, subject?, predicate?, object?, confidence? }` objects;\n * malformed responses are tolerated (counted as `emptyExtractions`)\n * so a flaky cheap model never wedges the pipeline.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest, SessionScope, Tracer } from '@graphorin/core';\nimport {\n type ConflictDecision,\n type ReconcileDecision,\n reconcileToConflictDecision,\n} from '../../conflict/index.js';\nimport {\n type ContextualRetrievalMode,\n contextualizeWithLlm,\n} from '../../internal/contextualize.js';\nimport { newMemoryId } from '../../internal/id.js';\nimport { withMemorySpan } from '../../internal/spans.js';\nimport type {\n ConsolidatorMemoryStoreExt,\n MemoryStoreAdapter,\n SessionMessageRecord,\n} from '../../internal/storage-adapter.js';\nimport type { EpisodicMemory } from '../../tiers/episodic-memory.js';\nimport type { FactInput, SemanticMemory } from '../../tiers/semantic-memory.js';\nimport type { BudgetTracker } from '../budget.js';\nimport { applyNoiseFilters, type NoiseFilterPreset, renderText } from '../noise-filter.js';\nimport { preFilterCandidate, reconcileCandidate } from '../reconcile.js';\nimport type { PhaseOutcome } from '../types.js';\n\n/** Top-s nearest neighbours surfaced to the reconcile pre-filter + LLM. */\nconst RECONCILE_TOP_K = 10;\n\n/** Inputs accepted by {@link runStandardPhase}. */\nexport interface StandardPhaseDeps {\n readonly semantic: SemanticMemory;\n /**\n * Episodic tier used for auto-episode-formation (P1-2). Omitted /\n * `null` ⇒ episode formation is skipped regardless of `formEpisodes`.\n */\n readonly episodic?: EpisodicMemory | null;\n /** Auto-form a quarantined episode per processed slice (P1-2). */\n readonly formEpisodes: boolean;\n /** Ask the episode summary call for a `[1, 10]` importance score (P1-2). */\n readonly importanceScoring: boolean;\n /**\n * Auto-promotion policy (MCON-2). When `true`, an injection-clean extraction\n * fact is admitted `active` instead of quarantined. Injection-flagged facts\n * always stay quarantined. Off by default.\n */\n readonly autoPromoteExtraction: boolean;\n /**\n * Contextual-retrieval mode for additive fact writes (P1-3). `'llm'`\n * spends one budgeted cheap-model call per `add` to author a situating\n * prefix; `'late-chunk'` / `'off'` defer to the shared\n * {@link SemanticMemory} instance (no extra call here).\n */\n readonly contextualRetrieval: ContextualRetrievalMode;\n readonly store: MemoryStoreAdapter;\n readonly consolidatorStore: ConsolidatorMemoryStoreExt | null;\n readonly provider: Provider;\n readonly tracer: Tracer;\n readonly scope: SessionScope;\n readonly cheapModel: string | null;\n readonly noiseFilters: ReadonlyArray<NoiseFilterPreset>;\n readonly maxBatchSize: number;\n readonly lastProcessedMessageId: string | null;\n readonly budget: BudgetTracker;\n /** Computes USD cost for the recorded usage. Defaults to 0 when omitted. */\n readonly priceUsage?: (usage: { promptTokens: number; completionTokens: number }) => number;\n readonly tier: 'cheap' | 'standard' | 'full' | 'custom';\n /** Override the wall clock — used by tests + the runtime clock seam. */\n readonly now?: () => number;\n /**\n * Pre-fetched message batch — when supplied, the phase skips its\n * own `listMessagesSince(...)` call and operates on the supplied\n * rows. The runtime always pre-fetches the batch so the cursor\n * advance can use the same data the phase processed.\n */\n readonly batch?: ReadonlyArray<SessionMessageRecord>;\n}\n\ninterface ExtractedFact {\n readonly text: string;\n readonly subject?: string;\n readonly predicate?: string;\n readonly object?: string;\n readonly confidence?: number;\n /** Raw `[1, 10]` importance as returned by the model, pre-normalization (MCON-12). */\n readonly importance?: number;\n}\n\n/** Parsed episode-summary payload (P1-2). */\ninterface ExtractedEpisode {\n readonly summary: string;\n /** Raw `[1, 10]` importance as returned by the model, pre-normalization. */\n readonly importance?: number;\n}\n\nconst EXTRACTION_SYSTEM_PROMPT = [\n 'You are a memory-extraction assistant for a long-running personal-assistant runtime.',\n 'Read the supplied conversation slice and return the durable facts it asserts about the user, the world, or stable preferences.',\n 'Skip greetings, banter, transient state, and anything the assistant produced as boilerplate.',\n 'For each fact also rate how important it is for remembering the user, on an integer scale from 1 (incidental detail) to 10 (identity-defining).',\n 'Return a single JSON object: { \"facts\": [{ \"text\": string, \"subject\"?: string, \"predicate\"?: string, \"object\"?: string, \"confidence\"?: number, \"importance\"?: number }] }.',\n 'If the slice contains no durable facts, return { \"facts\": [] }.',\n].join(' ');\n\n/**\n * Per-call output-token ceilings (MCON-14). `BudgetTracker.record()`\n * only runs AFTER `provider.generate` returns, so without a request cap\n * a degenerate model response could blow through the daily ceiling in a\n * single call before `pause` can take effect. All consolidator output\n * shapes are small and well-defined; the caps are deliberately roomy.\n */\nconst EXTRACTION_MAX_TOKENS = 1024;\nconst EPISODE_MAX_TOKENS = 512;\n\nconst EPISODE_SUMMARY_SYSTEM_PROMPT = [\n 'You are an episode-summarization assistant for a long-running personal-assistant runtime.',\n 'Read the supplied conversation slice and write one concise third-person summary of what happened — the episode.',\n 'Return a single JSON object: { \"summary\": string }.',\n].join(' ');\n\nconst EPISODE_SUMMARY_IMPORTANCE_SYSTEM_PROMPT = [\n 'You are an episode-summarization assistant for a long-running personal-assistant runtime.',\n 'Read the supplied conversation slice and write one concise third-person summary of what happened — the episode.',\n 'Also rate how important / poignant this episode is for remembering the user, on an integer scale from 1 (mundane) to 10 (deeply significant).',\n 'Return a single JSON object: { \"summary\": string, \"importance\": number }.',\n].join(' ');\n\n/**\n * Run the standard phase. Returns a {@link PhaseOutcome}; errors are\n * surfaced via `status: 'failed'` + `errorMessage` so the caller can\n * land the batch in the DLQ.\n *\n * @stable\n */\nexport async function runStandardPhase(deps: StandardPhaseDeps): Promise<PhaseOutcome> {\n return withMemorySpan(\n deps.tracer,\n 'memory.consolidate.standard',\n deps.scope,\n {\n 'consolidator.phase': 'standard',\n 'consolidator.tier': deps.tier,\n 'consolidator.cheap_model': deps.cheapModel ?? deps.provider.modelId,\n },\n async (span) => {\n const startedAt = (typeof deps.now === 'function' ? deps.now : Date.now)();\n const session = deps.store.session;\n let rawBatch: ReadonlyArray<SessionMessageRecord>;\n if (deps.batch !== undefined) {\n rawBatch = deps.batch;\n } else {\n if (typeof session.listMessagesSince !== 'function') {\n span.setAttributes({\n 'consolidator.standard.skipped': 'no-cursor-support',\n 'consolidator.duration_ms': 0,\n 'consolidator.facts_extracted': 0,\n 'consolidator.budget_used_usd': 0,\n });\n return emptyOutcome('completed');\n }\n rawBatch = await session.listMessagesSince(\n deps.scope,\n deps.lastProcessedMessageId,\n deps.maxBatchSize,\n );\n }\n const filtered = applyNoiseFilters(\n rawBatch as ReadonlyArray<SessionMessageRecord>,\n deps.noiseFilters,\n );\n span.setAttributes({\n 'consolidator.standard.batch_size': rawBatch.length,\n 'consolidator.standard.kept_count': filtered.kept.length,\n 'consolidator.standard.noise_filtered': filtered.droppedCount,\n });\n if (filtered.kept.length === 0) {\n span.setAttributes({\n 'consolidator.duration_ms': Math.max(\n 0,\n (typeof deps.now === 'function' ? deps.now : Date.now)() - startedAt,\n ),\n 'consolidator.facts_extracted': 0,\n 'consolidator.budget_used_usd': 0,\n });\n return {\n ...emptyOutcome('completed'),\n noiseFilteredCount: filtered.droppedCount,\n };\n }\n\n const transcript = renderTranscript(filtered.kept);\n const request = buildRequest(deps, transcript);\n const response = await deps.provider.generate(request);\n const usage = response.usage;\n const cost =\n deps.priceUsage?.({\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n }) ?? 0;\n\n const facts = parseExtraction(response.text);\n let factsCreated = 0;\n let factsUpdated = 0;\n let conflictsResolved = 0;\n let reconcileTokens = 0;\n let reconcileCost = 0;\n let contextualTokens = 0;\n let contextualCost = 0;\n const extractionTokens =\n (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0) + (usage.reasoningTokens ?? 0);\n deps.budget.record({ phase: 'standard', tokens: extractionTokens, costUsd: cost });\n\n for (const fact of facts) {\n if (fact.text.trim().length === 0) continue;\n const input = buildFactInput(fact);\n\n // Neighbour-aware reconciliation (P0-3). A cheap, LLM-free pre-filter\n // (Stage 1 exact-dedup + Stage 2 embedding zones) classifies the\n // candidate against its nearest neighbours; only the ambiguous\n // CONFLICT-CHECK mid-zone spends a reconcile LLM call. With no\n // embedder configured `neighbors` is empty → every candidate is a\n // fresh `add`, preserving the pre-reconcile behaviour.\n const neighbors = await deps.semantic.neighbors(deps.scope, fact.text, {\n topK: RECONCILE_TOP_K,\n });\n const route = await preFilterCandidate(fact.text, neighbors);\n\n let decision: ReconcileDecision;\n let audited: ConflictDecision;\n if (route.route === 'reconcile' && !deps.budget.snapshot().paused) {\n const result = await reconcileCandidate({\n candidateText: fact.text,\n neighbors: neighbors.map((h) => ({\n id: h.record.id,\n text: h.record.text,\n ...(h.record.validFrom !== undefined ? { validFrom: h.record.validFrom } : {}),\n })),\n provider: deps.provider,\n scope: deps.scope,\n });\n decision = result.decision;\n audited = reconcileToConflictDecision(decision);\n reconcileTokens += result.usage.totalTokens;\n const callCost =\n deps.priceUsage?.({\n promptTokens: result.usage.promptTokens,\n completionTokens: result.usage.completionTokens,\n }) ?? 0;\n reconcileCost += callCost;\n deps.budget.record({\n phase: 'standard',\n tokens: result.usage.totalTokens,\n costUsd: callCost,\n });\n } else if (route.route === 'reconcile') {\n // Mid-zone but the budget is exhausted — fall back to a safe\n // additive write rather than spending an over-budget LLM call.\n decision = { action: 'add', reason: 'reconcile-budget-paused' };\n audited = { kind: 'admit', stage: 'defer-to-deep', reason: 'reconcile-budget-paused' };\n } else if (route.route === 'noop') {\n decision = { action: 'noop', targetId: route.targetId, reason: route.reason };\n audited = {\n kind: 'dedup',\n stage: route.reason === 'exact-hash-match' ? 'exact-dedup' : 'embedding-three-zone',\n existingId: route.targetId,\n ...(route.similarity !== undefined ? { similarity: route.similarity } : {}),\n reason: route.reason,\n };\n } else {\n decision = { action: 'add', reason: route.reason };\n audited = { kind: 'admit', stage: 'embedding-three-zone', reason: route.reason };\n }\n\n // Apply the decision. Updates / conflicts route through the\n // bi-temporal supersede (close the old interval, insert the new) —\n // never a destructive delete (P0-3).\n let candidateId: string;\n switch (decision.action) {\n case 'add': {\n // Contextual retrieval (P1-3). In `'llm'` mode (and only when\n // the budget is not exhausted) spend one cheap-model call to\n // author a situating prefix and pass it as the write's index\n // text; the helper degrades to the deterministic late-chunk\n // prefix on any failure. `'late-chunk'` / `'off'` add nothing\n // here — the shared SemanticMemory instance contextualizes the\n // write. This LLM call is the only contextualization that\n // touches a provider, keeping `'llm'` strictly consolidator-only.\n let indexText: string | undefined;\n if (deps.contextualRetrieval === 'llm' && !deps.budget.snapshot().paused) {\n const ctx = await contextualizeWithLlm(\n {\n text: input.text,\n ...(input.subject !== undefined ? { subject: input.subject } : {}),\n ...(input.predicate !== undefined ? { predicate: input.predicate } : {}),\n ...(input.object !== undefined ? { object: input.object } : {}),\n },\n deps.provider,\n );\n indexText = ctx.indexText;\n const ctxTokens =\n (ctx.usage.promptTokens ?? 0) +\n (ctx.usage.completionTokens ?? 0) +\n (ctx.usage.reasoningTokens ?? 0);\n const ctxCost =\n deps.priceUsage?.({\n promptTokens: ctx.usage.promptTokens,\n completionTokens: ctx.usage.completionTokens,\n }) ?? 0;\n contextualTokens += ctxTokens;\n contextualCost += ctxCost;\n deps.budget.record({ phase: 'standard', tokens: ctxTokens, costUsd: ctxCost });\n }\n // `pipeline: 'off'` — the standard phase has already made the\n // conflict decision; a second inline pass would re-search +\n // double-audit.\n const stored = await deps.semantic.remember(deps.scope, input, {\n pipeline: 'off',\n ...(indexText !== undefined ? { indexText } : {}),\n ...(deps.autoPromoteExtraction ? { autoPromoteSynthesized: true } : {}),\n });\n candidateId = stored.id;\n factsCreated += 1;\n break;\n }\n case 'noop': {\n // Duplicate of `targetId` — record the dedup, write nothing.\n candidateId = newMemoryId('fact');\n break;\n }\n case 'update': {\n const { new: stored } = await deps.semantic.supersede(\n deps.scope,\n decision.targetId,\n input,\n decision.reason,\n );\n candidateId = stored.id;\n factsUpdated += 1;\n break;\n }\n case 'conflict': {\n const { new: stored } = await deps.semantic.supersede(\n deps.scope,\n decision.targetId,\n input,\n decision.reason,\n );\n candidateId = stored.id;\n factsUpdated += 1;\n conflictsResolved += 1;\n break;\n }\n }\n\n // Every decision is auditable in `fact_conflicts` (best-effort —\n // a store without the conflict surface simply skips it).\n await recordConflictDecision(deps.store, deps.scope, candidateId, audited);\n }\n\n // Episode formation (P1-2). Summarize the processed slice into one\n // quarantined episode carrying an LLM importance score, so the\n // episodic triple-signal ranking (recency × relevance × importance)\n // runs on all three signals. Budget-gated + provenance-tagged\n // (P1-4): skipped when `formEpisodes` is off, no episodic tier is\n // wired, or the budget is exhausted — the slice still advanced the\n // cursor, so the phase degrades to fact-only behaviour.\n let episodesFormed = 0;\n let episodeTokens = 0;\n let episodeCost = 0;\n if (\n deps.formEpisodes &&\n deps.episodic != null &&\n filtered.kept.length > 0 &&\n !deps.budget.snapshot().paused\n ) {\n const episodeRes = await deps.provider.generate(\n buildEpisodeRequest(deps, transcript, deps.importanceScoring),\n );\n const epUsage = episodeRes.usage;\n episodeTokens =\n (epUsage.promptTokens ?? 0) +\n (epUsage.completionTokens ?? 0) +\n (epUsage.reasoningTokens ?? 0);\n episodeCost =\n deps.priceUsage?.({\n promptTokens: epUsage.promptTokens,\n completionTokens: epUsage.completionTokens,\n }) ?? 0;\n deps.budget.record({ phase: 'standard', tokens: episodeTokens, costUsd: episodeCost });\n\n const parsed = parseEpisode(episodeRes.text);\n if (parsed !== null && parsed.summary.trim().length > 0) {\n const importance = deps.importanceScoring\n ? normalizeImportance(parsed.importance)\n : undefined;\n const nowFallback = new Date(\n (typeof deps.now === 'function' ? deps.now : Date.now)(),\n ).toISOString();\n const startedAt = filtered.kept[0]?.createdAt ?? nowFallback;\n const endedAt = filtered.kept[filtered.kept.length - 1]?.createdAt ?? startedAt;\n await deps.episodic.record(deps.scope, {\n summary: parsed.summary,\n startedAt,\n endedAt,\n ...(importance !== undefined ? { importance } : {}),\n provenance: 'extraction',\n status: 'quarantined',\n });\n episodesFormed = 1;\n }\n }\n\n const totalTokens = extractionTokens + reconcileTokens + episodeTokens + contextualTokens;\n const totalCost = cost + reconcileCost + episodeCost + contextualCost;\n const snapshot = deps.budget.snapshot();\n\n span.setAttributes({\n 'consolidator.duration_ms': Math.max(\n 0,\n (typeof deps.now === 'function' ? deps.now : Date.now)() - startedAt,\n ),\n 'consolidator.facts_extracted': factsCreated,\n 'consolidator.budget_used_usd': totalCost,\n 'consolidator.standard.facts_extracted': factsCreated,\n 'consolidator.standard.facts_updated': factsUpdated,\n 'consolidator.standard.conflicts_resolved': conflictsResolved,\n 'consolidator.standard.episodes_formed': episodesFormed,\n 'consolidator.standard.reconcile_tokens': reconcileTokens,\n 'consolidator.standard.episode_tokens': episodeTokens,\n 'consolidator.standard.contextual_tokens': contextualTokens,\n 'consolidator.standard.tokens.input': usage.promptTokens,\n 'consolidator.standard.tokens.output': usage.completionTokens,\n 'consolidator.standard.cost.estimate.usd': totalCost,\n 'consolidator.budget.remaining.tokens': snapshot.tokensRemaining,\n 'consolidator.budget.remaining.usd': snapshot.costRemaining,\n 'consolidator.exceeded': snapshot.paused,\n });\n\n return {\n phase: 'standard',\n status: 'completed',\n factsCreated,\n factsUpdated,\n conflictsResolved,\n episodesFormed,\n insightsCreated: 0,\n noiseFilteredCount: filtered.droppedCount,\n emptyExtractions: facts.length === 0 ? 1 : 0,\n llmTokensUsed: totalTokens,\n llmCostUsd: totalCost,\n errorMessage: null,\n };\n },\n );\n}\n\nfunction buildRequest(deps: StandardPhaseDeps, transcript: string): ProviderRequest {\n return {\n messages: [\n {\n role: 'user',\n content: `Conversation slice:\\n${transcript}`,\n },\n ],\n systemMessage: EXTRACTION_SYSTEM_PROMPT,\n temperature: 0,\n maxTokens: EXTRACTION_MAX_TOKENS,\n metadata: {\n userId: deps.scope.userId,\n ...(deps.scope.sessionId !== undefined ? { sessionId: deps.scope.sessionId } : {}),\n ...(deps.scope.agentId !== undefined ? { agentId: deps.scope.agentId } : {}),\n },\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Build the episode-summarization request (P1-2). Uses the same\n * transcript + structured-output seam as {@link buildRequest}; the\n * system prompt switches on `withImportance` so a poignancy score is\n * only requested when importance scoring is enabled.\n */\nfunction buildEpisodeRequest(\n deps: StandardPhaseDeps,\n transcript: string,\n withImportance: boolean,\n): ProviderRequest {\n return {\n messages: [\n {\n role: 'user',\n content: `Conversation slice:\\n${transcript}`,\n },\n ],\n systemMessage: withImportance\n ? EPISODE_SUMMARY_IMPORTANCE_SYSTEM_PROMPT\n : EPISODE_SUMMARY_SYSTEM_PROMPT,\n temperature: 0,\n maxTokens: EPISODE_MAX_TOKENS,\n metadata: {\n userId: deps.scope.userId,\n ...(deps.scope.sessionId !== undefined ? { sessionId: deps.scope.sessionId } : {}),\n ...(deps.scope.agentId !== undefined ? { agentId: deps.scope.agentId } : {}),\n },\n outputType: { kind: 'structured' },\n };\n}\n\nfunction renderTranscript(messages: ReadonlyArray<SessionMessageRecord>): string {\n return messages\n .map((m) => {\n const role = m.message.role;\n const text = renderText(m.message);\n return `[${m.sequence}] ${role}: ${text}`;\n })\n .join('\\n');\n}\n\n/**\n * Parse the model output into the structured fact list. Tolerates\n * fenced blocks + trailing commentary so a chatty model does not\n * wedge the cursor.\n *\n * @internal\n */\nexport function parseExtraction(text: string | undefined): ReadonlyArray<ExtractedFact> {\n if (text === undefined || text.length === 0) return [];\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return [];\n let parsed: unknown;\n try {\n parsed = JSON.parse(candidate);\n } catch {\n const slice = sliceJsonObject(candidate);\n if (slice === null) return [];\n try {\n parsed = JSON.parse(slice);\n } catch {\n return [];\n }\n }\n if (parsed === null || typeof parsed !== 'object') return [];\n const root = parsed as { facts?: unknown };\n const facts = Array.isArray(root.facts) ? root.facts : Array.isArray(parsed) ? parsed : [];\n const out: ExtractedFact[] = [];\n for (const item of facts) {\n if (item === null || typeof item !== 'object') continue;\n const obj = item as Record<string, unknown>;\n const text = typeof obj.text === 'string' ? obj.text : null;\n if (text === null || text.trim().length === 0) continue;\n const fact: ExtractedFact = {\n text,\n ...(typeof obj.subject === 'string' ? { subject: obj.subject } : {}),\n ...(typeof obj.predicate === 'string' ? { predicate: obj.predicate } : {}),\n ...(typeof obj.object === 'string' ? { object: obj.object } : {}),\n ...(typeof obj.confidence === 'number' ? { confidence: obj.confidence } : {}),\n ...(typeof obj.importance === 'number' ? { importance: obj.importance } : {}),\n };\n out.push(fact);\n }\n return out;\n}\n\n/**\n * Parse the episode-summary model output into `{ summary, importance? }`\n * (P1-2). Tolerates fenced blocks + an optional `{ episode: {...} }`\n * wrapper; returns `null` when no usable `summary` string is present\n * (so an extraction-shaped `{ facts: [...] }` payload is rejected, not\n * mistaken for an episode).\n *\n * @internal\n */\nexport function parseEpisode(text: string | undefined): ExtractedEpisode | null {\n if (text === undefined || text.length === 0) return null;\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(candidate);\n } catch {\n const slice = sliceJsonObject(candidate);\n if (slice === null) return null;\n try {\n parsed = JSON.parse(slice);\n } catch {\n return null;\n }\n }\n if (parsed === null || typeof parsed !== 'object') return null;\n const root = parsed as Record<string, unknown>;\n const inner =\n typeof root.summary === 'string'\n ? root\n : root.episode !== null && typeof root.episode === 'object'\n ? (root.episode as Record<string, unknown>)\n : root;\n const summary = typeof inner.summary === 'string' ? inner.summary : null;\n if (summary === null || summary.trim().length === 0) return null;\n return {\n summary,\n ...(typeof inner.importance === 'number' ? { importance: inner.importance } : {}),\n };\n}\n\n/**\n * Normalize a raw `[1, 10]` poignancy score into the episodic\n * `importance` field's `[0, 1]` range (P1-2). Out-of-range values are\n * clamped to `[1, 10]` first (so `15 → 1.0`, `0 → 0.1`); non-finite /\n * missing scores return `undefined` so the episode is recorded without\n * an importance signal rather than a bogus one.\n *\n * @internal\n */\nexport function normalizeImportance(raw: number | undefined): number | undefined {\n if (typeof raw !== 'number' || !Number.isFinite(raw)) return undefined;\n const clamped = Math.min(10, Math.max(1, raw));\n return clamped / 10;\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction sliceJsonObject(text: string): string | null {\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start < 0 || end < start) return null;\n return text.slice(start, end + 1);\n}\n\n/**\n * Build the {@link FactInput} for an extracted candidate. P1-4:\n * distilled-from-transcript facts are synthesized memory — tagged\n * `extraction` so they land quarantined (excluded from action-driving\n * recall until validated).\n */\nfunction buildFactInput(fact: ExtractedFact): FactInput {\n // MCON-12: normalize the model's raw [1, 10] importance into the\n // [0.1, 1.0] store scale (same contract as episodes); non-finite raw\n // scores drop the signal rather than fabricating one.\n const importance = normalizeImportance(fact.importance);\n return {\n text: fact.text,\n provenance: 'extraction',\n ...(fact.subject !== undefined ? { subject: fact.subject } : {}),\n ...(fact.predicate !== undefined ? { predicate: fact.predicate } : {}),\n ...(fact.object !== undefined ? { object: fact.object } : {}),\n ...(fact.confidence !== undefined ? { confidence: fact.confidence } : {}),\n ...(importance !== undefined ? { importance } : {}),\n };\n}\n\n/**\n * Record a reconcile / pre-filter decision into `fact_conflicts` so every\n * write-path decision stays auditable (P0-3). Best-effort: a storage\n * adapter without the conflict surface simply skips it.\n */\nasync function recordConflictDecision(\n store: MemoryStoreAdapter,\n scope: SessionScope,\n candidateId: string,\n decision: ConflictDecision,\n): Promise<void> {\n const conflicts = store.conflicts;\n if (conflicts === undefined) return;\n await conflicts.recordDecision({\n scope,\n candidateId,\n decision: decision.kind,\n stage: decision.stage,\n ...(decision.kind === 'dedup' || decision.kind === 'supersede'\n ? { existingId: decision.existingId }\n : {}),\n ...(decision.kind === 'dedup' && decision.similarity !== undefined\n ? { similarity: decision.similarity }\n : {}),\n ...(decision.reason !== undefined ? { reason: decision.reason } : {}),\n detectedBy: 'reconcile',\n });\n}\n\nfunction emptyOutcome(status: PhaseOutcome['status']): PhaseOutcome {\n return {\n phase: 'standard',\n status,\n factsCreated: 0,\n factsUpdated: 0,\n conflictsResolved: 0,\n episodesFormed: 0,\n insightsCreated: 0,\n noiseFilteredCount: 0,\n emptyExtractions: 0,\n llmTokensUsed: 0,\n llmCostUsd: null,\n errorMessage: null,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"standard.js","names":["rawBatch: ReadonlyArray<SessionMessageRecord>","decision: ReconcileDecision","audited: ConflictDecision","candidateId: string","indexText: string | undefined","startedAt","parsed: unknown","out: ExtractedFact[]","text","fact: ExtractedFact"],"sources":["../../../src/consolidator/phases/standard.ts"],"sourcesContent":["/**\n * Standard phase - extracts new facts from session messages via the\n * configured cheap-tier provider. The phase is gated by the active\n * tier (DEC-144 / ADR-038) and the budget tracker; it never runs\n * under `tier: 'free'`.\n *\n * The extraction prompt asks the model to return a JSON array of\n * `{ text, subject?, predicate?, object?, confidence? }` objects;\n * malformed responses are tolerated (counted as `emptyExtractions`)\n * so a flaky cheap model never wedges the pipeline.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest, SessionScope, Tracer } from '@graphorin/core';\nimport {\n type ConflictDecision,\n type ReconcileDecision,\n reconcileToConflictDecision,\n} from '../../conflict/index.js';\nimport {\n type ContextualRetrievalMode,\n contextualizeWithLlm,\n} from '../../internal/contextualize.js';\nimport { newMemoryId } from '../../internal/id.js';\nimport { withMemorySpan } from '../../internal/spans.js';\nimport type {\n ConsolidatorMemoryStoreExt,\n MemoryStoreAdapter,\n SessionMessageRecord,\n} from '../../internal/storage-adapter.js';\nimport type { EpisodicMemory } from '../../tiers/episodic-memory.js';\nimport type { FactInput, SemanticMemory } from '../../tiers/semantic-memory.js';\nimport type { BudgetTracker } from '../budget.js';\nimport { applyNoiseFilters, type NoiseFilterPreset, renderText } from '../noise-filter.js';\nimport { preFilterCandidate, reconcileCandidate } from '../reconcile.js';\nimport type { PhaseOutcome } from '../types.js';\n\n/** Top-s nearest neighbours surfaced to the reconcile pre-filter + LLM. */\nconst RECONCILE_TOP_K = 10;\n\n/** Inputs accepted by {@link runStandardPhase}. */\nexport interface StandardPhaseDeps {\n readonly semantic: SemanticMemory;\n /**\n * Episodic tier used for auto-episode-formation (P1-2). Omitted /\n * `null` ⇒ episode formation is skipped regardless of `formEpisodes`.\n */\n readonly episodic?: EpisodicMemory | null;\n /** Auto-form a quarantined episode per processed slice (P1-2). */\n readonly formEpisodes: boolean;\n /** Ask the episode summary call for a `[1, 10]` importance score (P1-2). */\n readonly importanceScoring: boolean;\n /**\n * Auto-promotion policy (MCON-2). When `true`, an injection-clean extraction\n * fact is admitted `active` instead of quarantined. Injection-flagged facts\n * always stay quarantined. Off by default.\n */\n readonly autoPromoteExtraction: boolean;\n /**\n * Contextual-retrieval mode for additive fact writes (P1-3). `'llm'`\n * spends one budgeted cheap-model call per `add` to author a situating\n * prefix; `'late-chunk'` / `'off'` defer to the shared\n * {@link SemanticMemory} instance (no extra call here).\n */\n readonly contextualRetrieval: ContextualRetrievalMode;\n readonly store: MemoryStoreAdapter;\n readonly consolidatorStore: ConsolidatorMemoryStoreExt | null;\n readonly provider: Provider;\n readonly tracer: Tracer;\n readonly scope: SessionScope;\n readonly cheapModel: string | null;\n readonly noiseFilters: ReadonlyArray<NoiseFilterPreset>;\n readonly maxBatchSize: number;\n readonly lastProcessedMessageId: string | null;\n readonly budget: BudgetTracker;\n /** Computes USD cost for the recorded usage. Defaults to 0 when omitted. */\n readonly priceUsage?: (usage: { promptTokens: number; completionTokens: number }) => number;\n readonly tier: 'cheap' | 'standard' | 'full' | 'custom';\n /** Override the wall clock - used by tests + the runtime clock seam. */\n readonly now?: () => number;\n /**\n * Pre-fetched message batch - when supplied, the phase skips its\n * own `listMessagesSince(...)` call and operates on the supplied\n * rows. The runtime always pre-fetches the batch so the cursor\n * advance can use the same data the phase processed.\n */\n readonly batch?: ReadonlyArray<SessionMessageRecord>;\n}\n\ninterface ExtractedFact {\n readonly text: string;\n readonly subject?: string;\n readonly predicate?: string;\n readonly object?: string;\n readonly confidence?: number;\n /** Raw `[1, 10]` importance as returned by the model, pre-normalization (MCON-12). */\n readonly importance?: number;\n}\n\n/** Parsed episode-summary payload (P1-2). */\ninterface ExtractedEpisode {\n readonly summary: string;\n /** Raw `[1, 10]` importance as returned by the model, pre-normalization. */\n readonly importance?: number;\n}\n\nconst EXTRACTION_SYSTEM_PROMPT = [\n 'You are a memory-extraction assistant for a long-running personal-assistant runtime.',\n 'Read the supplied conversation slice and return the durable facts it asserts about the user, the world, or stable preferences.',\n 'Skip greetings, banter, transient state, and anything the assistant produced as boilerplate.',\n // C5 (memory-consolidation-08): decontextualization - proposition-\n // granular retrieval only works when each stored fact stands alone.\n 'Each fact text MUST be a self-contained proposition understandable with no surrounding context: resolve pronouns and ellipses to the named person or thing (\"she\" -> \"Maria\"), inline the concrete entities, and never write a fact that needs a neighbouring fact to make sense.',\n 'For each fact also rate how important it is for remembering the user, on an integer scale from 1 (incidental detail) to 10 (identity-defining).',\n 'Return a single JSON object: { \"facts\": [{ \"text\": string, \"subject\"?: string, \"predicate\"?: string, \"object\"?: string, \"confidence\"?: number, \"importance\"?: number }] }.',\n 'If the slice contains no durable facts, return { \"facts\": [] }.',\n].join(' ');\n\n/**\n * Per-call output-token ceilings (MCON-14). `BudgetTracker.record()`\n * only runs AFTER `provider.generate` returns, so without a request cap\n * a degenerate model response could blow through the daily ceiling in a\n * single call before `pause` can take effect. All consolidator output\n * shapes are small and well-defined; the caps are deliberately roomy.\n */\nconst EXTRACTION_MAX_TOKENS = 1024;\nconst EPISODE_MAX_TOKENS = 512;\n\nconst EPISODE_SUMMARY_SYSTEM_PROMPT = [\n 'You are an episode-summarization assistant for a long-running personal-assistant runtime.',\n 'Read the supplied conversation slice and write one concise third-person summary of what happened - the episode.',\n 'Return a single JSON object: { \"summary\": string }.',\n].join(' ');\n\nconst EPISODE_SUMMARY_IMPORTANCE_SYSTEM_PROMPT = [\n 'You are an episode-summarization assistant for a long-running personal-assistant runtime.',\n 'Read the supplied conversation slice and write one concise third-person summary of what happened - the episode.',\n 'Also rate how important / poignant this episode is for remembering the user, on an integer scale from 1 (mundane) to 10 (deeply significant).',\n 'Return a single JSON object: { \"summary\": string, \"importance\": number }.',\n].join(' ');\n\n/**\n * Run the standard phase. Returns a {@link PhaseOutcome}; errors are\n * surfaced via `status: 'failed'` + `errorMessage` so the caller can\n * land the batch in the DLQ.\n *\n * @stable\n */\nexport async function runStandardPhase(deps: StandardPhaseDeps): Promise<PhaseOutcome> {\n return withMemorySpan(\n deps.tracer,\n 'memory.consolidate.standard',\n deps.scope,\n {\n 'consolidator.phase': 'standard',\n 'consolidator.tier': deps.tier,\n 'consolidator.cheap_model': deps.cheapModel ?? deps.provider.modelId,\n },\n async (span) => {\n const startedAt = (typeof deps.now === 'function' ? deps.now : Date.now)();\n const session = deps.store.session;\n let rawBatch: ReadonlyArray<SessionMessageRecord>;\n if (deps.batch !== undefined) {\n rawBatch = deps.batch;\n } else {\n if (typeof session.listMessagesSince !== 'function') {\n span.setAttributes({\n 'consolidator.standard.skipped': 'no-cursor-support',\n 'consolidator.duration_ms': 0,\n 'consolidator.facts_extracted': 0,\n 'consolidator.budget_used_usd': 0,\n });\n return emptyOutcome('completed');\n }\n rawBatch = await session.listMessagesSince(\n deps.scope,\n deps.lastProcessedMessageId,\n deps.maxBatchSize,\n );\n }\n const filtered = applyNoiseFilters(\n rawBatch as ReadonlyArray<SessionMessageRecord>,\n deps.noiseFilters,\n );\n span.setAttributes({\n 'consolidator.standard.batch_size': rawBatch.length,\n 'consolidator.standard.kept_count': filtered.kept.length,\n 'consolidator.standard.noise_filtered': filtered.droppedCount,\n });\n if (filtered.kept.length === 0) {\n span.setAttributes({\n 'consolidator.duration_ms': Math.max(\n 0,\n (typeof deps.now === 'function' ? deps.now : Date.now)() - startedAt,\n ),\n 'consolidator.facts_extracted': 0,\n 'consolidator.budget_used_usd': 0,\n });\n return {\n ...emptyOutcome('completed'),\n noiseFilteredCount: filtered.droppedCount,\n };\n }\n\n const transcript = renderTranscript(filtered.kept);\n const request = buildRequest(deps, transcript);\n const response = await deps.provider.generate(request);\n const usage = response.usage;\n const cost =\n deps.priceUsage?.({\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n }) ?? 0;\n\n const facts = parseExtraction(response.text);\n let factsCreated = 0;\n let factsUpdated = 0;\n let conflictsResolved = 0;\n let reconcileTokens = 0;\n let reconcileCost = 0;\n let contextualTokens = 0;\n let contextualCost = 0;\n const extractionTokens =\n (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0) + (usage.reasoningTokens ?? 0);\n deps.budget.record({ phase: 'standard', tokens: extractionTokens, costUsd: cost });\n\n for (const fact of facts) {\n if (fact.text.trim().length === 0) continue;\n const input = buildFactInput(fact);\n\n // Neighbour-aware reconciliation (P0-3). A cheap, LLM-free pre-filter\n // (Stage 1 exact-dedup + Stage 2 embedding zones) classifies the\n // candidate against its nearest neighbours; only the ambiguous\n // CONFLICT-CHECK mid-zone spends a reconcile LLM call. With no\n // embedder configured `neighbors` is empty → every candidate is a\n // fresh `add`, preserving the pre-reconcile behaviour.\n const neighbors = await deps.semantic.neighbors(deps.scope, fact.text, {\n topK: RECONCILE_TOP_K,\n });\n let route = await preFilterCandidate(fact.text, neighbors);\n // memory-consolidation-07: without an embedder `neighbors` is\n // always empty, so dedup never fires and a DLQ replay (or plain\n // re-run) of a partially-committed slice re-writes every\n // already-committed fact verbatim. Guard with an\n // embedder-independent exact-text lookup: FTS top hits +\n // string equality. Skipped in the mainline (embedder) config,\n // where the KNN stages already catch exact duplicates.\n if (route.route !== 'noop' && neighbors.length === 0) {\n const exactId = await findExactTextDuplicate(deps.semantic, deps.scope, fact.text);\n if (exactId !== null) {\n route = { route: 'noop', targetId: exactId, reason: 'exact-text-fts' };\n }\n }\n\n let decision: ReconcileDecision;\n let audited: ConflictDecision;\n if (route.route === 'reconcile' && !deps.budget.snapshot().paused) {\n const result = await reconcileCandidate({\n candidateText: fact.text,\n neighbors: neighbors.map((h) => ({\n id: h.record.id,\n text: h.record.text,\n ...(h.record.validFrom !== undefined ? { validFrom: h.record.validFrom } : {}),\n })),\n provider: deps.provider,\n scope: deps.scope,\n });\n decision = result.decision;\n audited = reconcileToConflictDecision(decision);\n reconcileTokens += result.usage.totalTokens;\n const callCost =\n deps.priceUsage?.({\n promptTokens: result.usage.promptTokens,\n completionTokens: result.usage.completionTokens,\n }) ?? 0;\n reconcileCost += callCost;\n deps.budget.record({\n phase: 'standard',\n tokens: result.usage.totalTokens,\n costUsd: callCost,\n });\n } else if (route.route === 'reconcile') {\n // Mid-zone but the budget is exhausted - fall back to a safe\n // additive write rather than spending an over-budget LLM call.\n decision = { action: 'add', reason: 'reconcile-budget-paused' };\n audited = { kind: 'admit', stage: 'defer-to-deep', reason: 'reconcile-budget-paused' };\n } else if (route.route === 'noop') {\n decision = { action: 'noop', targetId: route.targetId, reason: route.reason };\n audited = {\n kind: 'dedup',\n stage:\n route.reason === 'exact-hash-match' || route.reason === 'exact-text-fts'\n ? 'exact-dedup'\n : 'embedding-three-zone',\n existingId: route.targetId,\n ...(route.similarity !== undefined ? { similarity: route.similarity } : {}),\n reason: route.reason,\n };\n } else {\n decision = { action: 'add', reason: route.reason };\n audited = { kind: 'admit', stage: 'embedding-three-zone', reason: route.reason };\n }\n\n // Apply the decision. Updates / conflicts route through the\n // bi-temporal supersede (close the old interval, insert the new) -\n // never a destructive delete (P0-3).\n let candidateId: string;\n switch (decision.action) {\n case 'add': {\n // Contextual retrieval (P1-3). In `'llm'` mode (and only when\n // the budget is not exhausted) spend one cheap-model call to\n // author a situating prefix and pass it as the write's index\n // text; the helper degrades to the deterministic late-chunk\n // prefix on any failure. `'late-chunk'` / `'off'` add nothing\n // here - the shared SemanticMemory instance contextualizes the\n // write. This LLM call is the only contextualization that\n // touches a provider, keeping `'llm'` strictly consolidator-only.\n let indexText: string | undefined;\n if (deps.contextualRetrieval === 'llm' && !deps.budget.snapshot().paused) {\n const ctx = await contextualizeWithLlm(\n {\n text: input.text,\n ...(input.subject !== undefined ? { subject: input.subject } : {}),\n ...(input.predicate !== undefined ? { predicate: input.predicate } : {}),\n ...(input.object !== undefined ? { object: input.object } : {}),\n },\n deps.provider,\n );\n indexText = ctx.indexText;\n const ctxTokens =\n (ctx.usage.promptTokens ?? 0) +\n (ctx.usage.completionTokens ?? 0) +\n (ctx.usage.reasoningTokens ?? 0);\n const ctxCost =\n deps.priceUsage?.({\n promptTokens: ctx.usage.promptTokens,\n completionTokens: ctx.usage.completionTokens,\n }) ?? 0;\n contextualTokens += ctxTokens;\n contextualCost += ctxCost;\n deps.budget.record({ phase: 'standard', tokens: ctxTokens, costUsd: ctxCost });\n }\n // `pipeline: 'off'` - the standard phase has already made the\n // conflict decision; a second inline pass would re-search +\n // double-audit.\n const stored = await deps.semantic.remember(deps.scope, input, {\n pipeline: 'off',\n ...(indexText !== undefined ? { indexText } : {}),\n ...(deps.autoPromoteExtraction ? { autoPromoteSynthesized: true } : {}),\n });\n candidateId = stored.id;\n factsCreated += 1;\n break;\n }\n case 'noop': {\n // Duplicate of `targetId` - record the dedup, write nothing.\n candidateId = newMemoryId('fact');\n break;\n }\n case 'update': {\n const { new: stored } = await deps.semantic.supersede(\n deps.scope,\n decision.targetId,\n input,\n decision.reason,\n );\n candidateId = stored.id;\n factsUpdated += 1;\n break;\n }\n case 'conflict': {\n const { new: stored } = await deps.semantic.supersede(\n deps.scope,\n decision.targetId,\n input,\n decision.reason,\n );\n candidateId = stored.id;\n factsUpdated += 1;\n conflictsResolved += 1;\n break;\n }\n }\n\n // Every decision is auditable in `fact_conflicts` (best-effort -\n // a store without the conflict surface simply skips it).\n await recordConflictDecision(deps.store, deps.scope, candidateId, audited);\n }\n\n // Episode formation (P1-2). Summarize the processed slice into one\n // quarantined episode carrying an LLM importance score, so the\n // episodic triple-signal ranking (recency × relevance × importance)\n // runs on all three signals. Budget-gated + provenance-tagged\n // (P1-4): skipped when `formEpisodes` is off, no episodic tier is\n // wired, or the budget is exhausted - the slice still advanced the\n // cursor, so the phase degrades to fact-only behaviour.\n let episodesFormed = 0;\n let episodeTokens = 0;\n let episodeCost = 0;\n if (\n deps.formEpisodes &&\n deps.episodic != null &&\n filtered.kept.length > 0 &&\n !deps.budget.snapshot().paused\n ) {\n const episodeRes = await deps.provider.generate(\n buildEpisodeRequest(deps, transcript, deps.importanceScoring),\n );\n const epUsage = episodeRes.usage;\n episodeTokens =\n (epUsage.promptTokens ?? 0) +\n (epUsage.completionTokens ?? 0) +\n (epUsage.reasoningTokens ?? 0);\n episodeCost =\n deps.priceUsage?.({\n promptTokens: epUsage.promptTokens,\n completionTokens: epUsage.completionTokens,\n }) ?? 0;\n deps.budget.record({ phase: 'standard', tokens: episodeTokens, costUsd: episodeCost });\n\n const parsed = parseEpisode(episodeRes.text);\n if (parsed !== null && parsed.summary.trim().length > 0) {\n const importance = deps.importanceScoring\n ? normalizeImportance(parsed.importance)\n : undefined;\n const nowFallback = new Date(\n (typeof deps.now === 'function' ? deps.now : Date.now)(),\n ).toISOString();\n const startedAt = filtered.kept[0]?.createdAt ?? nowFallback;\n const endedAt = filtered.kept[filtered.kept.length - 1]?.createdAt ?? startedAt;\n await deps.episodic.record(deps.scope, {\n summary: parsed.summary,\n startedAt,\n endedAt,\n ...(importance !== undefined ? { importance } : {}),\n provenance: 'extraction',\n status: 'quarantined',\n // D3: an auto-formed episode is the agent's own synthesis.\n owner: 'agent',\n });\n episodesFormed = 1;\n }\n }\n\n const totalTokens = extractionTokens + reconcileTokens + episodeTokens + contextualTokens;\n const totalCost = cost + reconcileCost + episodeCost + contextualCost;\n const snapshot = deps.budget.snapshot();\n\n span.setAttributes({\n 'consolidator.duration_ms': Math.max(\n 0,\n (typeof deps.now === 'function' ? deps.now : Date.now)() - startedAt,\n ),\n 'consolidator.facts_extracted': factsCreated,\n 'consolidator.budget_used_usd': totalCost,\n 'consolidator.standard.facts_extracted': factsCreated,\n 'consolidator.standard.facts_updated': factsUpdated,\n 'consolidator.standard.conflicts_resolved': conflictsResolved,\n 'consolidator.standard.episodes_formed': episodesFormed,\n 'consolidator.standard.reconcile_tokens': reconcileTokens,\n 'consolidator.standard.episode_tokens': episodeTokens,\n 'consolidator.standard.contextual_tokens': contextualTokens,\n 'consolidator.standard.tokens.input': usage.promptTokens,\n 'consolidator.standard.tokens.output': usage.completionTokens,\n 'consolidator.standard.cost.estimate.usd': totalCost,\n 'consolidator.budget.remaining.tokens': snapshot.tokensRemaining,\n 'consolidator.budget.remaining.usd': snapshot.costRemaining,\n 'consolidator.exceeded': snapshot.paused,\n });\n\n return {\n phase: 'standard',\n status: 'completed',\n factsCreated,\n factsUpdated,\n conflictsResolved,\n episodesFormed,\n insightsCreated: 0,\n noiseFilteredCount: filtered.droppedCount,\n emptyExtractions: facts.length === 0 ? 1 : 0,\n llmTokensUsed: totalTokens,\n llmCostUsd: totalCost,\n errorMessage: null,\n };\n },\n );\n}\n\nfunction buildRequest(deps: StandardPhaseDeps, transcript: string): ProviderRequest {\n // memory-consolidation-08: temporal anchoring - state today's date and\n // instruct the model to resolve relative time into absolute dates, so\n // \"I'm interviewing next Friday\" becomes a dated, durable fact.\n const today = new Date(deps.now?.() ?? Date.now()).toISOString().slice(0, 10);\n return {\n messages: [\n {\n role: 'user',\n content: `Conversation slice:\\n${transcript}`,\n },\n ],\n systemMessage:\n `${EXTRACTION_SYSTEM_PROMPT} Today is ${today}. Each transcript line carries its ` +\n 'timestamp in parentheses; resolve relative dates (\"next Friday\", \"last month\") ' +\n 'into absolute ISO dates in the extracted fact text.',\n temperature: 0,\n maxTokens: EXTRACTION_MAX_TOKENS,\n metadata: {\n userId: deps.scope.userId,\n ...(deps.scope.sessionId !== undefined ? { sessionId: deps.scope.sessionId } : {}),\n ...(deps.scope.agentId !== undefined ? { agentId: deps.scope.agentId } : {}),\n },\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Build the episode-summarization request (P1-2). Uses the same\n * transcript + structured-output seam as {@link buildRequest}; the\n * system prompt switches on `withImportance` so a poignancy score is\n * only requested when importance scoring is enabled.\n */\nfunction buildEpisodeRequest(\n deps: StandardPhaseDeps,\n transcript: string,\n withImportance: boolean,\n): ProviderRequest {\n return {\n messages: [\n {\n role: 'user',\n content: `Conversation slice:\\n${transcript}`,\n },\n ],\n systemMessage: withImportance\n ? EPISODE_SUMMARY_IMPORTANCE_SYSTEM_PROMPT\n : EPISODE_SUMMARY_SYSTEM_PROMPT,\n temperature: 0,\n maxTokens: EPISODE_MAX_TOKENS,\n metadata: {\n userId: deps.scope.userId,\n ...(deps.scope.sessionId !== undefined ? { sessionId: deps.scope.sessionId } : {}),\n ...(deps.scope.agentId !== undefined ? { agentId: deps.scope.agentId } : {}),\n },\n outputType: { kind: 'structured' },\n };\n}\n\nfunction renderTranscript(messages: ReadonlyArray<SessionMessageRecord>): string {\n return messages\n .map((m) => {\n const role = m.message.role;\n const text = renderText(m.message);\n // memory-consolidation-08: per-message timestamps anchor the\n // extraction - without them relative time (\"next Friday\", \"last\n // month\") distils into facts with no resolvable timeframe.\n const stamp = typeof m.createdAt === 'string' && m.createdAt.length > 0 ? m.createdAt : '';\n return stamp.length > 0\n ? `[${m.sequence}] (${stamp}) ${role}: ${text}`\n : `[${m.sequence}] ${role}: ${text}`;\n })\n .join('\\n');\n}\n\n/**\n * Parse the model output into the structured fact list. Tolerates\n * fenced blocks + trailing commentary so a chatty model does not\n * wedge the cursor.\n *\n * @internal\n */\nexport function parseExtraction(text: string | undefined): ReadonlyArray<ExtractedFact> {\n if (text === undefined || text.length === 0) return [];\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return [];\n let parsed: unknown;\n try {\n parsed = JSON.parse(candidate);\n } catch {\n const slice = sliceJsonObject(candidate);\n if (slice === null) return [];\n try {\n parsed = JSON.parse(slice);\n } catch {\n return [];\n }\n }\n if (parsed === null || typeof parsed !== 'object') return [];\n const root = parsed as { facts?: unknown };\n const facts = Array.isArray(root.facts) ? root.facts : Array.isArray(parsed) ? parsed : [];\n const out: ExtractedFact[] = [];\n for (const item of facts) {\n if (item === null || typeof item !== 'object') continue;\n const obj = item as Record<string, unknown>;\n const text = typeof obj.text === 'string' ? obj.text : null;\n if (text === null || text.trim().length === 0) continue;\n const fact: ExtractedFact = {\n text,\n ...(typeof obj.subject === 'string' ? { subject: obj.subject } : {}),\n ...(typeof obj.predicate === 'string' ? { predicate: obj.predicate } : {}),\n ...(typeof obj.object === 'string' ? { object: obj.object } : {}),\n ...(typeof obj.confidence === 'number' ? { confidence: obj.confidence } : {}),\n ...(typeof obj.importance === 'number' ? { importance: obj.importance } : {}),\n };\n out.push(fact);\n }\n return out;\n}\n\n/**\n * Parse the episode-summary model output into `{ summary, importance? }`\n * (P1-2). Tolerates fenced blocks + an optional `{ episode: {...} }`\n * wrapper; returns `null` when no usable `summary` string is present\n * (so an extraction-shaped `{ facts: [...] }` payload is rejected, not\n * mistaken for an episode).\n *\n * @internal\n */\nexport function parseEpisode(text: string | undefined): ExtractedEpisode | null {\n if (text === undefined || text.length === 0) return null;\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(candidate);\n } catch {\n const slice = sliceJsonObject(candidate);\n if (slice === null) return null;\n try {\n parsed = JSON.parse(slice);\n } catch {\n return null;\n }\n }\n if (parsed === null || typeof parsed !== 'object') return null;\n const root = parsed as Record<string, unknown>;\n const inner =\n typeof root.summary === 'string'\n ? root\n : root.episode !== null && typeof root.episode === 'object'\n ? (root.episode as Record<string, unknown>)\n : root;\n const summary = typeof inner.summary === 'string' ? inner.summary : null;\n if (summary === null || summary.trim().length === 0) return null;\n return {\n summary,\n ...(typeof inner.importance === 'number' ? { importance: inner.importance } : {}),\n };\n}\n\n/**\n * Normalize a raw `[1, 10]` poignancy score into the episodic\n * `importance` field's `[0, 1]` range (P1-2). Out-of-range values are\n * clamped to `[1, 10]` first (so `15 → 1.0`, `0 → 0.1`); non-finite /\n * missing scores return `undefined` so the episode is recorded without\n * an importance signal rather than a bogus one.\n *\n * @internal\n */\nexport function normalizeImportance(raw: number | undefined): number | undefined {\n if (typeof raw !== 'number' || !Number.isFinite(raw)) return undefined;\n const clamped = Math.min(10, Math.max(1, raw));\n return clamped / 10;\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction sliceJsonObject(text: string): string | null {\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start < 0 || end < start) return null;\n return text.slice(start, end + 1);\n}\n\n/**\n * Build the {@link FactInput} for an extracted candidate. P1-4:\n * distilled-from-transcript facts are synthesized memory - tagged\n * `extraction` so they land quarantined (excluded from action-driving\n * recall until validated).\n */\n/**\n * Embedder-independent exact-duplicate lookup\n * (memory-consolidation-07): FTS top hits + strict string equality.\n * Returns the existing fact's id or `null`. Failures are swallowed -\n * the guard is an optimization, never a reason to fail the slice.\n */\nasync function findExactTextDuplicate(\n semantic: SemanticMemory,\n scope: SessionScope,\n text: string,\n): Promise<string | null> {\n try {\n // Extraction output lands quarantined (P1-4) - the committed copy a\n // replay would duplicate is exactly a quarantined row, so the guard\n // must look past the retrieval gate.\n const hits = await semantic.search(scope, text, { topK: 5, includeQuarantined: true });\n return hits.find((h) => h.record.text === text)?.record.id ?? null;\n } catch {\n return null;\n }\n}\n\nfunction buildFactInput(fact: ExtractedFact): FactInput {\n // MCON-12: normalize the model's raw [1, 10] importance into the\n // [0.1, 1.0] store scale (same contract as episodes); non-finite raw\n // scores drop the signal rather than fabricating one.\n const importance = normalizeImportance(fact.importance);\n return {\n text: fact.text,\n provenance: 'extraction',\n // D3: an extracted fact is the agent's own inference about the user.\n owner: 'agent',\n ...(fact.subject !== undefined ? { subject: fact.subject } : {}),\n ...(fact.predicate !== undefined ? { predicate: fact.predicate } : {}),\n ...(fact.object !== undefined ? { object: fact.object } : {}),\n ...(fact.confidence !== undefined ? { confidence: fact.confidence } : {}),\n ...(importance !== undefined ? { importance } : {}),\n };\n}\n\n/**\n * Record a reconcile / pre-filter decision into `fact_conflicts` so every\n * write-path decision stays auditable (P0-3). Best-effort: a storage\n * adapter without the conflict surface simply skips it.\n */\nasync function recordConflictDecision(\n store: MemoryStoreAdapter,\n scope: SessionScope,\n candidateId: string,\n decision: ConflictDecision,\n): Promise<void> {\n const conflicts = store.conflicts;\n if (conflicts === undefined) return;\n await conflicts.recordDecision({\n scope,\n candidateId,\n decision: decision.kind,\n stage: decision.stage,\n ...(decision.kind === 'dedup' || decision.kind === 'supersede'\n ? { existingId: decision.existingId }\n : {}),\n ...(decision.kind === 'dedup' && decision.similarity !== undefined\n ? { similarity: decision.similarity }\n : {}),\n ...(decision.reason !== undefined ? { reason: decision.reason } : {}),\n detectedBy: 'reconcile',\n });\n}\n\nfunction emptyOutcome(status: PhaseOutcome['status']): PhaseOutcome {\n return {\n phase: 'standard',\n status,\n factsCreated: 0,\n factsUpdated: 0,\n conflictsResolved: 0,\n episodesFormed: 0,\n insightsCreated: 0,\n noiseFilteredCount: 0,\n emptyExtractions: 0,\n llmTokensUsed: 0,\n llmCostUsd: null,\n errorMessage: null,\n };\n}\n"],"mappings":"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"}
|
|
@@ -164,7 +164,7 @@ function buildReconcileRequest(args) {
|
|
|
164
164
|
}
|
|
165
165
|
/**
|
|
166
166
|
* Minimal candidate `Fact` for the pre-filter stage context. Stage 1
|
|
167
|
-
* reads only `text`; Stage 2 reads only the neighbour scores
|
|
167
|
+
* reads only `text`; Stage 2 reads only the neighbour scores - the
|
|
168
168
|
* other fields are never inspected, so a synthetic id/scope is safe.
|
|
169
169
|
*/
|
|
170
170
|
function synthCandidate(text) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.js","names":["ctx: StageContext","fallback: ReconcileDecision","parsed: unknown"],"sources":["../../src/consolidator/reconcile.ts"],"sourcesContent":["/**\n * Neighbour-aware write reconciliation (P0-3) — the `reconcile` half of\n * Mem0's extract→reconcile loop (arXiv:2504.19413), with Graphorin's\n * bi-temporal twist.\n *\n * Two pieces live here:\n *\n * - {@link preFilterCandidate} — a *cheap*, LLM-free classifier that\n * reuses the conflict pipeline's Stage 1 (exact-dedup) + Stage 2\n * (embedding three-zone) over the candidate's nearest neighbours.\n * Clear hot / near-dup neighbours short-circuit to `noop`; a clearly\n * cold field (or no neighbours at all) short-circuits to `add`. Only\n * the ambiguous CONFLICT-CHECK mid-zone is routed to `reconcile`, so\n * the consolidator spends an LLM call only where it actually helps.\n *\n * - {@link reconcileCandidate} — one provider pass that, with the\n * neighbours *in view*, chooses ADD / UPDATE / NOOP / CONFLICT.\n * Parsing is defensive: malformed output, an unknown action, or a\n * `targetId` that is not one of the supplied neighbours all fall back\n * to a safe additive write (audited) so a flaky model never wedges\n * the pipeline or rewrites the wrong fact.\n *\n * @packageDocumentation\n */\n\nimport type { Fact, MemoryHit, Provider, ProviderRequest, SessionScope } from '@graphorin/core';\nimport {\n type ConflictThresholds,\n DEFAULT_CONFLICT_THRESHOLDS,\n type ReconcileDecision,\n type StageContext,\n} from '../conflict/index.js';\nimport { enLocalePack, type LocalePack } from '../conflict/locale-packs/index.js';\nimport { stage1ExactDedup } from '../conflict/stages/stage1-exact-dedup.js';\nimport { stage2EmbeddingThreeZone } from '../conflict/stages/stage2-embedding-three-zone.js';\n\n/**\n * Pre-filter routing decision. `add` and `noop` are resolved without an\n * LLM call; `reconcile` means the candidate landed in the ambiguous\n * mid-zone and should be handed to {@link reconcileCandidate}.\n *\n * @stable\n */\nexport type PreFilterRoute =\n | { readonly route: 'add'; readonly reason: string }\n | {\n readonly route: 'noop';\n readonly targetId: string;\n readonly similarity?: number;\n readonly reason: string;\n }\n | { readonly route: 'reconcile' };\n\n/**\n * Classify a candidate against its nearest neighbours using the conflict\n * pipeline's first two (cheap, LLM-free) stages. Reuses\n * {@link stage1ExactDedup} + {@link stage2EmbeddingThreeZone} verbatim so\n * the zone thresholds stay aligned with the inline write path.\n *\n * @stable\n */\nexport async function preFilterCandidate(\n candidateText: string,\n neighbors: ReadonlyArray<MemoryHit<Fact>>,\n opts: { readonly thresholds?: ConflictThresholds; readonly localePack?: LocalePack } = {},\n): Promise<PreFilterRoute> {\n const thresholds = opts.thresholds ?? DEFAULT_CONFLICT_THRESHOLDS;\n const localePack = opts.localePack ?? enLocalePack;\n const ctx: StageContext = {\n candidate: synthCandidate(candidateText),\n existing: neighbors,\n localePack,\n thresholds,\n };\n\n const s1 = await stage1ExactDedup.evaluate(ctx);\n if (s1.kind === 'dedup') {\n return {\n route: 'noop',\n targetId: s1.existingId,\n ...(s1.similarity !== undefined ? { similarity: s1.similarity } : {}),\n reason: s1.reason ?? 'exact-hash-match',\n };\n }\n\n const s2 = await stage2EmbeddingThreeZone.evaluate(ctx);\n if (s2.kind === 'dedup') {\n return {\n route: 'noop',\n targetId: s2.existingId,\n ...(s2.similarity !== undefined ? { similarity: s2.similarity } : {}),\n reason: s2.reason ?? 'embedding-zone',\n };\n }\n if (s2.kind === 'admit') {\n return { route: 'add', reason: s2.reason ?? 'embedding-cold-zone' };\n }\n // s2.kind === 'continue' → CONFLICT-CHECK mid-zone → spend an LLM call.\n return { route: 'reconcile' };\n}\n\n/** A neighbour surfaced to the reconcile prompt. */\nexport interface ReconcileNeighbor {\n readonly id: string;\n readonly text: string;\n readonly validFrom?: string;\n}\n\n/** Token usage for the single reconcile provider call (budget accounting). */\nexport interface ReconcileUsage {\n readonly promptTokens: number;\n readonly completionTokens: number;\n readonly totalTokens: number;\n}\n\n/** Result of {@link reconcileCandidate}: the decision + its token cost. */\nexport interface ReconcileResult {\n readonly decision: ReconcileDecision;\n readonly usage: ReconcileUsage;\n}\n\n/** Inputs to {@link reconcileCandidate}. */\nexport interface ReconcileCandidateArgs {\n readonly candidateText: string;\n readonly neighbors: ReadonlyArray<ReconcileNeighbor>;\n readonly provider: Provider;\n readonly scope: SessionScope;\n}\n\n/**\n * System prompt for the reconcile pass. Mirrors the deep-phase judge\n * style but expands the decision space to the full ADD/UPDATE/NOOP/\n * CONFLICT loop and asks for the `targetId` of the resolved neighbour.\n *\n * @internal\n */\nexport const RECONCILE_SYSTEM_PROMPT = [\n \"You reconcile a candidate memory against the user's existing related memories.\",\n 'Return strictly JSON: { \"action\": \"add\" | \"update\" | \"noop\" | \"conflict\", \"targetId\"?: string, \"reason\": string }.',\n 'Use \"update\" when the candidate is a newer version of an existing memory (a changed preference, location, job, or fact).',\n 'Use \"noop\" when the candidate adds nothing new (a duplicate or subset of an existing memory).',\n 'Use \"conflict\" when the candidate contradicts an existing memory and the older one should be closed.',\n 'Use \"add\" when the candidate is independent of every listed memory, or when you are unsure.',\n 'For \"update\", \"noop\", and \"conflict\", set \"targetId\" to the id of the existing memory you are resolving against.',\n].join(' ');\n\n/**\n * Run one reconcile pass. The decision is always safe to apply: parse\n * failures and dangling `targetId`s degrade to an additive write.\n *\n * @stable\n */\nexport async function reconcileCandidate(args: ReconcileCandidateArgs): Promise<ReconcileResult> {\n const request = buildReconcileRequest(args);\n const response = await args.provider.generate(request);\n const usage = response.usage;\n const promptTokens = usage.promptTokens ?? 0;\n const completionTokens = usage.completionTokens ?? 0;\n const totalTokens = promptTokens + completionTokens + (usage.reasoningTokens ?? 0);\n const validIds = new Set(args.neighbors.map((n) => n.id));\n const decision = parseReconcile(response.text, validIds);\n return { decision, usage: { promptTokens, completionTokens, totalTokens } };\n}\n\n/**\n * Parse the reconcile model output into a {@link ReconcileDecision}.\n * Tolerates fenced blocks + trailing commentary; an unparseable body,\n * an unknown action, or a `targetId` that is not in `validTargetIds`\n * all degrade to `{ action: 'add' }` (audited via the reason).\n *\n * @internal\n */\nexport function parseReconcile(\n text: string | undefined,\n validTargetIds: ReadonlySet<string>,\n): ReconcileDecision {\n const fallback: ReconcileDecision = { action: 'add', reason: 'reconcile-parse-failure' };\n if (text === undefined || text.length === 0) return fallback;\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return fallback;\n let parsed: unknown;\n try {\n parsed = JSON.parse(candidate);\n } catch {\n const slice = sliceJsonObject(candidate);\n if (slice === null) return fallback;\n try {\n parsed = JSON.parse(slice);\n } catch {\n return fallback;\n }\n }\n if (parsed === null || typeof parsed !== 'object') return fallback;\n const obj = parsed as { action?: unknown; targetId?: unknown; reason?: unknown };\n const reason = typeof obj.reason === 'string' && obj.reason.length > 0 ? obj.reason : undefined;\n if (obj.action === 'add') {\n return { action: 'add', ...(reason !== undefined ? { reason } : {}) };\n }\n if (obj.action === 'update' || obj.action === 'noop' || obj.action === 'conflict') {\n const targetId = typeof obj.targetId === 'string' ? obj.targetId : null;\n if (targetId === null || !validTargetIds.has(targetId)) {\n // The model referenced a fact that is not one of the supplied\n // neighbours — never rewrite an unverified id; add instead.\n return { action: 'add', reason: 'reconcile-invalid-target' };\n }\n if (obj.action === 'noop') {\n return { action: 'noop', targetId, ...(reason !== undefined ? { reason } : {}) };\n }\n return { action: obj.action, targetId, reason: reason ?? `reconcile-${obj.action}` };\n }\n return fallback;\n}\n\nfunction buildReconcileRequest(args: ReconcileCandidateArgs): ProviderRequest {\n const neighborLines = args.neighbors\n .map((n, i) => {\n const vf = n.validFrom !== undefined ? ` (validFrom: ${n.validFrom})` : '';\n return `${i + 1}. [id: ${n.id}]${vf} ${n.text}`;\n })\n .join('\\n');\n const userBlock = [\n `Candidate memory: ${args.candidateText}`,\n '',\n 'Existing related memories:',\n neighborLines.length > 0 ? neighborLines : '(none)',\n ].join('\\n');\n return {\n messages: [{ role: 'user', content: userBlock }],\n systemMessage: RECONCILE_SYSTEM_PROMPT,\n temperature: 0,\n // MCON-14: per-call output cap — the decision shape is tiny.\n maxTokens: 256,\n metadata: {\n userId: args.scope.userId,\n ...(args.scope.sessionId !== undefined ? { sessionId: args.scope.sessionId } : {}),\n ...(args.scope.agentId !== undefined ? { agentId: args.scope.agentId } : {}),\n },\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Minimal candidate `Fact` for the pre-filter stage context. Stage 1\n * reads only `text`; Stage 2 reads only the neighbour scores — the\n * other fields are never inspected, so a synthetic id/scope is safe.\n */\nfunction synthCandidate(text: string): Fact {\n return {\n id: '__reconcile_candidate__',\n kind: 'semantic',\n userId: '__reconcile_candidate__',\n sensitivity: 'internal',\n text,\n createdAt: new Date(0).toISOString(),\n };\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction sliceJsonObject(text: string): string | null {\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start < 0 || end < start) return null;\n return text.slice(start, end + 1);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reconcile.js","names":["ctx: StageContext","fallback: ReconcileDecision","parsed: unknown"],"sources":["../../src/consolidator/reconcile.ts"],"sourcesContent":["/**\n * Neighbour-aware write reconciliation (P0-3) - the `reconcile` half of\n * Mem0's extract→reconcile loop (arXiv:2504.19413), with Graphorin's\n * bi-temporal twist.\n *\n * Two pieces live here:\n *\n * - {@link preFilterCandidate} - a *cheap*, LLM-free classifier that\n * reuses the conflict pipeline's Stage 1 (exact-dedup) + Stage 2\n * (embedding three-zone) over the candidate's nearest neighbours.\n * Clear hot / near-dup neighbours short-circuit to `noop`; a clearly\n * cold field (or no neighbours at all) short-circuits to `add`. Only\n * the ambiguous CONFLICT-CHECK mid-zone is routed to `reconcile`, so\n * the consolidator spends an LLM call only where it actually helps.\n *\n * - {@link reconcileCandidate} - one provider pass that, with the\n * neighbours *in view*, chooses ADD / UPDATE / NOOP / CONFLICT.\n * Parsing is defensive: malformed output, an unknown action, or a\n * `targetId` that is not one of the supplied neighbours all fall back\n * to a safe additive write (audited) so a flaky model never wedges\n * the pipeline or rewrites the wrong fact.\n *\n * @packageDocumentation\n */\n\nimport type { Fact, MemoryHit, Provider, ProviderRequest, SessionScope } from '@graphorin/core';\nimport {\n type ConflictThresholds,\n DEFAULT_CONFLICT_THRESHOLDS,\n type ReconcileDecision,\n type StageContext,\n} from '../conflict/index.js';\nimport { enLocalePack, type LocalePack } from '../conflict/locale-packs/index.js';\nimport { stage1ExactDedup } from '../conflict/stages/stage1-exact-dedup.js';\nimport { stage2EmbeddingThreeZone } from '../conflict/stages/stage2-embedding-three-zone.js';\n\n/**\n * Pre-filter routing decision. `add` and `noop` are resolved without an\n * LLM call; `reconcile` means the candidate landed in the ambiguous\n * mid-zone and should be handed to {@link reconcileCandidate}.\n *\n * @stable\n */\nexport type PreFilterRoute =\n | { readonly route: 'add'; readonly reason: string }\n | {\n readonly route: 'noop';\n readonly targetId: string;\n readonly similarity?: number;\n readonly reason: string;\n }\n | { readonly route: 'reconcile' };\n\n/**\n * Classify a candidate against its nearest neighbours using the conflict\n * pipeline's first two (cheap, LLM-free) stages. Reuses\n * {@link stage1ExactDedup} + {@link stage2EmbeddingThreeZone} verbatim so\n * the zone thresholds stay aligned with the inline write path.\n *\n * @stable\n */\nexport async function preFilterCandidate(\n candidateText: string,\n neighbors: ReadonlyArray<MemoryHit<Fact>>,\n opts: { readonly thresholds?: ConflictThresholds; readonly localePack?: LocalePack } = {},\n): Promise<PreFilterRoute> {\n const thresholds = opts.thresholds ?? DEFAULT_CONFLICT_THRESHOLDS;\n const localePack = opts.localePack ?? enLocalePack;\n const ctx: StageContext = {\n candidate: synthCandidate(candidateText),\n existing: neighbors,\n localePack,\n thresholds,\n };\n\n const s1 = await stage1ExactDedup.evaluate(ctx);\n if (s1.kind === 'dedup') {\n return {\n route: 'noop',\n targetId: s1.existingId,\n ...(s1.similarity !== undefined ? { similarity: s1.similarity } : {}),\n reason: s1.reason ?? 'exact-hash-match',\n };\n }\n\n const s2 = await stage2EmbeddingThreeZone.evaluate(ctx);\n if (s2.kind === 'dedup') {\n return {\n route: 'noop',\n targetId: s2.existingId,\n ...(s2.similarity !== undefined ? { similarity: s2.similarity } : {}),\n reason: s2.reason ?? 'embedding-zone',\n };\n }\n if (s2.kind === 'admit') {\n return { route: 'add', reason: s2.reason ?? 'embedding-cold-zone' };\n }\n // s2.kind === 'continue' → CONFLICT-CHECK mid-zone → spend an LLM call.\n return { route: 'reconcile' };\n}\n\n/** A neighbour surfaced to the reconcile prompt. */\nexport interface ReconcileNeighbor {\n readonly id: string;\n readonly text: string;\n readonly validFrom?: string;\n}\n\n/** Token usage for the single reconcile provider call (budget accounting). */\nexport interface ReconcileUsage {\n readonly promptTokens: number;\n readonly completionTokens: number;\n readonly totalTokens: number;\n}\n\n/** Result of {@link reconcileCandidate}: the decision + its token cost. */\nexport interface ReconcileResult {\n readonly decision: ReconcileDecision;\n readonly usage: ReconcileUsage;\n}\n\n/** Inputs to {@link reconcileCandidate}. */\nexport interface ReconcileCandidateArgs {\n readonly candidateText: string;\n readonly neighbors: ReadonlyArray<ReconcileNeighbor>;\n readonly provider: Provider;\n readonly scope: SessionScope;\n}\n\n/**\n * System prompt for the reconcile pass. Mirrors the deep-phase judge\n * style but expands the decision space to the full ADD/UPDATE/NOOP/\n * CONFLICT loop and asks for the `targetId` of the resolved neighbour.\n *\n * @internal\n */\nexport const RECONCILE_SYSTEM_PROMPT = [\n \"You reconcile a candidate memory against the user's existing related memories.\",\n 'Return strictly JSON: { \"action\": \"add\" | \"update\" | \"noop\" | \"conflict\", \"targetId\"?: string, \"reason\": string }.',\n 'Use \"update\" when the candidate is a newer version of an existing memory (a changed preference, location, job, or fact).',\n 'Use \"noop\" when the candidate adds nothing new (a duplicate or subset of an existing memory).',\n 'Use \"conflict\" when the candidate contradicts an existing memory and the older one should be closed.',\n 'Use \"add\" when the candidate is independent of every listed memory, or when you are unsure.',\n 'For \"update\", \"noop\", and \"conflict\", set \"targetId\" to the id of the existing memory you are resolving against.',\n].join(' ');\n\n/**\n * Run one reconcile pass. The decision is always safe to apply: parse\n * failures and dangling `targetId`s degrade to an additive write.\n *\n * @stable\n */\nexport async function reconcileCandidate(args: ReconcileCandidateArgs): Promise<ReconcileResult> {\n const request = buildReconcileRequest(args);\n const response = await args.provider.generate(request);\n const usage = response.usage;\n const promptTokens = usage.promptTokens ?? 0;\n const completionTokens = usage.completionTokens ?? 0;\n const totalTokens = promptTokens + completionTokens + (usage.reasoningTokens ?? 0);\n const validIds = new Set(args.neighbors.map((n) => n.id));\n const decision = parseReconcile(response.text, validIds);\n return { decision, usage: { promptTokens, completionTokens, totalTokens } };\n}\n\n/**\n * Parse the reconcile model output into a {@link ReconcileDecision}.\n * Tolerates fenced blocks + trailing commentary; an unparseable body,\n * an unknown action, or a `targetId` that is not in `validTargetIds`\n * all degrade to `{ action: 'add' }` (audited via the reason).\n *\n * @internal\n */\nexport function parseReconcile(\n text: string | undefined,\n validTargetIds: ReadonlySet<string>,\n): ReconcileDecision {\n const fallback: ReconcileDecision = { action: 'add', reason: 'reconcile-parse-failure' };\n if (text === undefined || text.length === 0) return fallback;\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return fallback;\n let parsed: unknown;\n try {\n parsed = JSON.parse(candidate);\n } catch {\n const slice = sliceJsonObject(candidate);\n if (slice === null) return fallback;\n try {\n parsed = JSON.parse(slice);\n } catch {\n return fallback;\n }\n }\n if (parsed === null || typeof parsed !== 'object') return fallback;\n const obj = parsed as { action?: unknown; targetId?: unknown; reason?: unknown };\n const reason = typeof obj.reason === 'string' && obj.reason.length > 0 ? obj.reason : undefined;\n if (obj.action === 'add') {\n return { action: 'add', ...(reason !== undefined ? { reason } : {}) };\n }\n if (obj.action === 'update' || obj.action === 'noop' || obj.action === 'conflict') {\n const targetId = typeof obj.targetId === 'string' ? obj.targetId : null;\n if (targetId === null || !validTargetIds.has(targetId)) {\n // The model referenced a fact that is not one of the supplied\n // neighbours - never rewrite an unverified id; add instead.\n return { action: 'add', reason: 'reconcile-invalid-target' };\n }\n if (obj.action === 'noop') {\n return { action: 'noop', targetId, ...(reason !== undefined ? { reason } : {}) };\n }\n return { action: obj.action, targetId, reason: reason ?? `reconcile-${obj.action}` };\n }\n return fallback;\n}\n\nfunction buildReconcileRequest(args: ReconcileCandidateArgs): ProviderRequest {\n const neighborLines = args.neighbors\n .map((n, i) => {\n const vf = n.validFrom !== undefined ? ` (validFrom: ${n.validFrom})` : '';\n return `${i + 1}. [id: ${n.id}]${vf} ${n.text}`;\n })\n .join('\\n');\n const userBlock = [\n `Candidate memory: ${args.candidateText}`,\n '',\n 'Existing related memories:',\n neighborLines.length > 0 ? neighborLines : '(none)',\n ].join('\\n');\n return {\n messages: [{ role: 'user', content: userBlock }],\n systemMessage: RECONCILE_SYSTEM_PROMPT,\n temperature: 0,\n // MCON-14: per-call output cap - the decision shape is tiny.\n maxTokens: 256,\n metadata: {\n userId: args.scope.userId,\n ...(args.scope.sessionId !== undefined ? { sessionId: args.scope.sessionId } : {}),\n ...(args.scope.agentId !== undefined ? { agentId: args.scope.agentId } : {}),\n },\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Minimal candidate `Fact` for the pre-filter stage context. Stage 1\n * reads only `text`; Stage 2 reads only the neighbour scores - the\n * other fields are never inspected, so a synthetic id/scope is safe.\n */\nfunction synthCandidate(text: string): Fact {\n return {\n id: '__reconcile_candidate__',\n kind: 'semantic',\n userId: '__reconcile_candidate__',\n sensitivity: 'internal',\n text,\n createdAt: new Date(0).toISOString(),\n };\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction sliceJsonObject(text: string): string | null {\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start < 0 || end < start) return null;\n return text.slice(start, end + 1);\n}\n"],"mappings":"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"}
|
|
@@ -21,7 +21,7 @@ interface Consolidator {
|
|
|
21
21
|
* idle/cron scheduler so admins can flush the queue on demand.
|
|
22
22
|
*/
|
|
23
23
|
fireNow(phase: ConsolidatorPhase, scope?: SessionScope): Promise<PhaseOutcome | null>;
|
|
24
|
-
/** Replace the active tier
|
|
24
|
+
/** Replace the active tier - recomputes ceilings + phase set. */
|
|
25
25
|
setTier(tier: ConsolidatorTier): Promise<void>;
|
|
26
26
|
/** Pause the consolidator until the next budget reset. */
|
|
27
27
|
pause(): Promise<void>;
|
|
@@ -31,16 +31,16 @@ interface Consolidator {
|
|
|
31
31
|
onPhaseFinished(listener: PhaseListener): () => void;
|
|
32
32
|
/**
|
|
33
33
|
* Record memory-pipeline LLM spend that happened OUTSIDE a phase run
|
|
34
|
-
* (MCON-15
|
|
34
|
+
* (MCON-15 - e.g. workflow induction) so the daily ceilings cover it.
|
|
35
35
|
* Counted under the deep-phase bucket.
|
|
36
36
|
*/
|
|
37
37
|
recordExternalSpend(tokens: number, costUsd?: number): void;
|
|
38
|
-
/** Active config
|
|
38
|
+
/** Active config - frozen snapshot. */
|
|
39
39
|
config(): ConsolidatorConfig;
|
|
40
40
|
/**
|
|
41
41
|
* Register this consolidator's cron / idle triggers with a
|
|
42
42
|
* `@graphorin/triggers` scheduler so they fire `trigger(...)`
|
|
43
|
-
* automatically (the daemon ↔ triggers bridge
|
|
43
|
+
* automatically (the daemon ↔ triggers bridge - MCON-4). Uses the
|
|
44
44
|
* configured `defaultScope`; throws if none was set. Turn / event
|
|
45
45
|
* triggers are skipped (consumer-emitted). The standalone server calls
|
|
46
46
|
* this in `beforeStart`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","names":[],"sources":["../../src/consolidator/runtime.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","names":[],"sources":["../../src/consolidator/runtime.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;AA+GiC,UAtChB,YAAA,CAsCgB;EAAO,KAAA,EAAA,EArC7B,OAqC6B,CAAA,IAAA,CAAA;EAQxB,IAAA,EAAA,EA5CN,OA4CM,CAAA,IAAA,CAAkB;kBA3ChB,kCAAkC,eAAe,QAAQ;YAC/D,QAAQ;;;;;iBAKH,2BAA2B,eAAe,QAAQ;;gBAEnD,mBAAmB;;WAExB;;YAEC;;4BAEgB;;;;;;;;YAQhB;;;;;;;;;mCASuB,gBAAgB,QAAQ;;;;kBAIzC,eAAe;;;;;;;iBAQjB,kBAAA,OAAyB,4BAA4B"}
|
|
@@ -7,6 +7,7 @@ import { LockManager } from "./lock.js";
|
|
|
7
7
|
import { runDeepPhase } from "./phases/deep.js";
|
|
8
8
|
import { runLightPhase } from "./phases/light.js";
|
|
9
9
|
import { runStandardPhase } from "./phases/standard.js";
|
|
10
|
+
import { runLearnedContextPass } from "./phases/learned-context.js";
|
|
10
11
|
import { runReflectionPass } from "./phases/reflect.js";
|
|
11
12
|
import { registerConsolidatorTriggers } from "./scheduler.js";
|
|
12
13
|
import { CONSOLIDATOR_TIER_DEFAULTS } from "./types.js";
|
|
@@ -14,6 +15,12 @@ import { NOOP_TRACER } from "@graphorin/core";
|
|
|
14
15
|
|
|
15
16
|
//#region src/consolidator/runtime.ts
|
|
16
17
|
/**
|
|
18
|
+
* Age after which an unresolved CONFLICT-CHECK row is expired as
|
|
19
|
+
* `'admit'` at tiers with no deep phase (memory-consolidation-04) -
|
|
20
|
+
* the judge that would resolve it never runs there.
|
|
21
|
+
*/
|
|
22
|
+
const PENDING_CONFLICT_TTL_MS = 10080 * 60 * 1e3;
|
|
23
|
+
/**
|
|
17
24
|
* Build the runtime consolidator.
|
|
18
25
|
*
|
|
19
26
|
* @stable
|
|
@@ -24,6 +31,7 @@ function createConsolidator(opts) {
|
|
|
24
31
|
var ConsolidatorImpl = class {
|
|
25
32
|
#semantic;
|
|
26
33
|
#episodic;
|
|
34
|
+
#working;
|
|
27
35
|
#store;
|
|
28
36
|
#consolidatorStore;
|
|
29
37
|
#tracer;
|
|
@@ -33,6 +41,12 @@ var ConsolidatorImpl = class {
|
|
|
33
41
|
/** Per-phase provider overrides (MCON-7); fall back to `#provider`. */
|
|
34
42
|
#cheapProvider;
|
|
35
43
|
#deepProvider;
|
|
44
|
+
/**
|
|
45
|
+
* USD pricer for phase LLM usage (memory-consolidation-02). Without
|
|
46
|
+
* it every phase's `priceUsage?.(...) ?? 0` evaluates to zero and the
|
|
47
|
+
* `maxCostPerDay` ceiling can never trip.
|
|
48
|
+
*/
|
|
49
|
+
#priceUsage;
|
|
36
50
|
#defaultScope;
|
|
37
51
|
#listeners = /* @__PURE__ */ new Set();
|
|
38
52
|
#lockManager;
|
|
@@ -44,7 +58,7 @@ var ConsolidatorImpl = class {
|
|
|
44
58
|
/**
|
|
45
59
|
* Message ids of the batch the most recent `#dispatch` operated on
|
|
46
60
|
* (MCON-10). Captured so a thrown phase can enqueue its DLQ row with
|
|
47
|
-
* the REAL failed slice instead of `[]`
|
|
61
|
+
* the REAL failed slice instead of `[]` - replays must target the
|
|
48
62
|
* window that failed, not whatever the cursor points at later.
|
|
49
63
|
*/
|
|
50
64
|
#lastDispatchMessageIds = [];
|
|
@@ -59,6 +73,7 @@ var ConsolidatorImpl = class {
|
|
|
59
73
|
constructor(opts) {
|
|
60
74
|
this.#semantic = opts.semantic;
|
|
61
75
|
this.#episodic = opts.episodic ?? null;
|
|
76
|
+
this.#working = opts.working ?? null;
|
|
62
77
|
this.#store = opts.store;
|
|
63
78
|
this.#consolidatorStore = this.#store.consolidator ?? null;
|
|
64
79
|
this.#tracer = opts.tracer ?? NOOP_TRACER;
|
|
@@ -69,6 +84,7 @@ var ConsolidatorImpl = class {
|
|
|
69
84
|
this.#provider = opts.provider ?? null;
|
|
70
85
|
this.#cheapProvider = opts.cheapProvider ?? null;
|
|
71
86
|
this.#deepProvider = opts.deepProvider ?? null;
|
|
87
|
+
this.#priceUsage = opts.priceUsage ?? null;
|
|
72
88
|
this.#defaultScope = opts.defaultScope ?? null;
|
|
73
89
|
this.#config = resolveConfig(opts);
|
|
74
90
|
this.#lockManager = new LockManager({
|
|
@@ -125,7 +141,7 @@ var ConsolidatorImpl = class {
|
|
|
125
141
|
async setTier(tier) {
|
|
126
142
|
const preset = CONSOLIDATOR_TIER_DEFAULTS[tier];
|
|
127
143
|
if (preset === void 0) throw new Error(`[graphorin/memory] unknown consolidator tier '${tier}'`);
|
|
128
|
-
if (tier === "custom") throw new Error("[graphorin/memory] consolidator.setTier(\"custom\") requires explicit ceilings
|
|
144
|
+
if (tier === "custom") throw new Error("[graphorin/memory] consolidator.setTier(\"custom\") requires explicit ceilings - re-create the consolidator with `tier: \"custom\"` + `ceilings`.");
|
|
129
145
|
this.#config = Object.freeze({
|
|
130
146
|
...this.#config,
|
|
131
147
|
tier,
|
|
@@ -205,6 +221,8 @@ var ConsolidatorImpl = class {
|
|
|
205
221
|
async trigger(reason, scope) {
|
|
206
222
|
if (!this.#running) return null;
|
|
207
223
|
if (this.#manuallyPaused) return null;
|
|
224
|
+
await this.drainDlq(scope).catch(() => 0);
|
|
225
|
+
await this.#expireStalePendingConflicts(scope).catch(() => {});
|
|
208
226
|
const phases = this.#planPhases(reason);
|
|
209
227
|
if (phases.length === 0) return null;
|
|
210
228
|
if (reason.kind !== "manual" && this.#config.ceilings.cooldownMs > 0) {
|
|
@@ -225,12 +243,26 @@ var ConsolidatorImpl = class {
|
|
|
225
243
|
async fireNow(phase, scope) {
|
|
226
244
|
const target = scope ?? this.#defaultScope;
|
|
227
245
|
if (target === null) throw new Error("[graphorin/memory] consolidator.fireNow requires a scope (default scope was not configured).");
|
|
228
|
-
if (!this.#config.phases.includes(phase) && phase !== "light") process.stderr.write(`[graphorin/memory] consolidator.fireNow('${phase}') runs a phase not enabled for tier '${this.#config.tier}'
|
|
246
|
+
if (!this.#config.phases.includes(phase) && phase !== "light") process.stderr.write(`[graphorin/memory] consolidator.fireNow('${phase}') runs a phase not enabled for tier '${this.#config.tier}' - proceeding (manual flushes bypass phase gating).\n`);
|
|
229
247
|
return this.#runPhase(phase, target, {
|
|
230
248
|
kind: "manual",
|
|
231
249
|
value: phase
|
|
232
250
|
});
|
|
233
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* memory-consolidation-04: at tiers with no deep phase the pending
|
|
254
|
+
* CONFLICT-CHECK queue has no drain. Resolve rows older than 7 days
|
|
255
|
+
* as `'admit'` (the safe direction - the candidate fact stays live;
|
|
256
|
+
* only the never-coming judge call is skipped), bounded per sweep.
|
|
257
|
+
*/
|
|
258
|
+
async #expireStalePendingConflicts(scope) {
|
|
259
|
+
if (this.#config.phases.includes("deep")) return;
|
|
260
|
+
const conflicts = this.#store.conflicts;
|
|
261
|
+
if (conflicts === void 0) return;
|
|
262
|
+
const cutoff = this.#now() - PENDING_CONFLICT_TTL_MS;
|
|
263
|
+
const pending = await conflicts.listPending(scope, 200);
|
|
264
|
+
for (const row of pending) if (row.enqueuedAt <= cutoff) await conflicts.markResolved(row.id, "admit");
|
|
265
|
+
}
|
|
234
266
|
async drainDlq(scope) {
|
|
235
267
|
const store = this.#consolidatorStore;
|
|
236
268
|
if (store === null) return 0;
|
|
@@ -433,7 +465,7 @@ var ConsolidatorImpl = class {
|
|
|
433
465
|
const session = this.#store.session;
|
|
434
466
|
const rawBatch = typeof session.listMessagesSince === "function" ? await session.listMessagesSince(scope, lastProcessedMessageId, this.#config.maxStandardBatchSize) : [];
|
|
435
467
|
this.#lastDispatchMessageIds = rawBatch.map((r) => r.id);
|
|
436
|
-
const out = await runStandardPhase({
|
|
468
|
+
const out$1 = await runStandardPhase({
|
|
437
469
|
semantic: this.#semantic,
|
|
438
470
|
episodic: this.#episodic,
|
|
439
471
|
formEpisodes: this.#config.formEpisodes,
|
|
@@ -452,15 +484,16 @@ var ConsolidatorImpl = class {
|
|
|
452
484
|
budget: this.#budget,
|
|
453
485
|
tier: this.#config.tier === "free" ? "cheap" : this.#config.tier,
|
|
454
486
|
now: this.#now,
|
|
455
|
-
batch: rawBatch
|
|
487
|
+
batch: rawBatch,
|
|
488
|
+
...this.#priceUsage !== null ? { priceUsage: this.#priceUsage } : {}
|
|
456
489
|
});
|
|
457
490
|
const cursor = tipMessageId(rawBatch);
|
|
458
491
|
if (cursor !== null && this.#consolidatorStore !== null) await this.#consolidatorStore.upsertState(scope, { lastProcessedMessageId: cursor });
|
|
459
|
-
return out;
|
|
492
|
+
return out$1;
|
|
460
493
|
}
|
|
461
494
|
const deepProvider = this.#deepProvider ?? this.#provider;
|
|
462
495
|
if (deepProvider === null) throw new ProviderNotConfiguredError("deep");
|
|
463
|
-
|
|
496
|
+
let out = await runDeepPhase({
|
|
464
497
|
store: this.#store,
|
|
465
498
|
consolidatorStore: this.#consolidatorStore,
|
|
466
499
|
provider: deepProvider,
|
|
@@ -470,7 +503,8 @@ var ConsolidatorImpl = class {
|
|
|
470
503
|
maxConflictsPerRun: this.#config.maxDeepConflictsPerRun,
|
|
471
504
|
budget: this.#budget,
|
|
472
505
|
tier: this.#config.tier === "free" || this.#config.tier === "cheap" ? "standard" : this.#config.tier,
|
|
473
|
-
now: this.#now
|
|
506
|
+
now: this.#now,
|
|
507
|
+
...this.#priceUsage !== null ? { priceUsage: this.#priceUsage } : {}
|
|
474
508
|
});
|
|
475
509
|
const insightStore = this.#store.insights;
|
|
476
510
|
if (this.#config.reflection && this.#episodic !== null && insightStore !== void 0) {
|
|
@@ -486,17 +520,38 @@ var ConsolidatorImpl = class {
|
|
|
486
520
|
importanceThreshold: this.#config.importanceThreshold,
|
|
487
521
|
reflectionWatermark: priorWatermark,
|
|
488
522
|
maxQuestions: this.#config.reflectionMaxQuestions,
|
|
489
|
-
now: this.#now
|
|
523
|
+
now: this.#now,
|
|
524
|
+
...this.#priceUsage !== null ? { priceUsage: this.#priceUsage } : {}
|
|
490
525
|
});
|
|
491
526
|
if (this.#consolidatorStore !== null && reflection.nextWatermark !== priorWatermark) await this.#consolidatorStore.upsertState(scope, { reflectionWatermark: reflection.nextWatermark });
|
|
492
|
-
|
|
493
|
-
...
|
|
527
|
+
out = {
|
|
528
|
+
...out,
|
|
494
529
|
insightsCreated: reflection.insightsCreated,
|
|
495
|
-
llmTokensUsed:
|
|
496
|
-
llmCostUsd:
|
|
530
|
+
llmTokensUsed: out.llmTokensUsed + reflection.tokens,
|
|
531
|
+
llmCostUsd: out.llmCostUsd === null && reflection.costUsd === 0 ? null : (out.llmCostUsd ?? 0) + reflection.costUsd
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (this.#config.learnedContext && this.#working !== null) {
|
|
535
|
+
const learned = await runLearnedContextPass({
|
|
536
|
+
provider: deepProvider,
|
|
537
|
+
tracer: this.#tracer,
|
|
538
|
+
scope,
|
|
539
|
+
working: this.#working,
|
|
540
|
+
episodic: this.#episodic,
|
|
541
|
+
store: this.#store,
|
|
542
|
+
budget: this.#budget,
|
|
543
|
+
maxChars: this.#config.learnedContextMaxChars,
|
|
544
|
+
now: this.#now,
|
|
545
|
+
...this.#priceUsage !== null ? { priceUsage: this.#priceUsage } : {}
|
|
546
|
+
});
|
|
547
|
+
out = {
|
|
548
|
+
...out,
|
|
549
|
+
learnedContextUpdated: learned.updated,
|
|
550
|
+
llmTokensUsed: out.llmTokensUsed + learned.tokens,
|
|
551
|
+
llmCostUsd: out.llmCostUsd === null && learned.costUsd === 0 ? null : (out.llmCostUsd ?? 0) + learned.costUsd
|
|
497
552
|
};
|
|
498
553
|
}
|
|
499
|
-
return
|
|
554
|
+
return out;
|
|
500
555
|
}
|
|
501
556
|
#emit(outcome, scope, reason) {
|
|
502
557
|
for (const listener of this.#listeners) try {
|
|
@@ -522,7 +577,7 @@ function skipOutcome(phase, reason) {
|
|
|
522
577
|
emptyExtractions: 0,
|
|
523
578
|
llmTokensUsed: 0,
|
|
524
579
|
llmCostUsd: null,
|
|
525
|
-
errorMessage: `phase skipped
|
|
580
|
+
errorMessage: `phase skipped - ${reason}`
|
|
526
581
|
};
|
|
527
582
|
}
|
|
528
583
|
/** Resolve operator-supplied options into a fully-defaulted config. */
|
|
@@ -568,7 +623,9 @@ function resolveConfig(opts) {
|
|
|
568
623
|
reflection: opts.reflection ?? preset.reflection,
|
|
569
624
|
importanceThreshold: opts.importanceThreshold ?? preset.importanceThreshold,
|
|
570
625
|
reflectionMaxQuestions: opts.reflectionMaxQuestions ?? preset.reflectionMaxQuestions,
|
|
571
|
-
contextualRetrieval: opts.contextualRetrieval ?? preset.contextualRetrieval
|
|
626
|
+
contextualRetrieval: opts.contextualRetrieval ?? preset.contextualRetrieval,
|
|
627
|
+
learnedContext: opts.learnedContext ?? preset.learnedContext,
|
|
628
|
+
learnedContextMaxChars: opts.learnedContextMaxChars ?? preset.learnedContextMaxChars
|
|
572
629
|
});
|
|
573
630
|
}
|
|
574
631
|
function defaultTriggers() {
|