feat(kg): automatic self-curation — durable coverage grows + duplicates auto-merge - #322
Merged
Conversation
Three follow-ups so the durable tier actually stops re-learning:
- Render DURABLE insights at full length (2000 vs the fuzzy 300 cap) in both
toRecalledInsight and renderRecallBlocks, marked via RecalledInsight.durable.
The agent was getting a 300-char stub of the curated schema and re-running
dynamics_describe for fields it already had.
- Orchestrator prompt: when curated schema/reference knowledge is in the
recalled context, trust it and do NOT re-run discovery tools (dynamics_describe)
for those entities — go straight to dynamics_query.
- Ingest hygiene now gates ALL auto-harvest (not just durable promotion):
first-person agent narration ("Ich schaue kurz in den Memory…") is dropped
entirely instead of stored as a fuzzy MK that re-pollutes recall every
session. Length is NOT a gate for fuzzy (short facts stay); the durable tier
keeps its own length floor.
Tests: +narration-skip + short-fact-kept (promoteTurnIfSignificant) +
durable-full-render (durableRecall). 21/21 durable+promotion green, build+lint clean.
Verified live on omadia-test: redundant dynamics_describe calls ~halved
(11 → 6) for a course query; durable schema surfaces full-length. Remaining
re-discovery is for booking/participant entities not yet curated as durable
(needs backfill coverage — separate).
Generalizes the durable-knowledge lifecycle so it self-curates on ANY deployment, with no per-instance backfill/cleanup scripts. GAP #1 — durable coverage grows automatically: - excerptExtractor + significanceScorer now recognize learned data-model / schema / domain conventions (entities, field names, entity-sets, joins) as kind=reference + high significance (>=0.85). Such turns auto-promote to the durable tier (T3), so once the agent learns e.g. the booking/participant schema it becomes durable and is never re-discovered. Time-bound data snapshots stay mid-significance insights (not durable). GAP #2 — duplicates stop accumulating automatically: - the merge detector (already fires on every MK create) now RESOLVES high-confidence pairs itself instead of only flagging for an operator. Aggressive default threshold 0.90 (env kg_auto_merge_threshold) so paraphrased re-statements merge too; flag floor lowered to the threshold so sub-0.95 near-dups actually surface. SAFETY: a durable (manuallyAuthored) node is NEVER deleted; when exactly one side is durable it wins; both-durable is left for an operator; otherwise the OLDER node wins. Config-gated (kg_auto_merge_enabled, default on) + startup telemetry. Tests: test/autoMerge.test.ts (durable-protected, durable-as-source, both-durable-skip, older-wins, flag-only-without-threshold). 31/31 merge + build + lint green.
buildOrchestrator forwarded autoPromote+threshold but dropped autoPromoteDurableMinSignificance/Kinds — so registry-built agents (the default 'fallback') promoted only to the fuzzy tier, never durable, even though orchestratorDeps already carried the value (plugin.ts:562) and the static chatAgent@1 had it (durableAutoPromote=on@0.85). Now forwarded, so EVERY agent auto-promotes learned schema/reference knowledge to the durable tier. (Same class as the earlier 'missing modelRouting in registry-managed orchestrators' fix.)
CI 'audit (high+critical block)' was red on middleware: hono <=4.12.24 (high). `npm audit fix` (semver-compatible, no --force) bumps it in the lockfile only — high/critical → 0. The remaining uuid/botbuilder advisories are MODERATE (don't gate) and only fixable via a breaking botbuilder downgrade, so left untouched. web-ui audit already clean. Build green.
… CI flake) The 'same descriptor yields identical bytes' test compared raw xlsx (zip) bytes from two in-process renders. exceljs 4.4.0 stamps ZIP entry mtimes with the wall clock (DOS 2-second granularity) and exposes no API to pin them, so two renders straddling a 2s boundary differ even though the logical workbook + pinned created/modified are identical — flaky in slow CI, passes locally where both renders land in the same window (confirmed: byte-equal immediately, differ after a 2.5s gap). Freeze the clock across both renders via node:test mock.timers so the assertion verifies renderer determinism, not wall-clock timing. (Freezing Date globally inside the renderer would be unsafe under concurrent async on the server.) Unrelated to the KG changes in this PR.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #317 (durable knowledge tier). #317 made curated knowledge surface reliably; this PR makes the durable-knowledge lifecycle self-curating and automatic on any deployment — no per-instance backfill or cleanup scripts. Two outcomes:
dynamics_describe) for structure it already knows.MemorableKnowledgeis merged automatically (durable nodes protected) instead of only being flagged for an operator.Background — the reported problem
On the live test instance (3rd session on the same topic) the operator saw:
dynamics_describe/dynamics_querysweep) even though the schema was already in the knowledge graph;MemorableKnowledge;The ask was explicitly general, not data-specific: "on a live deployment this must happen automatically."
Root causes
toRecalledInsightandrenderRecallBlocksexcerptExtractor+significanceScorerhad no notion of schema/data-model/convention knowledgeinsight→ never auto-promoted to durable → coverage never grewMergeCandidaterecords) for an operator; it never resolved thembuildOrchestratorforwardedautoPromote/autoPromoteThresholdbut droppedautoPromoteDurableMinSignificance/Kindsfallback) promoted only to the fuzzy tier — durable auto-promotion fired only for the staticchatAgent@1(E is the same class as the earlier "missing modelRouting config in registry-managed per-Agent orchestrators" fix.)
What this PR changes
1. Durable full-render + trust prompt (A)
RecalledInsightgains adurablemarker; durable insights render at 2000 chars (fuzzy stays 300) in bothtoRecalledInsightandrenderRecallBlocks.2. Ingest hygiene for all auto-harvest (B)
passesIngestHygiene(narration regex) now gates every auto-harvested MK inpromoteTurnIfSignificant, not just durable promotion. Agent narration is dropped entirely (reason: 'hygiene-skip'). Length is not a gate for fuzzy (short facts like "Preis 1200€" are kept); the durable tier keeps its own length floor.3. Schema-aware classification → auto-durable (C)
excerptExtractorandsignificanceScorernow recognise data-model / schema / domain conventions (entities, field names, entity-sets, joins) askind=reference+ high significance (≥0.85). Such turns auto-promote to durable (T3), so once the agent learns e.g. the booking/participant schema it becomes durable and is not re-discovered. Time-bound data snapshots ("29 courses next week") stay mid-significance insights.4. Automatic merge of high-confidence duplicates (D)
autoMergeThresholditself instead of only flagging. Aggressive default 0.90 (kg_auto_merge_threshold) so paraphrased re-statements merge too; the flag floor is lowered to the threshold so sub-0.95 near-dups actually surface. Safety: amanuallyAuthored(durable) node is never deleted; when exactly one side is durable it wins; both-durable is left for an operator; otherwise the older node wins. Config-gated (kg_auto_merge_enabled, default on) + startup telemetry.5. Durable config reaches dynamic/registry agents (E)
buildOrchestratornow threadsautoPromoteDurableMinSignificance/autoPromoteDurableKindsinto the per-agentOrchestrator(the value was already provided inOrchestratorDeps; it was being dropped). So every agent — including registry-built ones — self-curates the durable tier, not justchatAgent@1.Config flags (all default to the new behaviour, all reversible)
kg_auto_merge_enabled/KG_AUTO_MERGE_ENABLEDkg_auto_merge_threshold0.90kg_durable_autopromote/KG_DURABLE_AUTOPROMOTETests
12 files changed, +381/-20. New/extended:test/autoMerge.test.ts— durable-protected, durable-as-source, both-durable-skip, older-wins, flag-only-without-threshold.test/promoteTurnIfSignificant.test.ts— narration →hygiene-skip; short factual turn still stored.test/durableRecall.test.ts— durable insight renders full-length (not truncated) +durableflag set.All merge / promotion / durable / recall / buildOrchestrator suites green; full workspace build + ESLint clean (node 22.22.3).
Verified live (omadia-test, on the dynamic
fallbackagent)dynamics_describecalls roughly halved (11 → 6) for a course query once the durable schema rendered in full and the trust-prompt applied;Out of scope / follow-ups
content_hashexact-dedup remains unset (low value — the semantic auto-merge supersedes it for the observed paraphrase duplicates).