@graphorin/memory 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -1
- package/README.md +31 -31
- package/dist/conflict/index.d.ts +3 -1
- package/dist/conflict/index.js +1 -1
- package/dist/conflict/locale-packs/en.d.ts +13 -0
- package/dist/conflict/locale-packs/en.d.ts.map +1 -0
- package/dist/conflict/locale-packs/en.js +1 -1
- package/dist/conflict/locale-packs/en.js.map +1 -1
- package/dist/conflict/pipeline.d.ts +2 -2
- package/dist/conflict/pipeline.js +3 -3
- package/dist/conflict/pipeline.js.map +1 -1
- package/dist/conflict/stages/helpers.js +1 -1
- package/dist/conflict/stages/helpers.js.map +1 -1
- package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -1
- package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
- package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -1
- package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -1
- package/dist/conflict/types.d.ts +7 -7
- package/dist/conflict/types.js +1 -1
- package/dist/conflict/types.js.map +1 -1
- package/dist/consolidator/budget.js +16 -3
- package/dist/consolidator/budget.js.map +1 -1
- package/dist/consolidator/decay.d.ts +13 -2
- package/dist/consolidator/decay.js +17 -5
- package/dist/consolidator/decay.js.map +1 -1
- package/dist/consolidator/dlq.js +1 -1
- package/dist/consolidator/dlq.js.map +1 -1
- package/dist/consolidator/errors.js +2 -2
- package/dist/consolidator/errors.js.map +1 -1
- package/dist/consolidator/index.js +8 -6
- package/dist/consolidator/index.js.map +1 -1
- package/dist/consolidator/lock.js +1 -1
- package/dist/consolidator/lock.js.map +1 -1
- package/dist/consolidator/noise-filter.js.map +1 -1
- package/dist/consolidator/phases/deep.js +6 -2
- package/dist/consolidator/phases/deep.js.map +1 -1
- package/dist/consolidator/phases/induce.d.ts +10 -10
- package/dist/consolidator/phases/induce.js +6 -6
- package/dist/consolidator/phases/induce.js.map +1 -1
- package/dist/consolidator/phases/learned-context.js +129 -0
- package/dist/consolidator/phases/learned-context.js.map +1 -0
- package/dist/consolidator/phases/light.js +2 -1
- package/dist/consolidator/phases/light.js.map +1 -1
- package/dist/consolidator/phases/reflect.js +5 -4
- package/dist/consolidator/phases/reflect.js.map +1 -1
- package/dist/consolidator/phases/standard.js +37 -8
- package/dist/consolidator/phases/standard.js.map +1 -1
- package/dist/consolidator/reconcile.js +1 -1
- package/dist/consolidator/reconcile.js.map +1 -1
- package/dist/consolidator/runtime.d.ts +4 -4
- package/dist/consolidator/runtime.d.ts.map +1 -1
- package/dist/consolidator/runtime.js +73 -16
- package/dist/consolidator/runtime.js.map +1 -1
- package/dist/consolidator/scheduler.d.ts +5 -5
- package/dist/consolidator/scheduler.js +1 -1
- package/dist/consolidator/scheduler.js.map +1 -1
- package/dist/consolidator/triggers.js +7 -7
- package/dist/consolidator/triggers.js.map +1 -1
- package/dist/consolidator/types.d.ts +68 -20
- package/dist/consolidator/types.d.ts.map +1 -1
- package/dist/consolidator/types.js +16 -6
- package/dist/consolidator/types.js.map +1 -1
- package/dist/context-engine/annotations.d.ts +9 -9
- package/dist/context-engine/annotations.js +3 -3
- package/dist/context-engine/annotations.js.map +1 -1
- package/dist/context-engine/auto-recall.js.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.d.ts +18 -3
- package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js +26 -5
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
- package/dist/context-engine/compaction/compactor.js +19 -6
- package/dist/context-engine/compaction/compactor.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +4 -0
- package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +2 -1
- package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -1
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts +28 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts.map +1 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.js +71 -0
- package/dist/context-engine/compaction/hooks/reanchor-recent-results.js.map +1 -0
- package/dist/context-engine/compaction/hooks/types.d.ts +15 -3
- package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -1
- package/dist/context-engine/compaction/index.d.ts +1 -0
- package/dist/context-engine/compaction/index.js +2 -1
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +10 -3
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.js +42 -5
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
- package/dist/context-engine/compaction/thresholds.js.map +1 -1
- package/dist/context-engine/compaction/types.d.ts +47 -7
- package/dist/context-engine/compaction/types.d.ts.map +1 -1
- package/dist/context-engine/engine.d.ts +23 -4
- package/dist/context-engine/engine.d.ts.map +1 -1
- package/dist/context-engine/engine.js +69 -15
- package/dist/context-engine/engine.js.map +1 -1
- package/dist/context-engine/index.d.ts +1 -0
- package/dist/context-engine/index.d.ts.map +1 -1
- package/dist/context-engine/index.js +2 -1
- package/dist/context-engine/locale-packs/en.js +14 -13
- package/dist/context-engine/locale-packs/en.js.map +1 -1
- package/dist/context-engine/locale-packs/types.d.ts +10 -10
- package/dist/context-engine/locale-packs/types.d.ts.map +1 -1
- package/dist/context-engine/locale-packs/types.js.map +1 -1
- package/dist/context-engine/metadata.d.ts +1 -1
- package/dist/context-engine/metadata.js +2 -2
- package/dist/context-engine/metadata.js.map +1 -1
- package/dist/context-engine/preambles/inbound-en.d.ts +1 -1
- package/dist/context-engine/preambles/inbound-en.js +1 -1
- package/dist/context-engine/preambles/inbound-en.js.map +1 -1
- package/dist/context-engine/privacy-filter.d.ts +2 -2
- package/dist/context-engine/privacy-filter.js +1 -1
- package/dist/context-engine/privacy-filter.js.map +1 -1
- package/dist/context-engine/templates/base-en.d.ts +1 -1
- package/dist/context-engine/templates/base-en.js +1 -1
- package/dist/context-engine/templates/base-en.js.map +1 -1
- package/dist/context-engine/templates/composer.d.ts +1 -1
- package/dist/context-engine/templates/composer.js.map +1 -1
- package/dist/context-engine/token-budget.d.ts +1 -1
- package/dist/context-engine/token-budget.js +1 -1
- package/dist/context-engine/token-budget.js.map +1 -1
- package/dist/context-engine/token-counter.d.ts +16 -7
- package/dist/context-engine/token-counter.d.ts.map +1 -1
- package/dist/context-engine/token-counter.js +31 -9
- package/dist/context-engine/token-counter.js.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +4 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/facade.d.ts +44 -15
- package/dist/facade.d.ts.map +1 -1
- package/dist/facade.js +16 -8
- package/dist/facade.js.map +1 -1
- package/dist/graph/entity-resolver.d.ts +2 -2
- package/dist/graph/entity-resolver.js +2 -2
- package/dist/graph/entity-resolver.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/internal/contextualize.js +2 -2
- package/dist/internal/contextualize.js.map +1 -1
- package/dist/internal/embedder-binding.js +1 -1
- package/dist/internal/embedder-binding.js.map +1 -1
- package/dist/internal/id.js +1 -1
- package/dist/internal/id.js.map +1 -1
- package/dist/internal/injection-heuristics.js +25 -2
- package/dist/internal/injection-heuristics.js.map +1 -1
- package/dist/internal/spans.js +1 -1
- package/dist/internal/spans.js.map +1 -1
- package/dist/internal/storage-adapter.d.ts +71 -22
- package/dist/internal/storage-adapter.d.ts.map +1 -1
- package/dist/migration/embedder-migration.d.ts +7 -7
- package/dist/migration/embedder-migration.js.map +1 -1
- package/dist/search/explain.d.ts +7 -4
- package/dist/search/explain.d.ts.map +1 -1
- package/dist/search/explain.js +1 -1
- package/dist/search/explain.js.map +1 -1
- package/dist/search/fit-weights.d.ts +48 -0
- package/dist/search/fit-weights.d.ts.map +1 -0
- package/dist/search/fit-weights.js +70 -0
- package/dist/search/fit-weights.js.map +1 -0
- package/dist/search/graph-ppr.d.ts +36 -0
- package/dist/search/graph-ppr.d.ts.map +1 -0
- package/dist/search/graph-ppr.js +38 -0
- package/dist/search/graph-ppr.js.map +1 -0
- package/dist/search/index.d.ts +4 -1
- package/dist/search/index.js +4 -1
- package/dist/search/iterative.d.ts +15 -8
- package/dist/search/iterative.d.ts.map +1 -1
- package/dist/search/iterative.js +10 -9
- package/dist/search/iterative.js.map +1 -1
- package/dist/search/query-transform.d.ts +6 -6
- package/dist/search/query-transform.js +6 -6
- package/dist/search/query-transform.js.map +1 -1
- package/dist/search/rrf.d.ts +4 -4
- package/dist/search/rrf.js +4 -4
- package/dist/search/rrf.js.map +1 -1
- package/dist/search/trust.d.ts +30 -0
- package/dist/search/trust.d.ts.map +1 -0
- package/dist/search/trust.js +50 -0
- package/dist/search/trust.js.map +1 -0
- package/dist/search/types.d.ts +1 -1
- package/dist/tiers/episodic-memory.d.ts +6 -4
- package/dist/tiers/episodic-memory.d.ts.map +1 -1
- package/dist/tiers/episodic-memory.js +3 -2
- package/dist/tiers/episodic-memory.js.map +1 -1
- package/dist/tiers/index.d.ts +1 -1
- package/dist/tiers/index.js +1 -1
- package/dist/tiers/insight-memory.d.ts +4 -4
- package/dist/tiers/insight-memory.js +3 -3
- package/dist/tiers/insight-memory.js.map +1 -1
- package/dist/tiers/procedural-memory.d.ts +29 -7
- package/dist/tiers/procedural-memory.d.ts.map +1 -1
- package/dist/tiers/procedural-memory.js +66 -6
- package/dist/tiers/procedural-memory.js.map +1 -1
- package/dist/tiers/semantic-memory.d.ts +111 -30
- package/dist/tiers/semantic-memory.d.ts.map +1 -1
- package/dist/tiers/semantic-memory.js +120 -27
- package/dist/tiers/semantic-memory.js.map +1 -1
- package/dist/tiers/session-memory.d.ts +7 -7
- package/dist/tiers/session-memory.js +7 -7
- package/dist/tiers/session-memory.js.map +1 -1
- package/dist/tiers/shared-memory.d.ts +1 -1
- package/dist/tiers/shared-memory.js +1 -1
- package/dist/tiers/shared-memory.js.map +1 -1
- package/dist/tiers/working-memory.d.ts +3 -3
- package/dist/tiers/working-memory.js +2 -2
- package/dist/tiers/working-memory.js.map +1 -1
- package/dist/tools/block-tools.d.ts +9 -9
- package/dist/tools/block-tools.d.ts.map +1 -1
- package/dist/tools/block-tools.js +3 -3
- package/dist/tools/block-tools.js.map +1 -1
- package/dist/tools/fact-tools.d.ts +48 -31
- package/dist/tools/fact-tools.d.ts.map +1 -1
- package/dist/tools/fact-tools.js +30 -18
- package/dist/tools/fact-tools.js.map +1 -1
- package/dist/tools/index.d.ts +9 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/recall-tools.d.ts +40 -20
- package/dist/tools/recall-tools.d.ts.map +1 -1
- package/dist/tools/recall-tools.js +29 -10
- package/dist/tools/recall-tools.js.map +1 -1
- package/dist/tools/runbook-tools.d.ts +76 -0
- package/dist/tools/runbook-tools.d.ts.map +1 -0
- package/dist/tools/runbook-tools.js +53 -0
- package/dist/tools/runbook-tools.js.map +1 -0
- package/dist/tools/taint.js +33 -0
- package/dist/tools/taint.js.map +1 -0
- package/dist/tools/types.d.ts +1 -1
- package/package.json +8 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"learned-context.js","names":["sections: string[]"],"sources":["../../../src/consolidator/phases/learned-context.ts"],"sourcesContent":["/**\n * Learned-context pass (D3) - Letta-style sleep-time maintenance of a\n * persistent, size-bounded, agent-editable digest block. The deep phase\n * rewrites one reserved working-memory block (`learned_context`) from\n * the previous digest + fresh evidence (recent episodes, active\n * insights, active procedures), so every subsequent step's system\n * prompt carries a compact \"what I have learned about this user /\n * project\" summary inside the stable KV-cache prefix - without\n * retrieval latency and without unbounded growth.\n *\n * The block itself is an ordinary working block: it is spliced into\n * layer 3 of the assembled context automatically, re-anchored after\n * compaction by the persona-block hook pattern, and remains editable by\n * the agent through the existing `block_*` tools (the pass rewrites it\n * on its own cadence, folding any agent edits into the next rewrite).\n *\n * Runs after the deep-phase reflection pass, reusing the same budget /\n * lock / run-audit envelope. Gated off by default at every tier\n * (`learnedContext: false`) - Wave-D trial discipline.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest, SessionScope, Tracer } from '@graphorin/core';\nimport { withMemorySpan } from '../../internal/spans.js';\nimport type { MemoryStoreAdapter } from '../../internal/storage-adapter.js';\nimport type { EpisodicMemory } from '../../tiers/episodic-memory.js';\nimport { defineBlock, type WorkingMemory } from '../../tiers/working-memory.js';\nimport type { BudgetTracker } from '../budget.js';\n\n/** Reserved working-block label maintained by the pass. */\nexport const LEARNED_CONTEXT_BLOCK_LABEL = 'learned_context';\n\n/** Default character bound for the digest block. */\nexport const DEFAULT_LEARNED_CONTEXT_MAX_CHARS = 1200;\n\n/** How many recent episode summaries feed one rewrite. */\nconst EPISODE_CONTEXT_LIMIT = 15;\n\n/** How many active insights / procedures feed one rewrite. */\nconst INSIGHT_CONTEXT_LIMIT = 10;\nconst PROCEDURE_CONTEXT_LIMIT = 8;\n\nconst LEARNED_CONTEXT_SYSTEM_PROMPT = [\n 'You maintain the learned-context block of a long-running personal-assistant memory:',\n 'a compact standing digest of what is durably known about the user and their active',\n 'work (profile, preferences, ongoing projects, validated insights, working procedures).',\n 'Rewrite the digest by folding the new evidence into the previous digest: keep what is',\n 'still true, drop what is stale or superseded, and never invent anything the evidence',\n 'does not support. Write plain prose or short dashed lines - no markdown headings, no',\n 'code fences, no preamble. Stay strictly under the character budget.',\n].join(' ');\n\n/** Inputs accepted by {@link runLearnedContextPass}. */\nexport interface LearnedContextDeps {\n readonly provider: Provider;\n readonly tracer: Tracer;\n readonly scope: SessionScope;\n /** Working tier that owns the digest block. */\n readonly working: WorkingMemory;\n /** Recent-episode evidence source. `null` ⇒ episodes are skipped. */\n readonly episodic: EpisodicMemory | null;\n /** Storage adapter - supplies active insights + procedures as evidence. */\n readonly store: MemoryStoreAdapter;\n readonly budget: BudgetTracker;\n /** Character bound enforced on the stored digest. */\n readonly maxChars: number;\n readonly priceUsage?: (usage: { promptTokens: number; completionTokens: number }) => number;\n /** Override the wall clock - used by tests. */\n readonly now?: () => number;\n}\n\n/** Summary returned by {@link runLearnedContextPass}. */\nexport interface LearnedContextOutcome {\n /** True when the block was rewritten this pass. */\n readonly updated: boolean;\n /** Stored digest length after the pass (0 when skipped and absent). */\n readonly chars: number;\n readonly tokens: number;\n readonly costUsd: number;\n /** Why the pass made no LLM call, when it did not. */\n readonly skippedReason?: 'budget' | 'no-evidence' | 'empty-rewrite' | 'provider-error';\n}\n\n/**\n * Strip an accidental leading code fence from the model's digest and\n * clamp it to the character budget. Returns `null` for an effectively\n * empty rewrite (the previous digest is then left untouched).\n */\nexport function normalizeLearnedContext(raw: string, maxChars: number): string | null {\n let text = raw.trim();\n const fence = /^```[^\\n]*\\n([\\s\\S]*?)\\n?```\\s*$/.exec(text);\n if (fence?.[1] !== undefined) text = fence[1].trim();\n if (text.length === 0) return null;\n return text.length > maxChars ? text.slice(0, maxChars).trimEnd() : text;\n}\n\n/** Build the single rewrite request (pure - testable offline). */\nexport function buildLearnedContextRequest(args: {\n readonly previous: string;\n readonly episodes: ReadonlyArray<string>;\n readonly insights: ReadonlyArray<string>;\n readonly procedures: ReadonlyArray<string>;\n readonly maxChars: number;\n}): ProviderRequest {\n const sections: string[] = [\n `Character budget: ${args.maxChars}.`,\n args.previous.length > 0\n ? `Previous digest:\\n${args.previous}`\n : 'Previous digest: (none yet - write the first one)',\n ];\n if (args.episodes.length > 0) {\n sections.push(`Recent episodes:\\n${args.episodes.map((s) => `- ${s}`).join('\\n')}`);\n }\n if (args.insights.length > 0) {\n sections.push(`Validated insights:\\n${args.insights.map((s) => `- ${s}`).join('\\n')}`);\n }\n if (args.procedures.length > 0) {\n sections.push(`Active procedures:\\n${args.procedures.map((s) => `- ${s}`).join('\\n')}`);\n }\n sections.push('Return ONLY the rewritten digest text.');\n return {\n messages: [{ role: 'user', content: sections.join('\\n\\n') }],\n systemMessage: LEARNED_CONTEXT_SYSTEM_PROMPT,\n temperature: 0,\n maxTokens: Math.max(256, Math.ceil(args.maxChars / 2)),\n };\n}\n\n/**\n * Run the learned-context rewrite. Resilient by construction: a\n * provider failure or empty rewrite leaves the existing block untouched\n * and never throws; only storage errors propagate.\n */\nexport async function runLearnedContextPass(\n deps: LearnedContextDeps,\n): Promise<LearnedContextOutcome> {\n return withMemorySpan(\n deps.tracer,\n 'memory.consolidate.learned-context',\n deps.scope,\n { 'consolidator.phase': 'learned-context' },\n async (span) => {\n const skip = (\n reason: NonNullable<LearnedContextOutcome['skippedReason']>,\n chars = 0,\n ): LearnedContextOutcome => {\n span.setAttributes({ 'consolidator.learned_context.skipped': reason });\n return { updated: false, chars, tokens: 0, costUsd: 0, skippedReason: reason };\n };\n if (deps.budget.snapshot().paused) return skip('budget');\n\n // Ensure the reserved block is registered so write() can create it.\n if (deps.working.definitionFor(LEARNED_CONTEXT_BLOCK_LABEL) === undefined) {\n deps.working.define(\n defineBlock({\n label: LEARNED_CONTEXT_BLOCK_LABEL,\n description:\n 'Standing digest of durable knowledge about the user and active work, ' +\n 'maintained by the deep-phase learned-context pass.',\n charLimit: deps.maxChars,\n }),\n );\n }\n const previous = (await deps.working.read(deps.scope, LEARNED_CONTEXT_BLOCK_LABEL)) ?? '';\n\n // Evidence: recent episodes (incl. quarantined auto-formed ones -\n // they carry the importance signal), ACTIVE insights only (the\n // digest drives behaviour, so quarantined synthesis stays out),\n // and active procedures.\n const episodes =\n deps.episodic === null\n ? []\n : (\n await deps.episodic.listRecent(deps.scope, EPISODE_CONTEXT_LIMIT, {\n includeQuarantined: true,\n })\n ).map((episode) => episode.summary);\n const insights =\n deps.store.insights === undefined\n ? []\n : (await deps.store.insights.list(deps.scope, { limit: INSIGHT_CONTEXT_LIMIT })).map(\n (insight) => insight.text,\n );\n const procedures = (await deps.store.procedural.list(deps.scope))\n .filter((rule) => rule.status !== 'quarantined')\n .slice(0, PROCEDURE_CONTEXT_LIMIT)\n .map((rule) => rule.text.split('\\n')[0] ?? rule.text);\n\n const evidenceCount = episodes.length + insights.length + procedures.length;\n span.setAttributes({ 'consolidator.learned_context.evidence': evidenceCount });\n // Nothing new to fold and nothing written yet ⇒ no paid call.\n if (evidenceCount === 0) return skip('no-evidence', previous.length);\n\n let tokens = 0;\n let costUsd = 0;\n try {\n const res = await deps.provider.generate(\n buildLearnedContextRequest({\n previous,\n episodes,\n insights,\n procedures,\n maxChars: deps.maxChars,\n }),\n );\n tokens =\n (res.usage.promptTokens ?? 0) +\n (res.usage.completionTokens ?? 0) +\n (res.usage.reasoningTokens ?? 0);\n costUsd =\n deps.priceUsage?.({\n promptTokens: res.usage.promptTokens ?? 0,\n completionTokens: res.usage.completionTokens ?? 0,\n }) ?? 0;\n deps.budget.record({ phase: 'deep', tokens, costUsd });\n const next = normalizeLearnedContext(res.text ?? '', deps.maxChars);\n if (next === null) {\n return { ...skip('empty-rewrite', previous.length), tokens, costUsd };\n }\n await deps.working.write(deps.scope, LEARNED_CONTEXT_BLOCK_LABEL, next);\n span.setAttributes({\n 'consolidator.learned_context.updated': true,\n 'consolidator.learned_context.chars': next.length,\n });\n return { updated: true, chars: next.length, tokens, costUsd };\n } catch {\n // Resilient: a provider failure must never fail the deep phase.\n return { ...skip('provider-error', previous.length), tokens, costUsd };\n }\n },\n );\n}\n"],"mappings":"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"}
|
|
@@ -34,6 +34,7 @@ async function runLightPhase(deps) {
|
|
|
34
34
|
importance: row.importance,
|
|
35
35
|
quarantined: row.status === "quarantined",
|
|
36
36
|
foreignProvenance: isForeignProvenance(row.provenance),
|
|
37
|
+
accessCount: row.accessCount ?? 0,
|
|
37
38
|
weights: deps.salienceWeights
|
|
38
39
|
});
|
|
39
40
|
if (score < deps.decayArchiveThreshold) {
|
|
@@ -84,7 +85,7 @@ async function runLightPhase(deps) {
|
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
/**
|
|
87
|
-
* `true` for provenance that did not originate first-party (P1-4)
|
|
88
|
+
* `true` for provenance that did not originate first-party (P1-4) - used
|
|
88
89
|
* to apply the mild salience penalty in capacity eviction. First-party
|
|
89
90
|
* is `null` (legacy / direct write), `'user'`, and `'extraction'` (the
|
|
90
91
|
* consolidator distilling the user's own session); `'tool'`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"light.js","names":["survivors: Array<{ id: string; salience: number }>"],"sources":["../../../src/consolidator/phases/light.ts"],"sourcesContent":["/**\n * Light phase
|
|
1
|
+
{"version":3,"file":"light.js","names":["survivors: Array<{ id: string; salience: number }>"],"sources":["../../../src/consolidator/phases/light.ts"],"sourcesContent":["/**\n * Light phase - no LLM. Decays every fact's retention curve and\n * archives facts whose salience has fallen below the configured\n * threshold (plus the X-1 capacity pass). Noise filtering happens in\n * the standard phase where the batch is actually consumed - the old\n * advisory re-count here read the same unconsumed messages on every\n * pass for a counter nothing acted on (MCON-17).\n *\n * @packageDocumentation\n */\n\nimport type { SessionScope, Tracer } from '@graphorin/core';\nimport { withMemorySpan } from '../../internal/spans.js';\nimport type {\n ConsolidatorMemoryStoreExt,\n MemoryStoreAdapter,\n} from '../../internal/storage-adapter.js';\nimport { type SalienceWeights, salience, selectForCapacityEviction } from '../decay.js';\nimport type { NoiseFilterPreset } from '../noise-filter.js';\nimport type { PhaseOutcome } from '../types.js';\n\n/** Inputs accepted by {@link runLightPhase}. */\nexport interface LightPhaseDeps {\n readonly store: MemoryStoreAdapter;\n readonly consolidatorStore: ConsolidatorMemoryStoreExt | null;\n readonly tracer: Tracer;\n readonly scope: SessionScope;\n readonly now: () => number;\n readonly decayTauDays: number;\n readonly decayArchiveThreshold: number;\n /**\n * Capacity-bounded eviction (X-1). When non-null, the light phase\n * archives the lowest-salience live facts in the decay window down to\n * this many, in addition to the threshold archiving. `null` (the\n * default) leaves storage unbounded - behaviour identical to pre-X-1.\n */\n readonly decayCapacity: number | null;\n /** Weights for the multi-signal salience score (X-1). */\n readonly salienceWeights: SalienceWeights;\n readonly noiseFilters: ReadonlyArray<NoiseFilterPreset>;\n readonly maxBatchSize: number;\n readonly lastProcessedMessageId: string | null;\n /** The active consolidator tier - surfaced on the AISpan attribute. */\n readonly tier?: 'free' | 'cheap' | 'standard' | 'full' | 'custom';\n}\n\n/**\n * Run the light phase against the supplied scope.\n *\n * @stable\n */\nexport async function runLightPhase(deps: LightPhaseDeps): Promise<PhaseOutcome> {\n return withMemorySpan(\n deps.tracer,\n 'memory.consolidate.light',\n deps.scope,\n {\n 'consolidator.phase': 'light',\n 'consolidator.tier': deps.tier ?? 'free',\n },\n async (span) => {\n const startedAt = deps.now();\n let facts = 0;\n let archived = 0;\n let capacityEvicted = 0;\n if (typeof deps.store.semantic.listForDecay === 'function') {\n // When a capacity bound is set we widen the decay window past the\n // batch size so there is always headroom to trim the overflow; the\n // window is the LRU head (ordered by last-accessed ASC), so the\n // bound converges over repeated passes.\n const decayLimit =\n deps.decayCapacity !== null\n ? Math.max(deps.maxBatchSize, deps.decayCapacity + deps.maxBatchSize)\n : deps.maxBatchSize;\n const rows = await deps.store.semantic.listForDecay(deps.scope, decayLimit);\n facts = rows.length;\n const now = deps.now();\n // Bind once so the optional method stays narrowed across `await`\n // (and keeps its `this`); `undefined` ⇒ the store can't archive.\n const archiveFact = deps.store.semantic.archiveFact?.bind(deps.store.semantic);\n // Pass 1 - multi-signal threshold archiving. Salience folds the\n // Ebbinghaus retention curve together with the P1-2 importance hint\n // and the P1-4 security-risk negative term, so a stale, low-value,\n // or quarantined fact crosses the threshold sooner. Neutral inputs\n // collapse salience to plain retention (pre-X-1 behaviour).\n const survivors: Array<{ id: string; salience: number }> = [];\n for (const row of rows) {\n if (row.archived) continue;\n const score = salience({\n now,\n lastAccessedAt: row.lastAccessedAt,\n createdAt: row.createdAt,\n strength: row.strength,\n tauDays: deps.decayTauDays,\n importance: row.importance,\n quarantined: row.status === 'quarantined',\n foreignProvenance: isForeignProvenance(row.provenance),\n // D3: retrieval-frequency reinforcement - inert at the\n // default weight 0; adapters without the column report 0.\n accessCount: row.accessCount ?? 0,\n weights: deps.salienceWeights,\n });\n if (score < deps.decayArchiveThreshold) {\n if (archiveFact !== undefined) {\n await archiveFact(row.id, 'low_salience');\n archived += 1;\n }\n continue;\n }\n survivors.push({ id: row.id, salience: score });\n }\n // Pass 2 - capacity-bounded eviction. Archive the lowest-salience\n // survivors (security-flagged first) until the window fits.\n if (deps.decayCapacity !== null && archiveFact !== undefined) {\n const evictIds = selectForCapacityEviction(survivors, deps.decayCapacity);\n for (const id of evictIds) {\n await archiveFact(id, 'capacity_exceeded');\n archived += 1;\n capacityEvicted += 1;\n }\n }\n }\n\n // MCON-17: the light phase no longer re-reads the unconsumed batch\n // just to produce an advisory dropped-count - the standard phase\n // filters where extraction actually consumes the messages.\n const noiseFilteredCount = 0;\n\n span.setAttributes({\n 'consolidator.duration_ms': Math.max(0, deps.now() - startedAt),\n 'consolidator.facts_extracted': 0,\n 'consolidator.budget_used_usd': 0,\n 'consolidator.light.facts_seen': facts,\n 'consolidator.light.facts_archived': archived,\n 'consolidator.light.capacity_evicted': capacityEvicted,\n 'consolidator.light.noise_filtered': noiseFilteredCount,\n });\n\n return {\n phase: 'light',\n status: 'completed',\n factsCreated: 0,\n factsUpdated: archived,\n conflictsResolved: 0,\n episodesFormed: 0,\n insightsCreated: 0,\n noiseFilteredCount,\n emptyExtractions: 0,\n llmTokensUsed: 0,\n llmCostUsd: null,\n errorMessage: null,\n };\n },\n );\n}\n\n/**\n * `true` for provenance that did not originate first-party (P1-4) - used\n * to apply the mild salience penalty in capacity eviction. First-party\n * is `null` (legacy / direct write), `'user'`, and `'extraction'` (the\n * consolidator distilling the user's own session); `'tool'`,\n * `'imported'`, and `'reflection'` are treated as foreign.\n */\nfunction isForeignProvenance(provenance: string | null): boolean {\n return provenance !== null && provenance !== 'user' && provenance !== 'extraction';\n}\n"],"mappings":";;;;;;;;;AAmDA,eAAsB,cAAc,MAA6C;AAC/E,QAAO,eACL,KAAK,QACL,4BACA,KAAK,OACL;EACE,sBAAsB;EACtB,qBAAqB,KAAK,QAAQ;EACnC,EACD,OAAO,SAAS;EACd,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,QAAQ;EACZ,IAAI,WAAW;EACf,IAAI,kBAAkB;AACtB,MAAI,OAAO,KAAK,MAAM,SAAS,iBAAiB,YAAY;GAK1D,MAAM,aACJ,KAAK,kBAAkB,OACnB,KAAK,IAAI,KAAK,cAAc,KAAK,gBAAgB,KAAK,aAAa,GACnE,KAAK;GACX,MAAM,OAAO,MAAM,KAAK,MAAM,SAAS,aAAa,KAAK,OAAO,WAAW;AAC3E,WAAQ,KAAK;GACb,MAAM,MAAM,KAAK,KAAK;GAGtB,MAAM,cAAc,KAAK,MAAM,SAAS,aAAa,KAAK,KAAK,MAAM,SAAS;GAM9E,MAAMA,YAAqD,EAAE;AAC7D,QAAK,MAAM,OAAO,MAAM;AACtB,QAAI,IAAI,SAAU;IAClB,MAAM,QAAQ,SAAS;KACrB;KACA,gBAAgB,IAAI;KACpB,WAAW,IAAI;KACf,UAAU,IAAI;KACd,SAAS,KAAK;KACd,YAAY,IAAI;KAChB,aAAa,IAAI,WAAW;KAC5B,mBAAmB,oBAAoB,IAAI,WAAW;KAGtD,aAAa,IAAI,eAAe;KAChC,SAAS,KAAK;KACf,CAAC;AACF,QAAI,QAAQ,KAAK,uBAAuB;AACtC,SAAI,gBAAgB,QAAW;AAC7B,YAAM,YAAY,IAAI,IAAI,eAAe;AACzC,kBAAY;;AAEd;;AAEF,cAAU,KAAK;KAAE,IAAI,IAAI;KAAI,UAAU;KAAO,CAAC;;AAIjD,OAAI,KAAK,kBAAkB,QAAQ,gBAAgB,QAAW;IAC5D,MAAM,WAAW,0BAA0B,WAAW,KAAK,cAAc;AACzE,SAAK,MAAM,MAAM,UAAU;AACzB,WAAM,YAAY,IAAI,oBAAoB;AAC1C,iBAAY;AACZ,wBAAmB;;;;EAQzB,MAAM,qBAAqB;AAE3B,OAAK,cAAc;GACjB,4BAA4B,KAAK,IAAI,GAAG,KAAK,KAAK,GAAG,UAAU;GAC/D,gCAAgC;GAChC,gCAAgC;GAChC,iCAAiC;GACjC,qCAAqC;GACrC,uCAAuC;GACvC,qCAAqC;GACtC,CAAC;AAEF,SAAO;GACL,OAAO;GACP,QAAQ;GACR,cAAc;GACd,cAAc;GACd,mBAAmB;GACnB,gBAAgB;GAChB,iBAAiB;GACjB;GACA,kBAAkB;GAClB,eAAe;GACf,YAAY;GACZ,cAAc;GACf;GAEJ;;;;;;;;;AAUH,SAAS,oBAAoB,YAAoC;AAC/D,QAAO,eAAe,QAAQ,eAAe,UAAU,eAAe"}
|
|
@@ -20,7 +20,7 @@ const INSIGHT_SYSTEM_PROMPT = [
|
|
|
20
20
|
"JSON: { \"insight\": string }."
|
|
21
21
|
].join(" ");
|
|
22
22
|
/**
|
|
23
|
-
* Run the reflection pass. Idempotent w.r.t. failures
|
|
23
|
+
* Run the reflection pass. Idempotent w.r.t. failures - a parse miss or
|
|
24
24
|
* empty synthesis simply yields no insight. Never throws on model
|
|
25
25
|
* output; only storage errors propagate.
|
|
26
26
|
*/
|
|
@@ -121,6 +121,7 @@ async function runReflectionPass(deps) {
|
|
|
121
121
|
salience: STARTING_SALIENCE,
|
|
122
122
|
provenance: "reflection",
|
|
123
123
|
status: "quarantined",
|
|
124
|
+
owner: "agent",
|
|
124
125
|
sensitivity: "internal",
|
|
125
126
|
createdAt: iso,
|
|
126
127
|
updatedAt: iso
|
|
@@ -155,7 +156,7 @@ async function runReflectionPass(deps) {
|
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
158
|
/**
|
|
158
|
-
* Normalise an insight for exact-dedup comparison
|
|
159
|
+
* Normalise an insight for exact-dedup comparison - lowercased, punctuation
|
|
159
160
|
* stripped, whitespace collapsed. Two insights that differ only in casing or
|
|
160
161
|
* trailing punctuation collapse to the same key.
|
|
161
162
|
*
|
|
@@ -219,7 +220,7 @@ function dedupeEvidence(items) {
|
|
|
219
220
|
return out;
|
|
220
221
|
}
|
|
221
222
|
/**
|
|
222
|
-
* Parse the salient-questions response
|
|
223
|
+
* Parse the salient-questions response - accepts `{ questions: [...] }`
|
|
223
224
|
* or a bare `[...]`, tolerating chatty / fenced output. Returns the
|
|
224
225
|
* trimmed, non-empty string questions (possibly `[]`).
|
|
225
226
|
*
|
|
@@ -234,7 +235,7 @@ function parseQuestions(text) {
|
|
|
234
235
|
return arr.filter((q) => typeof q === "string" && q.trim().length > 0).map((q) => q.trim());
|
|
235
236
|
}
|
|
236
237
|
/**
|
|
237
|
-
* Parse the insight-synthesis response
|
|
238
|
+
* Parse the insight-synthesis response - accepts `{ insight: string }`
|
|
238
239
|
* (or `{ text: string }`), tolerating chatty / fenced output. Returns
|
|
239
240
|
* the trimmed insight text, or `null` when absent / empty (insufficient
|
|
240
241
|
* evidence ⇒ no insight).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reflect.js","names":["created: string[]","out: Evidence[]"],"sources":["../../../src/consolidator/phases/reflect.ts"],"sourcesContent":["/**\n * Reflection pass (P1-1) — the deep phase's higher-order synthesis\n * step, following the Generative-Agents recipe. When the accumulated\n * importance of recent episodes crosses a threshold, ask the model for\n * the few most salient questions, retrieve evidence for each, and\n * synthesize an **insight** the evidence supports — recorded\n * quarantined, `provenance: 'reflection'`, and **citing the retrieved\n * memory ids** (citations are set by the framework from the actual\n * evidence, never chosen by the model, so they can never be\n * hallucinated). Insights start at ExpeL salience `2`; the pass prunes\n * any that have decayed to `0`.\n *\n * Runs after the deep-phase conflict drain (wired in the runtime\n * dispatch), reusing the same budget / lock / run-audit envelope. The\n * conflict-resolution logic in `deep.ts` is untouched.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest, SessionScope, Tracer } from '@graphorin/core';\nimport { newMemoryId } from '../../internal/id.js';\nimport { withMemorySpan } from '../../internal/spans.js';\nimport type { InsightMemoryStoreExt } from '../../internal/storage-adapter.js';\nimport type { EpisodicMemory } from '../../tiers/episodic-memory.js';\nimport type { SemanticMemory } from '../../tiers/semantic-memory.js';\nimport type { BudgetTracker } from '../budget.js';\n\n/** ExpeL starting salience for a freshly-synthesized insight. */\nconst STARTING_SALIENCE = 2;\n\n/** How many recent episode summaries to show the salient-questions prompt. */\nconst RECENT_CONTEXT_LIMIT = 20;\n\n/** Inputs accepted by {@link runReflectionPass}. */\nexport interface ReflectionDeps {\n readonly provider: Provider;\n readonly tracer: Tracer;\n readonly scope: SessionScope;\n /** Tier used to retrieve fact evidence for each salient question. */\n readonly semantic: SemanticMemory;\n /** Importance source + episode-evidence retrieval. `null` ⇒ pass is a no-op. */\n readonly episodic: EpisodicMemory | null;\n /** Insight write + salience surface. */\n readonly insights: InsightMemoryStoreExt;\n readonly budget: BudgetTracker;\n /** Reflection fires only when accumulated episode importance ≥ this. */\n readonly importanceThreshold: number;\n /**\n * `ended_at` (epoch ms) of the newest episode a prior pass already reflected\n * on (MCON-13). The gate accumulates importance only from strictly-newer\n * episodes, so a deep run with no new episodes never re-fires. `null` /\n * absent ⇒ nothing reflected yet (accumulate over all recent episodes).\n */\n readonly reflectionWatermark?: number | null;\n /** Upper bound on salient questions asked per pass. */\n readonly maxQuestions: number;\n readonly priceUsage?: (usage: { promptTokens: number; completionTokens: number }) => number;\n /** Override the wall clock — used by tests. */\n readonly now?: () => number;\n}\n\n/** Summary returned by {@link runReflectionPass}. */\nexport interface ReflectionOutcome {\n /** True when accumulated importance crossed the threshold and the pass ran. */\n readonly triggered: boolean;\n readonly accumulatedImportance: number;\n readonly questionsAsked: number;\n readonly insightsCreated: number;\n readonly tokens: number;\n readonly costUsd: number;\n /**\n * Watermark to persist for the next pass (MCON-13): the `ended_at` (epoch\n * ms) of the newest episode this pass reflected on, or the incoming\n * watermark unchanged when the pass did not fire. The runtime persists it\n * to `consolidator_state`.\n */\n readonly nextWatermark: number | null;\n}\n\nconst QUESTIONS_SYSTEM_PROMPT = [\n 'You are the reflection step (salient-questions) of a long-running personal-assistant memory.',\n 'Given a list of recent memories, name the few most salient, high-level questions whose',\n 'answers would most improve understanding of the user. Do NOT answer them; ask only what the',\n 'memories collectively raise. Return strictly JSON: { \"questions\": string[] }.',\n].join(' ');\n\nconst INSIGHT_SYSTEM_PROMPT = [\n 'You are the reflection step (insight-synthesis) of a long-running personal-assistant memory.',\n 'Given a salient question and the supporting evidence, synthesize ONE concise, higher-order',\n 'insight that the evidence collectively supports. Ground it ONLY in the supplied evidence; if',\n 'the evidence is insufficient, return an empty string. Do NOT invent facts. Return strictly',\n 'JSON: { \"insight\": string }.',\n].join(' ');\n\ninterface Evidence {\n readonly id: string;\n readonly text: string;\n}\n\n/**\n * Run the reflection pass. Idempotent w.r.t. failures — a parse miss or\n * empty synthesis simply yields no insight. Never throws on model\n * output; only storage errors propagate.\n */\nexport async function runReflectionPass(deps: ReflectionDeps): Promise<ReflectionOutcome> {\n return withMemorySpan(\n deps.tracer,\n 'memory.consolidate.reflect',\n deps.scope,\n { 'consolidator.phase': 'reflect' },\n async (span) => {\n const empty = (over: Partial<ReflectionOutcome> = {}): ReflectionOutcome => ({\n triggered: false,\n accumulatedImportance: 0,\n questionsAsked: 0,\n insightsCreated: 0,\n tokens: 0,\n costUsd: 0,\n nextWatermark: deps.reflectionWatermark ?? null,\n ...over,\n });\n\n if (deps.episodic === null) {\n span.setAttributes({ 'consolidator.reflect.skipped': 'no-episodic' });\n return empty();\n }\n if (deps.budget.snapshot().paused) {\n span.setAttributes({ 'consolidator.reflect.skipped': 'budget' });\n return empty();\n }\n\n // 1. Accumulated-importance gate. Recency, not relevance: list the most\n // recent episodes (MCON-1 — the old `'*'` FTS probe matched zero rows\n // on real SQLite). Auto-formed episodes (P1-2) land\n // quarantined, so importance accrues there — include them. Only count\n // episodes newer than the watermark so a pass with no new episodes\n // never re-fires the paid synthesis (MCON-13).\n const recentEpisodes = await deps.episodic.listRecent(deps.scope, 50, {\n includeQuarantined: true,\n });\n const watermark = deps.reflectionWatermark ?? null;\n const fresh =\n watermark === null\n ? recentEpisodes\n : recentEpisodes.filter((e) => Date.parse(e.endedAt) > watermark);\n const accumulated = fresh.reduce((sum, episode) => sum + (episode.importance ?? 0), 0);\n span.setAttributes({\n 'consolidator.reflect.accumulated_importance': accumulated,\n 'consolidator.reflect.threshold': deps.importanceThreshold,\n });\n if (accumulated < deps.importanceThreshold) {\n span.setAttributes({ 'consolidator.reflect.triggered': 0 });\n return empty({ accumulatedImportance: accumulated });\n }\n // The newest episode this pass will reflect on — persisted as the next\n // watermark so subsequent passes skip everything up to here.\n const advancedWatermark = fresh.reduce(\n (max, episode) => Math.max(max, Date.parse(episode.endedAt)),\n watermark ?? Number.NEGATIVE_INFINITY,\n );\n\n let tokens = 0;\n let costUsd = 0;\n const record = (usage: {\n promptTokens?: number;\n completionTokens?: number;\n reasoningTokens?: number;\n }): void => {\n const t =\n (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0) + (usage.reasoningTokens ?? 0);\n const c =\n deps.priceUsage?.({\n promptTokens: usage.promptTokens ?? 0,\n completionTokens: usage.completionTokens ?? 0,\n }) ?? 0;\n tokens += t;\n costUsd += c;\n deps.budget.record({ phase: 'deep', tokens: t, costUsd: c });\n };\n\n // 2. Salient questions over the new (post-watermark) memories.\n const recentSummaries = fresh\n .slice(0, RECENT_CONTEXT_LIMIT)\n .map((episode) => `- ${episode.summary}`)\n .join('\\n');\n const questionsRes = await deps.provider.generate(\n buildQuestionsRequest(deps, recentSummaries),\n );\n record(questionsRes.usage);\n const questions = parseQuestions(questionsRes.text).slice(0, Math.max(0, deps.maxQuestions));\n span.setAttributes({ 'consolidator.reflect.questions': questions.length });\n\n // Pre-filter new insights against already-stored ones so a re-fire over\n // overlapping evidence does not write near-duplicates (MCON-13). Offline\n // and embedding-free: normalised exact match, the insight analogue of\n // the conflict pipeline's stage-1 dedup.\n const seenInsights = new Set(\n (await deps.insights.list(deps.scope, { includeQuarantined: true, limit: 200 })).map((i) =>\n normalizeInsightText(i.text),\n ),\n );\n\n const nowFn = typeof deps.now === 'function' ? deps.now : Date.now;\n let insightsCreated = 0;\n const created: string[] = [];\n for (const question of questions) {\n if (deps.budget.snapshot().paused) break;\n\n // 3. Retrieve evidence. Facts are trustworthy (active only);\n // episodes include the quarantined auto-formed ones.\n const factHits = await deps.semantic.search(deps.scope, question, { topK: 5 });\n const epHits = await deps.episodic.search(deps.scope, question, {\n topK: 5,\n includeQuarantined: true,\n });\n const evidence = dedupeEvidence([\n ...factHits.map((h) => ({ id: h.record.id, text: h.record.text })),\n ...epHits.map((h) => ({ id: h.record.id, text: h.record.summary })),\n ]);\n // Citations are mandatory — no evidence ⇒ no insight.\n if (evidence.length === 0) continue;\n\n // 4. Synthesize an insight grounded in that evidence.\n if (deps.budget.snapshot().paused) break;\n const insightRes = await deps.provider.generate(\n buildInsightRequest(deps, question, evidence),\n );\n record(insightRes.usage);\n const text = parseInsight(insightRes.text);\n if (text === null) continue;\n\n // Skip near-duplicates of an already-stored insight (and of one just\n // written this pass).\n const normalized = normalizeInsightText(text);\n if (seenInsights.has(normalized)) continue;\n seenInsights.add(normalized);\n\n const iso = new Date(nowFn()).toISOString();\n const insightId = newMemoryId('ins');\n await deps.insights.insert({\n id: insightId,\n kind: 'insight',\n userId: deps.scope.userId,\n ...(deps.scope.sessionId !== undefined ? { sessionId: deps.scope.sessionId } : {}),\n ...(deps.scope.agentId !== undefined ? { agentId: deps.scope.agentId } : {}),\n text,\n // cites = the actual retrieved evidence ids (framework-set).\n cites: evidence.map((e) => e.id),\n salience: STARTING_SALIENCE,\n provenance: 'reflection',\n status: 'quarantined',\n sensitivity: 'internal',\n createdAt: iso,\n updatedAt: iso,\n });\n created.push(insightId);\n insightsCreated += 1;\n }\n\n // 5. ExpeL forgetting (MCON-16): decay every existing insight by 1\n // per reflection pass. Retrieval bumps +1 (InsightMemory.search),\n // so an insight recalled since the last pass nets level-or-better\n // while an unused one slides toward the prune floor — starting\n // salience 2 ⇒ pruned after two idle passes. Fresh insights from\n // THIS pass are exempt (they were inserted above at full salience\n // and have had no retrieval window yet).\n const freshIds = new Set(created);\n for (const existing of await deps.insights.list(deps.scope, {\n includeQuarantined: true,\n limit: 500,\n })) {\n if (freshIds.has(existing.id)) continue;\n await deps.insights.bumpSalience(existing.id, -1, 'reflection-pass-decay');\n }\n await deps.insights.prune(deps.scope);\n\n span.setAttributes({\n 'consolidator.reflect.triggered': 1,\n 'consolidator.reflect.insights_created': insightsCreated,\n 'consolidator.reflect.tokens': tokens,\n 'consolidator.reflect.cost.usd': costUsd,\n });\n return {\n triggered: true,\n accumulatedImportance: accumulated,\n questionsAsked: questions.length,\n insightsCreated,\n tokens,\n costUsd,\n // Advance past everything reflected on this pass. `advancedWatermark`\n // is finite here (the gate fired ⇒ `fresh` is non-empty).\n nextWatermark: Number.isFinite(advancedWatermark) ? advancedWatermark : watermark,\n };\n },\n );\n}\n\n/**\n * Normalise an insight for exact-dedup comparison — lowercased, punctuation\n * stripped, whitespace collapsed. Two insights that differ only in casing or\n * trailing punctuation collapse to the same key.\n *\n * @internal\n */\nexport function normalizeInsightText(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}\\s]/gu, ' ')\n .replace(/\\s+/gu, ' ')\n .trim();\n}\n\nfunction buildQuestionsRequest(deps: ReflectionDeps, recentSummaries: string): ProviderRequest {\n const userBlock = [\n 'Recent memories:',\n recentSummaries.length > 0 ? recentSummaries : '(none)',\n '',\n `Name up to ${deps.maxQuestions} salient questions.`,\n ].join('\\n');\n return {\n messages: [{ role: 'user', content: userBlock }],\n systemMessage: QUESTIONS_SYSTEM_PROMPT,\n temperature: 0,\n // MCON-14: per-call output cap — a short question list.\n maxTokens: 512,\n metadata: scopeMetadata(deps.scope),\n outputType: { kind: 'structured' },\n };\n}\n\nfunction buildInsightRequest(\n deps: ReflectionDeps,\n question: string,\n evidence: ReadonlyArray<Evidence>,\n): ProviderRequest {\n const evidenceBlock = evidence.map((e, i) => `[${i + 1}] (id=${e.id}) ${e.text}`).join('\\n');\n const userBlock = [`Question: ${question}`, '', 'Evidence:', evidenceBlock].join('\\n');\n return {\n messages: [{ role: 'user', content: userBlock }],\n systemMessage: INSIGHT_SYSTEM_PROMPT,\n temperature: 0,\n // MCON-14: per-call output cap — one insight object.\n maxTokens: 512,\n metadata: scopeMetadata(deps.scope),\n outputType: { kind: 'structured' },\n };\n}\n\nfunction scopeMetadata(scope: SessionScope): Record<string, string> {\n return {\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n };\n}\n\nfunction dedupeEvidence(items: ReadonlyArray<Evidence>): Evidence[] {\n const seen = new Set<string>();\n const out: Evidence[] = [];\n for (const e of items) {\n if (e.id.length === 0 || seen.has(e.id)) continue;\n seen.add(e.id);\n out.push(e);\n }\n return out;\n}\n\n/**\n * Parse the salient-questions response — accepts `{ questions: [...] }`\n * or a bare `[...]`, tolerating chatty / fenced output. Returns the\n * trimmed, non-empty string questions (possibly `[]`).\n *\n * @internal\n */\nexport function parseQuestions(text: string | undefined): string[] {\n if (text === undefined || text.length === 0) return [];\n const candidate = stripFence(text).trim();\n const parsed = tryParse(candidate);\n if (parsed === undefined) return [];\n const arr = Array.isArray(parsed)\n ? parsed\n : isRecord(parsed) && Array.isArray(parsed.questions)\n ? parsed.questions\n : null;\n if (arr === null) return [];\n return arr\n .filter((q): q is string => typeof q === 'string' && q.trim().length > 0)\n .map((q) => q.trim());\n}\n\n/**\n * Parse the insight-synthesis response — accepts `{ insight: string }`\n * (or `{ text: string }`), tolerating chatty / fenced output. Returns\n * the trimmed insight text, or `null` when absent / empty (insufficient\n * evidence ⇒ no insight).\n *\n * @internal\n */\nexport function parseInsight(text: string | undefined): string | null {\n if (text === undefined || text.length === 0) return null;\n const candidate = stripFence(text).trim();\n const parsed = tryParse(candidate);\n if (!isRecord(parsed)) return null;\n const raw = typeof parsed.insight === 'string' ? parsed.insight : null;\n const fallback = typeof parsed.text === 'string' ? parsed.text : null;\n const value = raw ?? fallback;\n if (value === null || value.trim().length === 0) return null;\n return value.trim();\n}\n\nfunction tryParse(candidate: string): unknown {\n if (candidate.length === 0) return undefined;\n try {\n return JSON.parse(candidate);\n } catch {\n const obj = sliceBetween(candidate, '{', '}');\n if (obj !== null) {\n try {\n return JSON.parse(obj);\n } catch {\n // fall through to array attempt\n }\n }\n const arr = sliceBetween(candidate, '[', ']');\n if (arr !== null) {\n try {\n return JSON.parse(arr);\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\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 sliceBetween(text: string, open: string, close: string): string | null {\n const start = text.indexOf(open);\n const end = text.lastIndexOf(close);\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":"reflect.js","names":["created: string[]","out: Evidence[]"],"sources":["../../../src/consolidator/phases/reflect.ts"],"sourcesContent":["/**\n * Reflection pass (P1-1) - the deep phase's higher-order synthesis\n * step, following the Generative-Agents recipe. When the accumulated\n * importance of recent episodes crosses a threshold, ask the model for\n * the few most salient questions, retrieve evidence for each, and\n * synthesize an **insight** the evidence supports - recorded\n * quarantined, `provenance: 'reflection'`, and **citing the retrieved\n * memory ids** (citations are set by the framework from the actual\n * evidence, never chosen by the model, so they can never be\n * hallucinated). Insights start at ExpeL salience `2`; the pass prunes\n * any that have decayed to `0`.\n *\n * Runs after the deep-phase conflict drain (wired in the runtime\n * dispatch), reusing the same budget / lock / run-audit envelope. The\n * conflict-resolution logic in `deep.ts` is untouched.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest, SessionScope, Tracer } from '@graphorin/core';\nimport { newMemoryId } from '../../internal/id.js';\nimport { withMemorySpan } from '../../internal/spans.js';\nimport type { InsightMemoryStoreExt } from '../../internal/storage-adapter.js';\nimport type { EpisodicMemory } from '../../tiers/episodic-memory.js';\nimport type { SemanticMemory } from '../../tiers/semantic-memory.js';\nimport type { BudgetTracker } from '../budget.js';\n\n/** ExpeL starting salience for a freshly-synthesized insight. */\nconst STARTING_SALIENCE = 2;\n\n/** How many recent episode summaries to show the salient-questions prompt. */\nconst RECENT_CONTEXT_LIMIT = 20;\n\n/** Inputs accepted by {@link runReflectionPass}. */\nexport interface ReflectionDeps {\n readonly provider: Provider;\n readonly tracer: Tracer;\n readonly scope: SessionScope;\n /** Tier used to retrieve fact evidence for each salient question. */\n readonly semantic: SemanticMemory;\n /** Importance source + episode-evidence retrieval. `null` ⇒ pass is a no-op. */\n readonly episodic: EpisodicMemory | null;\n /** Insight write + salience surface. */\n readonly insights: InsightMemoryStoreExt;\n readonly budget: BudgetTracker;\n /** Reflection fires only when accumulated episode importance ≥ this. */\n readonly importanceThreshold: number;\n /**\n * `ended_at` (epoch ms) of the newest episode a prior pass already reflected\n * on (MCON-13). The gate accumulates importance only from strictly-newer\n * episodes, so a deep run with no new episodes never re-fires. `null` /\n * absent ⇒ nothing reflected yet (accumulate over all recent episodes).\n */\n readonly reflectionWatermark?: number | null;\n /** Upper bound on salient questions asked per pass. */\n readonly maxQuestions: number;\n readonly priceUsage?: (usage: { promptTokens: number; completionTokens: number }) => number;\n /** Override the wall clock - used by tests. */\n readonly now?: () => number;\n}\n\n/** Summary returned by {@link runReflectionPass}. */\nexport interface ReflectionOutcome {\n /** True when accumulated importance crossed the threshold and the pass ran. */\n readonly triggered: boolean;\n readonly accumulatedImportance: number;\n readonly questionsAsked: number;\n readonly insightsCreated: number;\n readonly tokens: number;\n readonly costUsd: number;\n /**\n * Watermark to persist for the next pass (MCON-13): the `ended_at` (epoch\n * ms) of the newest episode this pass reflected on, or the incoming\n * watermark unchanged when the pass did not fire. The runtime persists it\n * to `consolidator_state`.\n */\n readonly nextWatermark: number | null;\n}\n\nconst QUESTIONS_SYSTEM_PROMPT = [\n 'You are the reflection step (salient-questions) of a long-running personal-assistant memory.',\n 'Given a list of recent memories, name the few most salient, high-level questions whose',\n 'answers would most improve understanding of the user. Do NOT answer them; ask only what the',\n 'memories collectively raise. Return strictly JSON: { \"questions\": string[] }.',\n].join(' ');\n\nconst INSIGHT_SYSTEM_PROMPT = [\n 'You are the reflection step (insight-synthesis) of a long-running personal-assistant memory.',\n 'Given a salient question and the supporting evidence, synthesize ONE concise, higher-order',\n 'insight that the evidence collectively supports. Ground it ONLY in the supplied evidence; if',\n 'the evidence is insufficient, return an empty string. Do NOT invent facts. Return strictly',\n 'JSON: { \"insight\": string }.',\n].join(' ');\n\ninterface Evidence {\n readonly id: string;\n readonly text: string;\n}\n\n/**\n * Run the reflection pass. Idempotent w.r.t. failures - a parse miss or\n * empty synthesis simply yields no insight. Never throws on model\n * output; only storage errors propagate.\n */\nexport async function runReflectionPass(deps: ReflectionDeps): Promise<ReflectionOutcome> {\n return withMemorySpan(\n deps.tracer,\n 'memory.consolidate.reflect',\n deps.scope,\n { 'consolidator.phase': 'reflect' },\n async (span) => {\n const empty = (over: Partial<ReflectionOutcome> = {}): ReflectionOutcome => ({\n triggered: false,\n accumulatedImportance: 0,\n questionsAsked: 0,\n insightsCreated: 0,\n tokens: 0,\n costUsd: 0,\n nextWatermark: deps.reflectionWatermark ?? null,\n ...over,\n });\n\n if (deps.episodic === null) {\n span.setAttributes({ 'consolidator.reflect.skipped': 'no-episodic' });\n return empty();\n }\n if (deps.budget.snapshot().paused) {\n span.setAttributes({ 'consolidator.reflect.skipped': 'budget' });\n return empty();\n }\n\n // 1. Accumulated-importance gate. Recency, not relevance: list the most\n // recent episodes (MCON-1 - the old `'*'` FTS probe matched zero rows\n // on real SQLite). Auto-formed episodes (P1-2) land\n // quarantined, so importance accrues there - include them. Only count\n // episodes newer than the watermark so a pass with no new episodes\n // never re-fires the paid synthesis (MCON-13).\n const recentEpisodes = await deps.episodic.listRecent(deps.scope, 50, {\n includeQuarantined: true,\n });\n const watermark = deps.reflectionWatermark ?? null;\n const fresh =\n watermark === null\n ? recentEpisodes\n : recentEpisodes.filter((e) => Date.parse(e.endedAt) > watermark);\n const accumulated = fresh.reduce((sum, episode) => sum + (episode.importance ?? 0), 0);\n span.setAttributes({\n 'consolidator.reflect.accumulated_importance': accumulated,\n 'consolidator.reflect.threshold': deps.importanceThreshold,\n });\n if (accumulated < deps.importanceThreshold) {\n span.setAttributes({ 'consolidator.reflect.triggered': 0 });\n return empty({ accumulatedImportance: accumulated });\n }\n // The newest episode this pass will reflect on - persisted as the next\n // watermark so subsequent passes skip everything up to here.\n const advancedWatermark = fresh.reduce(\n (max, episode) => Math.max(max, Date.parse(episode.endedAt)),\n watermark ?? Number.NEGATIVE_INFINITY,\n );\n\n let tokens = 0;\n let costUsd = 0;\n const record = (usage: {\n promptTokens?: number;\n completionTokens?: number;\n reasoningTokens?: number;\n }): void => {\n const t =\n (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0) + (usage.reasoningTokens ?? 0);\n const c =\n deps.priceUsage?.({\n promptTokens: usage.promptTokens ?? 0,\n completionTokens: usage.completionTokens ?? 0,\n }) ?? 0;\n tokens += t;\n costUsd += c;\n deps.budget.record({ phase: 'deep', tokens: t, costUsd: c });\n };\n\n // 2. Salient questions over the new (post-watermark) memories.\n const recentSummaries = fresh\n .slice(0, RECENT_CONTEXT_LIMIT)\n .map((episode) => `- ${episode.summary}`)\n .join('\\n');\n const questionsRes = await deps.provider.generate(\n buildQuestionsRequest(deps, recentSummaries),\n );\n record(questionsRes.usage);\n const questions = parseQuestions(questionsRes.text).slice(0, Math.max(0, deps.maxQuestions));\n span.setAttributes({ 'consolidator.reflect.questions': questions.length });\n\n // Pre-filter new insights against already-stored ones so a re-fire over\n // overlapping evidence does not write near-duplicates (MCON-13). Offline\n // and embedding-free: normalised exact match, the insight analogue of\n // the conflict pipeline's stage-1 dedup.\n const seenInsights = new Set(\n (await deps.insights.list(deps.scope, { includeQuarantined: true, limit: 200 })).map((i) =>\n normalizeInsightText(i.text),\n ),\n );\n\n const nowFn = typeof deps.now === 'function' ? deps.now : Date.now;\n let insightsCreated = 0;\n const created: string[] = [];\n for (const question of questions) {\n if (deps.budget.snapshot().paused) break;\n\n // 3. Retrieve evidence. Facts are trustworthy (active only);\n // episodes include the quarantined auto-formed ones.\n const factHits = await deps.semantic.search(deps.scope, question, { topK: 5 });\n const epHits = await deps.episodic.search(deps.scope, question, {\n topK: 5,\n includeQuarantined: true,\n });\n const evidence = dedupeEvidence([\n ...factHits.map((h) => ({ id: h.record.id, text: h.record.text })),\n ...epHits.map((h) => ({ id: h.record.id, text: h.record.summary })),\n ]);\n // Citations are mandatory - no evidence ⇒ no insight.\n if (evidence.length === 0) continue;\n\n // 4. Synthesize an insight grounded in that evidence.\n if (deps.budget.snapshot().paused) break;\n const insightRes = await deps.provider.generate(\n buildInsightRequest(deps, question, evidence),\n );\n record(insightRes.usage);\n const text = parseInsight(insightRes.text);\n if (text === null) continue;\n\n // Skip near-duplicates of an already-stored insight (and of one just\n // written this pass).\n const normalized = normalizeInsightText(text);\n if (seenInsights.has(normalized)) continue;\n seenInsights.add(normalized);\n\n const iso = new Date(nowFn()).toISOString();\n const insightId = newMemoryId('ins');\n await deps.insights.insert({\n id: insightId,\n kind: 'insight',\n userId: deps.scope.userId,\n ...(deps.scope.sessionId !== undefined ? { sessionId: deps.scope.sessionId } : {}),\n ...(deps.scope.agentId !== undefined ? { agentId: deps.scope.agentId } : {}),\n text,\n // cites = the actual retrieved evidence ids (framework-set).\n cites: evidence.map((e) => e.id),\n salience: STARTING_SALIENCE,\n provenance: 'reflection',\n status: 'quarantined',\n // D3: a synthesized insight is the agent's own inference.\n owner: 'agent',\n sensitivity: 'internal',\n createdAt: iso,\n updatedAt: iso,\n });\n created.push(insightId);\n insightsCreated += 1;\n }\n\n // 5. ExpeL forgetting (MCON-16): decay every existing insight by 1\n // per reflection pass. Retrieval bumps +1 (InsightMemory.search),\n // so an insight recalled since the last pass nets level-or-better\n // while an unused one slides toward the prune floor - starting\n // salience 2 ⇒ pruned after two idle passes. Fresh insights from\n // THIS pass are exempt (they were inserted above at full salience\n // and have had no retrieval window yet).\n const freshIds = new Set(created);\n for (const existing of await deps.insights.list(deps.scope, {\n includeQuarantined: true,\n limit: 500,\n })) {\n if (freshIds.has(existing.id)) continue;\n await deps.insights.bumpSalience(existing.id, -1, 'reflection-pass-decay');\n }\n await deps.insights.prune(deps.scope);\n\n span.setAttributes({\n 'consolidator.reflect.triggered': 1,\n 'consolidator.reflect.insights_created': insightsCreated,\n 'consolidator.reflect.tokens': tokens,\n 'consolidator.reflect.cost.usd': costUsd,\n });\n return {\n triggered: true,\n accumulatedImportance: accumulated,\n questionsAsked: questions.length,\n insightsCreated,\n tokens,\n costUsd,\n // Advance past everything reflected on this pass. `advancedWatermark`\n // is finite here (the gate fired ⇒ `fresh` is non-empty).\n nextWatermark: Number.isFinite(advancedWatermark) ? advancedWatermark : watermark,\n };\n },\n );\n}\n\n/**\n * Normalise an insight for exact-dedup comparison - lowercased, punctuation\n * stripped, whitespace collapsed. Two insights that differ only in casing or\n * trailing punctuation collapse to the same key.\n *\n * @internal\n */\nexport function normalizeInsightText(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}\\s]/gu, ' ')\n .replace(/\\s+/gu, ' ')\n .trim();\n}\n\nfunction buildQuestionsRequest(deps: ReflectionDeps, recentSummaries: string): ProviderRequest {\n const userBlock = [\n 'Recent memories:',\n recentSummaries.length > 0 ? recentSummaries : '(none)',\n '',\n `Name up to ${deps.maxQuestions} salient questions.`,\n ].join('\\n');\n return {\n messages: [{ role: 'user', content: userBlock }],\n systemMessage: QUESTIONS_SYSTEM_PROMPT,\n temperature: 0,\n // MCON-14: per-call output cap - a short question list.\n maxTokens: 512,\n metadata: scopeMetadata(deps.scope),\n outputType: { kind: 'structured' },\n };\n}\n\nfunction buildInsightRequest(\n deps: ReflectionDeps,\n question: string,\n evidence: ReadonlyArray<Evidence>,\n): ProviderRequest {\n const evidenceBlock = evidence.map((e, i) => `[${i + 1}] (id=${e.id}) ${e.text}`).join('\\n');\n const userBlock = [`Question: ${question}`, '', 'Evidence:', evidenceBlock].join('\\n');\n return {\n messages: [{ role: 'user', content: userBlock }],\n systemMessage: INSIGHT_SYSTEM_PROMPT,\n temperature: 0,\n // MCON-14: per-call output cap - one insight object.\n maxTokens: 512,\n metadata: scopeMetadata(deps.scope),\n outputType: { kind: 'structured' },\n };\n}\n\nfunction scopeMetadata(scope: SessionScope): Record<string, string> {\n return {\n userId: scope.userId,\n ...(scope.sessionId !== undefined ? { sessionId: scope.sessionId } : {}),\n ...(scope.agentId !== undefined ? { agentId: scope.agentId } : {}),\n };\n}\n\nfunction dedupeEvidence(items: ReadonlyArray<Evidence>): Evidence[] {\n const seen = new Set<string>();\n const out: Evidence[] = [];\n for (const e of items) {\n if (e.id.length === 0 || seen.has(e.id)) continue;\n seen.add(e.id);\n out.push(e);\n }\n return out;\n}\n\n/**\n * Parse the salient-questions response - accepts `{ questions: [...] }`\n * or a bare `[...]`, tolerating chatty / fenced output. Returns the\n * trimmed, non-empty string questions (possibly `[]`).\n *\n * @internal\n */\nexport function parseQuestions(text: string | undefined): string[] {\n if (text === undefined || text.length === 0) return [];\n const candidate = stripFence(text).trim();\n const parsed = tryParse(candidate);\n if (parsed === undefined) return [];\n const arr = Array.isArray(parsed)\n ? parsed\n : isRecord(parsed) && Array.isArray(parsed.questions)\n ? parsed.questions\n : null;\n if (arr === null) return [];\n return arr\n .filter((q): q is string => typeof q === 'string' && q.trim().length > 0)\n .map((q) => q.trim());\n}\n\n/**\n * Parse the insight-synthesis response - accepts `{ insight: string }`\n * (or `{ text: string }`), tolerating chatty / fenced output. Returns\n * the trimmed insight text, or `null` when absent / empty (insufficient\n * evidence ⇒ no insight).\n *\n * @internal\n */\nexport function parseInsight(text: string | undefined): string | null {\n if (text === undefined || text.length === 0) return null;\n const candidate = stripFence(text).trim();\n const parsed = tryParse(candidate);\n if (!isRecord(parsed)) return null;\n const raw = typeof parsed.insight === 'string' ? parsed.insight : null;\n const fallback = typeof parsed.text === 'string' ? parsed.text : null;\n const value = raw ?? fallback;\n if (value === null || value.trim().length === 0) return null;\n return value.trim();\n}\n\nfunction tryParse(candidate: string): unknown {\n if (candidate.length === 0) return undefined;\n try {\n return JSON.parse(candidate);\n } catch {\n const obj = sliceBetween(candidate, '{', '}');\n if (obj !== null) {\n try {\n return JSON.parse(obj);\n } catch {\n // fall through to array attempt\n }\n }\n const arr = sliceBetween(candidate, '[', ']');\n if (arr !== null) {\n try {\n return JSON.parse(arr);\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\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 sliceBetween(text: string, open: string, close: string): string | null {\n const start = text.indexOf(open);\n const end = text.lastIndexOf(close);\n if (start < 0 || end < start) return null;\n return text.slice(start, end + 1);\n}\n"],"mappings":"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"}
|
|
@@ -13,6 +13,7 @@ const EXTRACTION_SYSTEM_PROMPT = [
|
|
|
13
13
|
"You are a memory-extraction assistant for a long-running personal-assistant runtime.",
|
|
14
14
|
"Read the supplied conversation slice and return the durable facts it asserts about the user, the world, or stable preferences.",
|
|
15
15
|
"Skip greetings, banter, transient state, and anything the assistant produced as boilerplate.",
|
|
16
|
+
"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.",
|
|
16
17
|
"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).",
|
|
17
18
|
"Return a single JSON object: { \"facts\": [{ \"text\": string, \"subject\"?: string, \"predicate\"?: string, \"object\"?: string, \"confidence\"?: number, \"importance\"?: number }] }.",
|
|
18
19
|
"If the slice contains no durable facts, return { \"facts\": [] }."
|
|
@@ -28,12 +29,12 @@ const EXTRACTION_MAX_TOKENS = 1024;
|
|
|
28
29
|
const EPISODE_MAX_TOKENS = 512;
|
|
29
30
|
const EPISODE_SUMMARY_SYSTEM_PROMPT = [
|
|
30
31
|
"You are an episode-summarization assistant for a long-running personal-assistant runtime.",
|
|
31
|
-
"Read the supplied conversation slice and write one concise third-person summary of what happened
|
|
32
|
+
"Read the supplied conversation slice and write one concise third-person summary of what happened - the episode.",
|
|
32
33
|
"Return a single JSON object: { \"summary\": string }."
|
|
33
34
|
].join(" ");
|
|
34
35
|
const EPISODE_SUMMARY_IMPORTANCE_SYSTEM_PROMPT = [
|
|
35
36
|
"You are an episode-summarization assistant for a long-running personal-assistant runtime.",
|
|
36
|
-
"Read the supplied conversation slice and write one concise third-person summary of what happened
|
|
37
|
+
"Read the supplied conversation slice and write one concise third-person summary of what happened - the episode.",
|
|
37
38
|
"Also rate how important / poignant this episode is for remembering the user, on an integer scale from 1 (mundane) to 10 (deeply significant).",
|
|
38
39
|
"Return a single JSON object: { \"summary\": string, \"importance\": number }."
|
|
39
40
|
].join(" ");
|
|
@@ -109,7 +110,15 @@ async function runStandardPhase(deps) {
|
|
|
109
110
|
if (fact.text.trim().length === 0) continue;
|
|
110
111
|
const input = buildFactInput(fact);
|
|
111
112
|
const neighbors = await deps.semantic.neighbors(deps.scope, fact.text, { topK: RECONCILE_TOP_K });
|
|
112
|
-
|
|
113
|
+
let route = await preFilterCandidate(fact.text, neighbors);
|
|
114
|
+
if (route.route !== "noop" && neighbors.length === 0) {
|
|
115
|
+
const exactId = await findExactTextDuplicate(deps.semantic, deps.scope, fact.text);
|
|
116
|
+
if (exactId !== null) route = {
|
|
117
|
+
route: "noop",
|
|
118
|
+
targetId: exactId,
|
|
119
|
+
reason: "exact-text-fts"
|
|
120
|
+
};
|
|
121
|
+
}
|
|
113
122
|
let decision;
|
|
114
123
|
let audited;
|
|
115
124
|
if (route.route === "reconcile" && !deps.budget.snapshot().paused) {
|
|
@@ -154,7 +163,7 @@ async function runStandardPhase(deps) {
|
|
|
154
163
|
};
|
|
155
164
|
audited = {
|
|
156
165
|
kind: "dedup",
|
|
157
|
-
stage: route.reason === "exact-hash-match" ? "exact-dedup" : "embedding-three-zone",
|
|
166
|
+
stage: route.reason === "exact-hash-match" || route.reason === "exact-text-fts" ? "exact-dedup" : "embedding-three-zone",
|
|
158
167
|
existingId: route.targetId,
|
|
159
168
|
...route.similarity !== void 0 ? { similarity: route.similarity } : {},
|
|
160
169
|
reason: route.reason
|
|
@@ -250,7 +259,8 @@ async function runStandardPhase(deps) {
|
|
|
250
259
|
endedAt,
|
|
251
260
|
...importance !== void 0 ? { importance } : {},
|
|
252
261
|
provenance: "extraction",
|
|
253
|
-
status: "quarantined"
|
|
262
|
+
status: "quarantined",
|
|
263
|
+
owner: "agent"
|
|
254
264
|
});
|
|
255
265
|
episodesFormed = 1;
|
|
256
266
|
}
|
|
@@ -293,12 +303,13 @@ async function runStandardPhase(deps) {
|
|
|
293
303
|
});
|
|
294
304
|
}
|
|
295
305
|
function buildRequest(deps, transcript) {
|
|
306
|
+
const today = new Date(deps.now?.() ?? Date.now()).toISOString().slice(0, 10);
|
|
296
307
|
return {
|
|
297
308
|
messages: [{
|
|
298
309
|
role: "user",
|
|
299
310
|
content: `Conversation slice:\n${transcript}`
|
|
300
311
|
}],
|
|
301
|
-
systemMessage: EXTRACTION_SYSTEM_PROMPT,
|
|
312
|
+
systemMessage: `${EXTRACTION_SYSTEM_PROMPT} Today is ${today}. Each transcript line carries its timestamp in parentheses; resolve relative dates ("next Friday", "last month") into absolute ISO dates in the extracted fact text.`,
|
|
302
313
|
temperature: 0,
|
|
303
314
|
maxTokens: EXTRACTION_MAX_TOKENS,
|
|
304
315
|
metadata: {
|
|
@@ -336,7 +347,8 @@ function renderTranscript(messages) {
|
|
|
336
347
|
return messages.map((m) => {
|
|
337
348
|
const role = m.message.role;
|
|
338
349
|
const text = renderText(m.message);
|
|
339
|
-
|
|
350
|
+
const stamp = typeof m.createdAt === "string" && m.createdAt.length > 0 ? m.createdAt : "";
|
|
351
|
+
return stamp.length > 0 ? `[${m.sequence}] (${stamp}) ${role}: ${text}` : `[${m.sequence}] ${role}: ${text}`;
|
|
340
352
|
}).join("\n");
|
|
341
353
|
}
|
|
342
354
|
/**
|
|
@@ -442,15 +454,32 @@ function sliceJsonObject(text) {
|
|
|
442
454
|
}
|
|
443
455
|
/**
|
|
444
456
|
* Build the {@link FactInput} for an extracted candidate. P1-4:
|
|
445
|
-
* distilled-from-transcript facts are synthesized memory
|
|
457
|
+
* distilled-from-transcript facts are synthesized memory - tagged
|
|
446
458
|
* `extraction` so they land quarantined (excluded from action-driving
|
|
447
459
|
* recall until validated).
|
|
448
460
|
*/
|
|
461
|
+
/**
|
|
462
|
+
* Embedder-independent exact-duplicate lookup
|
|
463
|
+
* (memory-consolidation-07): FTS top hits + strict string equality.
|
|
464
|
+
* Returns the existing fact's id or `null`. Failures are swallowed -
|
|
465
|
+
* the guard is an optimization, never a reason to fail the slice.
|
|
466
|
+
*/
|
|
467
|
+
async function findExactTextDuplicate(semantic, scope, text) {
|
|
468
|
+
try {
|
|
469
|
+
return (await semantic.search(scope, text, {
|
|
470
|
+
topK: 5,
|
|
471
|
+
includeQuarantined: true
|
|
472
|
+
})).find((h) => h.record.text === text)?.record.id ?? null;
|
|
473
|
+
} catch {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
449
477
|
function buildFactInput(fact) {
|
|
450
478
|
const importance = normalizeImportance(fact.importance);
|
|
451
479
|
return {
|
|
452
480
|
text: fact.text,
|
|
453
481
|
provenance: "extraction",
|
|
482
|
+
owner: "agent",
|
|
454
483
|
...fact.subject !== void 0 ? { subject: fact.subject } : {},
|
|
455
484
|
...fact.predicate !== void 0 ? { predicate: fact.predicate } : {},
|
|
456
485
|
...fact.object !== void 0 ? { object: fact.object } : {},
|
|
@@ -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) {
|