Skip to main content
Status: design, not built. No basis field, no basis on any tool input or output, and no change to the supersede rule. Tracked as issue #409. Sebastian agreed on 2026-11-03 that the idea is worth pursuing (recorded in the issue). Every option below stays open until the owner decides; see Decisions for the owner. The job: a reader can tell a stated fact from an inferred belief, and how sure the writer was, before acting on it. The owner wrote “weight is 98 kg”; an extractor only inferred a status from prose. Today get_facts returns both as the same kind of row. This page uses three bases:
  • stated: the owner said it, in their own words or by confirming it.
  • observed: the writer read it from a source it can name, such as a file, a tool result or a device export.
  • inferred: the writer concluded it from other evidence, such as prose, a pattern or an estimate.

What exists today

Each claim cites the code it rests on, as of the commit that added this page.
  • Confidence on writes. confidence in [0, 1] is optional on remember facts (packages/schema/src/write.ts:157) and on import (packages/schema/src/import.ts:185), stored in a nullable real column (packages/db/src/schema/memory.ts:246). It has bounds and no meaning: the generated remember reference lists only its type and range (docs/reference/tools.mdx:88-92). No engine code acts on it: it is stored, copied, exported and returned, and the CLI prints it (apps/cli/src/format.ts:47).
  • Confidence on reads. get_facts returns confidence as nullable (packages/schema/src/mcp.ts:509). It does not return the row’s memoryId (apps/server/src/mcp/tools.ts:332-344, REST apps/server/src/rest/router.ts:595-604), so a reader cannot get from a fact to the memory that asserted it.
  • Who wrote a fact. A fact row has no writer column (packages/db/src/schema/memory.ts:235-264). The writer is recorded only as the actor_kind of the source memory’s audit event: create on a native write (packages/db/src/memory-write.ts:207-214), import with actor importer on an import (packages/core/src/import/index.ts:7-9). Every MCP write records user_mcp (apps/server/src/mcp/tools.ts:252, session continuity), whether the owner dictated the value or the agent inferred it.
  • Extracted-fact proposals. fact_proposals stores the extractor’s confidence (packages/db/src/schema/memory.ts:328), the review list returns it (packages/schema/src/review-fact-proposals.ts:46), and accepting copies it onto the new fact (packages/db/src/fact-proposals-apply.ts:103-112). The accept passes no actor and writes no memory event (packages/core/src/admin/proposals.ts:288), and the fact’s memory_id is the source memory. The fact row alone cannot tell an accepted extraction from a direct write; only the applied proposal row records it.
  • Review gate. get_facts selects from facts, never from fact_proposals (packages/db/src/facts-read.ts:307), and accepting is the only path from a proposal into it (MCP design item 9). Nothing extracted becomes queryable before review.
  • No extractor ships. insertFactProposals (packages/db/src/fact-proposals.ts:112) has no caller outside tests. The extractor is issue #228, still open. Issue #409’s “proposals carry the extractor’s confidence” describes the table and the accept path, not running behaviour: no shipped code creates a proposal.
  • Supersede (#369). Since engine v1.11.0 a fact key is single-valued by default: a write closes every open value of its key that started at or before it, at its own start (packages/db/src/facts-write.ts:241-252, memory model). A row marked cardinality: "many" or written with its own validTo closes nothing (facts-write.ts:108). The rule reads neither basis nor confidence. Accepting a proposal writes a single-valued fact (fact-proposals-apply.ts:103), unless the proposal has its own validTo. A proposal without validFrom starts at accept time (fact-proposals-apply.ts:110), so it closes the current value. Import opts out (packages/db/src/memory-import.ts:247). Current reads hide facts whose parent memory was superseded or archived (packages/db/src/facts-read.ts:153-159). This is engine behaviour. The managed service does not run it yet; its production rollout waits on a separate evaluation.
  • search and briefing. Neither reads facts; both return memories (packages/db/src/search.ts and packages/db/src/briefing-read.ts name the table only in comments). The briefing’s stale candidates include fact-type memories (packages/core/src/read/briefing.ts:108-113), which is a memory type, not a fact row.
  • Calibration precedent. The forgetting harness computes expected calibration error (ECE) over 10 equal-width bins (eval/src/forgetting-eval.mjs:65, :179). The forgetting pass reports it beside precision and does not make it a bar.

1. Basis: a stored field or derived from the writer

A. Derive it. No new column. The data can tell a direct write from an accepted proposal, by joining the applied proposal on memory and triple, and usually a native write from an import, by the source memory’s audit event. Only usually: an imported fact writes no event of its own and may ride any memory the tenant owns (packages/db/src/memory-import.ts:241-249). It cannot tell stated from inferred on the main path, because the owner and the agent write through the same user_mcp actor. For an accepted extraction it credits whoever wrote the source memory, not whoever confirmed the value, because the accept records no actor. Derivation answers “which path”, not “what basis”. B. Store a writer-declared basis per fact. A nullable basis column on facts, whose enum (stated, observed, inferred) lives in packages/schema with a generated CHECK (hard rule 2). It is optional on remember facts and on import, and carried through export. The remember input is a surface change under hard rule 8: it needs a regenerated snapshot (pnpm run docs:generate plus eval/fixtures/transport-surfaces.json). It is written once, at insert, and never updated (hard rule 1). The accept path sets the basis itself, to stated: accepting is the owner confirming the value, which is how stated is defined, and it is what keeps a later inferred write from replacing that value without review under 2B. The applied proposal row still records that the value began as an inference, and the copied confidence stays the extractor’s score, as today. The alternative keeps inferred and adds a reviewed marker that the supersede rule treats as grounded, which says more at the cost of a second column. Either way, acceptance confirms only as much as the review gate does: review_proposals accepts as user_mcp too (apps/server/src/mcp/tools-admin.ts:304). A missing basis is stored as null and read as unknown, and nothing is backfilled. Correcting a basis is a new assertion: an owner who confirms an inferred value writes it again as stated, which closes the inferred row at its own start under the single-valued rule. C. B plus a server-set write path (direct, proposal, import). A column replaces the join in A, at the cost of a second field to carry through export and import. Basis belongs on the fact, not the memory: one memory can state one value and infer another, and the memory types enum is out of scope. A declared basis is the writer’s claim, like confidence; the server can vouch for it only on the accept path. Recommendation: B, with an accepted proposal recorded as stated. Add C’s path only if a reader needs it; the applied proposal row already records it.

2. Supersede: may an inferred value close a stated one?

Group the bases in two tiers: grounded (stated, observed) and inferred. A. Basis-blind, as today. The newest start wins whatever its basis. Simple and already in the engine, but an agent’s estimate silently replaces the owner’s number in current reads, and the stated value survives only in range and asOf history. B. Inferred closes only inferred; a conflict goes to review. A grounded write closes any open value, as today. An inferred direct write closes open inferred values, but when the key has an open grounded value it is staged as a fact proposal instead, for the owner to accept or reject. Accepting any fact proposal, extracted or staged, closes under the normal rule and records stated (question 1), because the owner said yes. This reuses the review gate, keeps one open value per key and only appends. Its costs:
  • It catches only writers that volunteer inferred. Basis is the writer’s own claim, and the owner and the agent both write as user_mcp (apps/server/src/mcp/tools.ts:252). An agent that declares stated still closes the owner’s value, and so does one that omits basis if unknown counts as grounded (see below). B stops estimates that honest writers label as such; it is not a security boundary.
  • It covers supersede-eligible rows only. An inferred value marked cardinality: "many" or written with its own validTo closes nothing (facts-write.ts:108), so it is never staged, and a many value stays open beside the grounded one.
  • It covers get_facts only. The memory’s prose still asserts the staged value, so search and briefing keep returning that memory while its fact waits for review.
  • Surface and layering. remember must report a fact it staged instead of wrote: an additive output field, so a regenerated snapshot under hard rule 8. fact_proposals gains a second source besides the extractor. The staging decision lives in packages/core or packages/db, never in a transport (hard rule 5).
C. Inferred never closes grounded and is kept as closed history. The value would be invisible to current reads from the moment it is written. That loses it quietly; rejected. D. Single-valued per key and basis. Two current values for one key is the state #369 removed; rejected. Two limits hold under every option. A write closes only values that started at or before it (facts-write.ts:249-252), so a backdated stated value lands as history below a newer open inferred one; the owner restores it by writing it without validFrom. And confidence plays no part in closing (question 4). An unknown basis needs a precedence without a label. It covers every fact written before basis ships, and every later write that omits it. This choice sets how much of B’s gate survives (decisions 3 and 4):
  • U1. Unknown counts as grounded everywhere. Protects existing values, but a new write that omits basis is grounded too, so omitting it bypasses the gate.
  • U2. Unknown counts as grounded only for rows written before basis ships. The cutoff compares the fact’s created_at, which every path sets on the server (packages/db/src/schema/memory.ts:250; facts-write.ts:140-154 never writes it), not recorded_at, which import replays from the export (packages/schema/src/import.ts:188). A new write that omits basis counts as inferred, both when it would close a value and when a later write would close it. So does a row imported after the cutoff without a basis, even when its recorded_at is older: restoring an old export drops the protection its rows had. Omission no longer bypasses the gate, and a writer can still declare stated. The cost lands on today’s clients. None sends basis yet, so every update they make to a key whose open value predates the cutoff is staged for review instead of written, including an owner’s own dictated value sent through an older plugin. U2 changes behaviour for every current caller until it sends basis.
  • U3. Unknown counts as inferred everywhere. Keeps today’s behaviour for existing values and protects none of them.
Recommendation: B with U2, in its own PR after the basis field ships and the clients that write facts send it, and after the #369 production evaluation finishes, so that evaluation measures one rule, not two.

3. Reads: filter, rank or label

  • get_facts: label. Return basis (nullable) on every fact, on MCP and REST alike. An output field is an additive widening that a caller parsing a subset ignores, as recordedAt was (packages/schema/src/mcp.ts:496). It is still a surface change under hard rule 8 and needs a regenerated snapshot (pnpm run docs:generate plus eval/fixtures/transport-surfaces.json).
  • No ranking. For a single-valued key a current read returns one value, so there is nothing to rank. A cardinality: "many" key keeps every value open (packages/db/test/integration/facts-write.int.test.ts:263-272), and limit applies after ordering (packages/db/src/facts-read.ts:291-293, :317), so the order decides which of those values a bounded result keeps. Keep the shipped order for them too: recorded_at in list mode, valid_from in range mode. Putting grounded values first would silently change which values existing callers lose at the limit. Narrowing to the subject and predicate still returns at most 200 values (MAX_FACTS_LIMIT, packages/schema/src/mcp.ts:380, :482), and get_facts has no cursor, so a larger many key cannot be read in full through this surface. Full reads would need pagination, a separate surface change under hard rule 8, left as an open question rather than designed here.
  • No filter yet. A basis input is a tool change under hard rule 8, with no evidence yet that an agent needs it. Revisit when a transcript or an eval shows one.
  • Optional companion: return memoryId, which the row and the export already carry (packages/db/src/data-export.ts:387), so a reader can open the memory behind a fact.
  • search and briefing: no change. They return memories, never fact rows. Carrying basis there needs a join they do not make, or a memory-level field, which waits on the memory types question.
Recommendation: label only, on get_facts.

4. Calibration before agents rely on confidence

Two sources write confidence, and they differ:
  • Writer-declared (remember, import): any client model or a person, with no defined meaning and no ground truth the engine can check. The engine cannot measure its calibration.
  • Model-produced (the #228 extractor’s score): one pinned model on a known task, which a public fixture can test, as the forgetting pass does.
Recommendation:
  • Give the confidence input a description: the writer’s probability that the value is correct. A field description regenerates the reference snapshot but not the tool-selection embeddings, which embed tool descriptions only (eval/src/tool-selection.mjs:292).
  • Document writer-declared confidence as self-reported and uncalibrated.
  • Before the extractor applies a threshold, report its ECE on a public synthetic fixture with the forgetting harness’s binning, report-only as there. Review decisions add a second, biased check: applied and rejected fact proposals keep their confidence, but acceptance is not correctness, and if the extractor stores only proposals above its threshold, as issue #228 plans, the low bins stay empty.
  • No engine behaviour (supersede, ranking, filtering) depends on confidence until a calibration report exists for its source.

Out of scope

  • Retro-labelling existing facts without the owner’s review. Unknown stays unknown.
  • Any change to the memory types enum before this design is accepted.
  • Validation outside packages/schema (hard rule 2), and any write that is not append-and-supersede (hard rule 1).

Decisions for the owner

  1. Basis: store a writer-declared basis per fact (recommended), derive it from the writer, or store both a basis and a write path? Does an accepted proposal record stated (recommended), or inferred plus a reviewed marker?
  2. Values: are stated, observed and inferred the right set, and do stated and observed share the grounded tier?
  3. Supersede: does an inferred direct write that meets an open grounded value become a fact proposal (recommended), or does the rule stay basis-blind? Either way, the gate binds only writers that declare inferred or, under U2, omit basis.
  4. Unknown basis: grounded everywhere (U1), grounded only for rows whose created_at predates the basis release (U2, recommended), or inferred everywhere (U3)? U2 stages every current client’s update to a pre-cutoff key for review until that client sends basis, so it also needs a decision on when the rule turns on.
  5. Sequencing: does the supersede change wait for the #369 production evaluation (recommended)?
  6. Reads: label only on get_facts (recommended)? Should get_facts also return memoryId? For many keys, keep the shipped order (recommended) or put grounded values first, and is get_facts pagination worth designing (open)?
  7. Calibration: define confidence, label writer-declared values as uncalibrated, and require an ECE report before the extractor or any rule relies on it (recommended)?