Skip to content

Release 1.9.0: time that answers the past, owner data, integrity, a dark router, and the memory judges - #244

Merged
joslat merged 41 commits into
mainfrom
phase40/core-first
Oct 5, 2026
Merged

joslat merged 41 commits into
mainfrom
phase40/core-first

Conversation

@joslat

@joslat joslat commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Release 1.9.0

Version 1.9.0 with a dated changelog (2026-10-05). AgentMemory.Gate and AgentMemory.Validation are new on this line and
held from NuGet, like the Extensibility pair; the judge seams in Abstractions are [Experimental("AMGATE001")] until the
storage work settles.

What this branch does

Core-first work on the memory layer, then the memory routers it measured: one clock, time that answers questions about
the past, owner data you can erase and export, an integrity check, a dark router, and an opt-in judge at the fan-in and on
the write path. Everything new that changes behaviour is off unless registered or enabled; the defaults a host runs today
are unchanged except where a fix says so.

Time

  • One clock: every time in the memory path comes from IClock (40.45).
  • A question about the past keeps the value that was true then; a new value for the same period corrects rather than
    changes; a correction reaches a value a change made history (40.65, B-13).
  • An owner's memory as it was known at an instant (40.46); a date in the question, on a real store (40.54); a question
    that names a time keeps the conversation's messages (40.55 c).

Owner data and integrity

  • Erase, export and import an owner; export brings no merged-away entity back and joins no two owners (40.47).
  • Ingestion outcomes say what each write did (40.48); an integrity check of the store against the rules every write
    keeps, read-only (40.50); why a memory was not recalled, for facts, entities and preferences (40.49).
  • A fact that says nothing is no longer stored (40.51); a merge moves facts' links to the survivor (40.60); a corrected
    name reaches the facts that name it (38.2).

Recall fixes

  • Relationship recall is ordered by relevance to what was recalled, not by confidence alone; a relationship whose
    endpoint was met in an earlier message is resolved against the owner's stored entities instead of dropped (40.91).
  • Similarity alone no longer merges two people with different given names ("Erik Halvorsen" / "Sven Halvorsen") (40.93).

Routing

  • IMemoryRouter, dark: which memory kinds a recall reads, per question; restrict-only and deterministic (40.56), with
    the module contract for extensibility (40.62).
  • IMemoryGate and IMemoryUpdateJudge seams (41.06), used only when registered:
    • the gate sees the turn and everything a wide recall found and keeps what helps the reply. On two unseen test sets
      it served 96.1% / 96.5% of turns with every needed memory in the prompt, against 67.6% / 76.8% for today's recall,
      with less than half the memory tokens;
    • the update judge, on PersistenceStage, closes the stored fact or preference a new one replaces (threshold 0.8,
      top 3 similar live memories). Without a judge the write path is exactly what it was.
  • AgentMemory.Gate 0.1.0 implements both over System One endpoints. It is held from release and marked
    [Experimental("AMGATE001")]. Modes Today / Judge / Everything; today's recall is the automatic fallback on a
    timeout, an error or an unanswered memory type, with a warning naming the reason.

Validation and tooling

  • Validation packs (schema, data, expected storage, questions; no model), a shared-knowledge pack (40.57); the pack
    runner no longer disposes a caller's embedding generator (every pack after the first ran without embeddings) and
    takes a logging hook.
  • Routing scorer and the router arena (tools/AgentMemory.RouterArena, not packaged): frozen-set scoring, the jar's
    cross-validated selection with one read per test set, a storage jar on the real write path, and showcases through
    the library. Every run logs the stack's warnings and the components' decisions.
  • TypedMemEval sees bitemporal behaviour; StubEmbeddingGenerator returns the same vector for the same text in every
    process (40.40).
  • Docs: the time model and the router; the service-interface catalog (52).
  • An integrity rule for live memories stored without an embedding (a failed embedding call stores the memory without one and
    only logs it).

Checks

  • Unit 6,299 · Extensibility 42 · LongMemEval 1,060 · Semantic Kernel 55 · Performance 3: all pass.
  • Integration: 653 passed, 0 failed (29 skipped: the hosted-backend tests that need a live service).
  • Release build of AgentMemory.slnx, every sample included: 0 warnings, 0 errors.
  • Sample-run gate at f5476be: AgentWithMemory and MinimalAgent end to end on a throw-away Neo4j with GLM-5.3-Flash and bge-m3
    (Bitdeer): no failure or warning lines, memory recalled across turns.
  • Packages: the 17 listed projects packed at 1.9.0, inventory exact (4 held); every consumer project installed them and ran on
    net8.0, net9.0 and net10.0 (12 of 12).

🤖 Generated with Claude Code

https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH

joslat and others added 30 commits October 2, 2026 12:35
…ocess (PLAN 40.40)

It seeded from string.GetHashCode(), which .NET randomises per process, so its "deterministic"
vectors changed from run to run; on 4-dimension test vectors "Copenhagen" sometimes resolved to
"Lyon", and ClosedFactTakesItsEdgeIntegrationTests failed in the v1.8.0 release run while the PR run
of the same commit passed. The seed now comes from the text's SHA-256.

Unit 6,085 + 42 and integration 601 (29 skipped) pass with the new vectors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
….65)

ExtractionOptions.BitemporalChanges, off by default. Without it every closing is a
retraction: after "I moved to Madrid" the old city is no longer believed at all, so an
as-of question read with today's belief cannot return it, and an undated new value
counts as true at every past moment. With it, a closing says why (invalidated_reason):

- a change ends the old value's valid time when the new one began (its stated start,
  else when it was said), records when that end was learned, and keeps it believed;
- an undated new value starts when it was said (valid_from_inferred, read only as of);
- a marked correction withdraws belief and leaves valid time alone;
- a change's mirrored edge ends at the same moment.

Live recall is unchanged either way: a change still stamps invalidated_at. Only the
as-of fact queries read the reason, through one pair of predicates
(TemporalQueries.FactBelievedAsOf / FactValidAsOf) so they cannot drift; firing keeps
reading a changed value as withdrawn. Stores written without the option read exactly
as before.

New: FactClosureReason, an IFactRepository.SupersedeAsync overload taking it (the
default implementation keeps the old semantics), MemoryHistoryRecord.ClosedAs, shown
as closedAs by the MCP memory_lineage tool.

Tests: 5 unit, 7 integration on a real Neo4j (two pin today's behaviour with the
option off). Unit suites green; the related integration tests green (58).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…5, G1)

The Neo4j repositories stamped closings, live recall's "now", the recency reranker's
"now" and updated_at from DateTimeOffset.UtcNow, while the persistence stage and the
assembler used IClock. A host, a test or a replay that registered its own clock set
only half of the times one recall compares. The fact, entity, preference, relationship
and message repositories now take IClock (optional; the system clock without DI), the
reranker takes the caller's now, and the streaming extractor stamps its chunk message
from the clock.

OneClockGuardTests scans AgentMemory.Core and AgentMemory.Neo4j and fails on a new
wall-clock read outside five named exceptions (the clock itself, the per-day session
name, the migration ledger, the bootstrap backfill, the malformed-data default).
A_replayed_clock_stamps_every_time_a_closing_writes proves it end to end on a real
store: created_at, invalidated_at, a change's end, when it was recorded and an undated
value's inferred start all come from the replayed clock.

Unit suites green; the full integration suite green (609, net10.0).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… the clocks each question asks on (PLAN 40.66)

The verb recalled every timestamped question at its own date on the valid clock and the
machine's now on the transaction clock, with every fact stamped at ingestion time: right
for a corpus ingested moments ago, and blind to both clocks. "As of 19 January, which city
did the record show?" could not be asked of the store at all.

Three switches, off by default so every sealed measurement keeps its path:
- --bitemporal-clocks: a ReplayClock the profile registers as IClock (G1 makes every stamp
  read it), moved to each session's date before it is stored and extracted and to the
  question's date before recall; each question is recalled at the time it names (the
  library's own TemporalQueryParser, after a leading "As of <date>,") and, for a belief
  question, at the corpus's asof_instant, read from the corpus as shipped because
  AgentEval's entry model drops the typedmemeval block.
- --bitemporal-changes: ExtractionOptions.BitemporalChanges (40.65).
- --preset: the LongMemEval verb's product presets.

The dry run now prints one question of each clock with the instants it will be recalled
at, so a run is proven to see the phenomenon before anything is spent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Found live in a demo application on 2026-10-02: "Rosa | is named | Rosa" was stored and recalled as
the person's first memory, taking a recall slot and saying nothing. The naming fact itself
is already protected from being renamed under the user's name, so the tautology arrives
from the model's own subject. The fix sits where a fact's stored subject and object are
decided: when they are the same name (case and outer spaces ignored), the fact is skipped
with an IngestionItemOutcome (Validation, Skipped, MEMORY_FACT_TAUTOLOGY) and a debug line,
and the facts beside it are written as before.

Unit suite green (6,093); the extraction integration tests green (29).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ging it (PLAN 40.65)

With BitemporalChanges, write-time supersession closed every replaced value as a change.
A new value that starts no later than the one it replaces ("Calderwick as of February",
then "actually Ardenholm as of February") is a correction of that period: the old value
was never right for it, so belief is withdrawn and its valid time is left alone. Found
reading the TypedMemEval Bitemporal corpus, whose records are corrected exactly this way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
--judge-max-output-tokens, default unchanged (AgentEval's calibrated 1500). On the Bitdeer
lineage the judge is GLM-5.3, a reasoning model; at 1500 it ran out twice on one question
and returned no verdict, which is spend with no measurement. The lineage records 4096 for
the LongMemEval judge; runs on it now pass the same. AgentEval stamps the value per run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
MergeEntities moved MENTIONS, SAME_AS and RELATED_TO and left (:Fact)-[:ABOUT]->(source) behind,
on an entity every live read skips. They now move, under the same owner rule as the edges.
Move rather than chase merged_into: one rule with the other links, nothing for readers to learn.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
RenameOnCorrectedName restated the facts said about the old name and left the ones naming it as
their object ("Priya | walks | Rex" after "the dog is Max, not Rex"). They now follow: closed and
restated, owner-scoped; a naming fact of another subject is left alone. Restatements close as
corrections (BitemporalChanges keeps their valid time) and, with LinkFactsToEntities, get the
ABOUT links a new fact would. IFactRepository.GetByObjectAsync, default none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… second pass)

The matrix's "a date in the question becomes as of" row had unit tests only. On Neo4j: with
ResolveTemporalQueries, "Where did the user live 6 years ago?" after a move recalls the old city
and a question naming no time stays live; off (the library default), the date is not read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ith no model (PLAN 40.57)

A pack (agentmemory-pack/1) carries its ontology, conversations with what extraction yields for
each message, the storage they must leave and the questions they must answer, with the memory
kinds that hold each answer (the routing set's gold, 40.61). The runner builds the real stack
per pack (its preset and switches, scripted extraction, stub embeddings, a replayed clock) and
checks storage, recall membership and, on every question, isolation. Each run writes under a
fresh prefix and deletes nothing.

Six core packs ship (changes and corrections on both clocks plus a date in the question, names,
preferences, episodic, the graph, working memory); four planted defects fail the checks that
name them. agentmemory evaluate --pack core|<file>|<dir>. AgentMemory.Validation is HELD.

Found by the names pack: the rename's merge cleared the survivor's embedding and nothing
re-embedded it, so a renamed person dropped out of entity recall. Re-embedded from its name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…model (PLAN 40.61)

routing-set/1: each question or turn with the kinds that hold its answer, as groups (any kind
in a group serves it, every group is needed) plus kinds that may hold it too, and a dev or
held-out split. The scorer reports served answers, group recall, reads and wasted reads per
item, silence on turns that need nothing, and per-kind selection. agentmemory routing-score
--set <file> [--split] [--policy everything]: today's recall, the baseline a router must beat.

The frozen set itself stays out of the repository: its turns come from recorded demo conversations.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…(PLAN 40.56)

Recall read every kind on every turn; the Agent Framework hook (#88) could skip kinds but no
policy chose them per question, and no other host had it. MemoryOptions.Routing.Enabled (off):
a statement reads nothing; a question reads facts plus the graph, preferences or the session's
relevant messages when its words call for them. Restrict-only on the effective caps (a host's
caps are kept, never raised); rules are data (Routing.Rules adds a host's or a module's kind);
the choice and the rule behind each kind are recorded (MemoryRoutePlan.Routed,
memory.route.kinds). RecallRequest.Question carries the turn being answered; the provider sets it.

Dev split of the frozen routing set (7595f87c): 51/51 answers served, 1.59 reads per turn
(read everything: 5.00), silent on 39/39 turns that need nothing. Every core validation pack
passes with routing on; a statement recalls nothing on a real store. These rules are frozen
at this commit for the held-out split. routing-score --policy rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…PLAN 40.55 c)

The as-of path ran no relevant-message search, so a date in the question dropped the session's
episodes. It now runs the live search and keeps what was said by the transaction instant and not
withdrawn by then, GetRecentMessagesAsOf's rule; MaxRelevantMessages leaves the as-of divergence
list, and TotalItemsRetrieved counts the section.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
IngestionItemOutcome.Effect: Created, AlreadyStored (the upsert matched a stored fact, preference
or relationship), MergedWithinExtraction (a duplicate folded into another item); Closed lists the
facts a change or a correction closed, collected where the closing is written (an AsyncLocal set
for the persist in flight), so no extra query. Entities report Unreported until resolution says
whether it matched. Additive: every field a caller read before reads the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…AN 40.50, G6)

IMemoryIntegrityService and agentmemory integrity [--owner]: no edge joins two owners' memories;
a relationship edge belongs to its owned endpoints' owner; a fact closed as a change or a
correction has its successor, and a live fact has none; validity windows end after they begin;
every fact has a source (a warning: an API write may not). One read query per rule, a count and
up to five ids each. Each rule fails on a planted defect and passes on a clean store; every
validation pack now checks it on what its ingestion wrote, and all six hold.

Reworded from the plan: 'no live fact past its validity' is legitimate in a bitemporal store (a
fact that ended is still known); what is inconsistent is an inverted window or a live fact with
a successor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The history read already carried validity, why a row closed, its successors and sources; it
lacked an instant. MemoryHistoryQuery.AsOf (agentmemory history --as-of): rows written after it
are left out, and live-only keeps a row invalidated after it (it was live then). A dossier or a
module reads an owner's memory through it with no Cypher of its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
IMemoryOwnerDataService and agentmemory owner export|import|erase. Erase deletes every node
stamped with the owner and their conversations with every message, in batches, reporting what
went by label; the CLI asks for --confirm. Export: entities, facts and preferences live and
closed, relationships, supersessions and ABOUT links (agentmemory-export/1, no embeddings).
Import: fresh ids under an owner, embedded again, closings and links kept; JSON metadata turned
back into plain values. On a real store an erased owner leaves no node and others are
untouched; an export carried through JSON and imported under a new owner recalls the same, live
and about the past. The repositories' node mappers became internal so the export reuses them.

Not carried: valid_until_recorded_at (documented).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
IMemoryRecallExplainer.WhyNotFactAsync: for a recall request and a fact, the gate that kept it
out, in the order recall applies them: another owner's, the router, closed (with what replaced
it), decayed, invalidated, outside its valid time, below the similarity floor (its score on the
store's (1 + cos) / 2 scale, and the floor), outranked, cut by the budget; or recalled. Off the
hot path: it reads the fact and runs the recall again; normal recall is unchanged. On a real
store a planted fact below the floor is reported with its score, a closed one with its successor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…review)

The export took every entity of the owner's, merged-away and forgotten ones included, so an
import revived them live beside their survivor; and a fact still linked to such an entity (data
from before F8) was linked, in the same store, to the original owner's node, the edge the
integrity check forbids. Export now takes live entities, and only the links and relationships
whose ends are exported or shared; an import places a shared end by its id.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e branch review)

With BitemporalChanges a change keeps the old value believed as history, and a correction
closed only live facts, so "I never lived in Bilbao" left a past question answering Bilbao. A
new correction now also closes change-closed facts of the value it names, and the store turns
that closing into a correction from now on (reason and instant). A value said again with the old
one named (the correction is not new) leaves the past alone. Off: unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ty 0.10, PLAN 40.62)

IMemoryRouter.Route(question, additionalRules): a module's own rules run in the same engine and
on the same question as core recall, so the two never disagree about the core kinds; added rules
compile once per list (a module set holds one) and without backtracking, as spec 0.8 §6.2 asks
of patterns over untrusted text. IMemoryRouter.Check names a rule that needs backtracking or does
not compile. Neo4jMemoryContextProvider.QuestionOf (protected static): the one derivation of the
question, now used for RecallRequest.Question, for a subclass that routes module sections.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… (PLAN 40.57)

A pack session can be shared (ExtractionRequest.ShareWithEveryone, no user): what it says is
stored with no owner. core.shared: a book taught by one owner is recalled by the other, while the
teacher's own memories stay hers; isolation holds on every question. Passes plain and routed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/time-model.md: the two clocks, how a change and a correction are closed with and without
BitemporalChanges, asking about the past, what reads valid time, what is not modelled, the NAMS
backend. docs/routing.md: what the router chooses, configuring rules, how it is scored, modules,
the NAMS backend. CHANGELOG: stores written before keep their closings.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
IMemoryRecallExplainer.WhyNotAsync(request, kind, id) replaces the facts-only call (unreleased):
facts, entities and preferences go through one path of gates. New gates: merged (an entity merged
into another; the history now carries MergedIntoId) and not-embedded (no embedding, so similarity
search cannot find it; found on a real store: a direct merge leaves the survivor unembedded until
the backfill runs). Real-store test: merged names its survivor, not-embedded until the backfill
then recalled, another owner's, a preference recalled then invalidated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
routing-score gets --policy today: an Agent Framework host's default,
TrivialTurnRecallPolicy, which narrows a greeting or a thanks to the recent
messages and reads every kind on any other turn. On the frozen set it scores
the same as reading everything (0/39 silent on dev): the turns that need no
memory are statements, not greetings, so the earlier baseline was fair.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…(PLAN 40.67-40.82)

tools/AgentMemory.RouterArena: a recorder (a throwaway Neo4j, local embeddings, every
turn recalled through every door), askers for a model lane and a System One decision
model, a referee that scores routing (served, unneeded let in, doors, time), one
contestant per folder, rounds, finals on unseen turns, a timer, and the gate pairs a
local gate is trained on. Its private data is read from --data or ROUTER_ARENA_DATA.

Validation packs carry reasoning traces and run a decay pass; the CLI's routing
record verbs moved into the arena; a few comments no longer name a private host.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
…d surname (PLAN 40.91, 40.93)

40.91. Relationship recall returned the same few edges whatever was asked: they were ordered by confidence and age only.
They are now ordered by the rank of their best end among the recalled entities, then among the names the recalled facts
give; confidence and recency only break ties. And a relationship whose endpoint was met in an earlier message is no
longer dropped at extraction: with an owner, persistence resolves the endpoint against the owner's stored entities, and
skips it there, with the same outcome as before, when none is found.

40.93. Similarity alone no longer makes two people with different given names one person. "Erik Halvorsen" and
"Sven Halvorsen" score 0.88 on name embeddings; resolving one onto the other erased the brother and pointed his
relationships at the owner. A different order, an initial, a short form, an honorific or a one-letter typo still
resolve; exact names and aliases are never affected.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
…and takes a logging hook

ValidationPackRunner registered a caller's IEmbeddingGenerator through a factory, so each pack's container owned it and
disposed it with the pack's store. Every pack after the first then wrote its memories without embeddings: the failure was
logged, but the runner registered no logging, so nothing showed. The caller's generator is now wrapped in a borrowing
adapter whose Dispose does nothing; the caller keeps ownership. An integration test runs two packs on one runner and fails
without the fix.

The runner also takes an optional logging configuration, so a host sees what the stack decided and what failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
joslat and others added 8 commits October 4, 2026 23:06
…ge on the write path closes what a change replaces (PLAN 41.06)

Two seams in Abstractions, both used only when registered:

- IMemoryGate: given the turn, the conversation and every memory a wide recall found, the probability that each one
  helps the reply. Measured on two unseen test sets, a gate in front of today's recall served 96.1% and 96.5% of turns
  (every needed memory in the prompt) against today's 67.6% and 76.8%, with less than half the memory tokens.
- IMemoryUpdateJudge: given what was said and pairs of (new memory, most similar stored memory), the probability that
  the new one replaces the stored one. PersistenceStage takes it as an optional last constructor parameter; for each
  fact and preference an extraction created it asks about the owner's top UpdateJudgeCandidates (3) live memories of
  the same kind, and closes those at or above UpdateJudgeThreshold (0.8): a fact as a change (valid time ends, belief
  stays), a preference by supersession. Without a judge the write path is exactly what it was. A judge that fails or
  leaves a pair unanswered closes nothing and says so in the log.

AgentMemory.Gate 0.1.0 (held from release, [Experimental("AMGATE001")]) implements both over System One endpoints:

- AddAgentMemoryGate decorates the registered IMemoryContextAssembler. Modes: Today (the inner assembler, unchanged),
  Judge (a wide recall, then the blended judges keep what scores at or above the threshold), Everything (the wide recall
  as found). Today's recall is the fallback: on a timeout (3 s), an error, or a memory type no judge answered, the
  request goes to the inner assembler unchanged. The decision is in the context's metadata (gate.*) and in the log;
  every fallback is a warning with its reason.
- Several judges blend by weight, renormalised over those that answered; an optional bank of labelled examples gives
  each question its three nearest by embedding.
- The update judge asks one keyed question per pair with written criteria.

ExtractionStageResult gains SourceText (what the extraction read), so the update judge is shown what was said. The
service-interface catalog in docs/architecture.md and its guard go from 50 to 52.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
… through the library (PLAN 40.83-40.98, 41.07, 41.08)

- jar cv|freeze|read|replay: the selection in .NET. Every family is estimated by five-fold cross-validation on the
  training turns, chosen on all of them and frozen with a fingerprint; a test set is read once and a second read is
  refused; replay rescores a set already read and compares with the recorded read.
- store-jar: writes each labelled turn through the real write path on a throwaway store loaded with the world, and
  records what was written, so storage routers are decided offline on recorded writes.
- showcase: a few questions through the library end to end, three ways (everything a wide recall finds, today's
  recall, the gate), with tokens, recall and end-to-end time, replies and the context each saw.
- store-showcase: a conversation of changes written twice (today's write path, then with the update judge), read
  back (created, closed, still live), then asked about through the gate.
- The facts the recalled facts replaced, read beside the as-of readings (40.91); a second world with its own person
  (40.92); doors timed alone and the store warmed on every turn (40.83).
- Every run logs to the console: warnings from the whole stack, the gate's and the write path's decisions. A run that
  loads the same world every turn embeds each text once (a cache in front of the embedder).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
A substitute matched a non-nullable MemoryScope with Arg.Any<MemoryScope?>(); a test blocked on a task instead of
awaiting it. The Release build of the solution is back to zero warnings.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
… 41.06)

MemoryGateMode.Today becomes Floor: recall cut by the similarity floor (RecallOptions.MinSimilarityScore) and a cap per
type, as without the gate, and the fallback when no judge answers. The context's metadata and the fallback warnings say
"floor". Everything's summary now says what was measured: ten to seventeen times the memory tokens of Judge, replies
graded about five points more accurate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
… 41.10)

A write whose embedding call fails stores the memory without one, and only logs it: recall by meaning never finds it again.
That is how a test runner that disposed its embedding generator went unnoticed for a whole storage run. The integrity check
now counts live facts, preferences and entities with no embedding (recall.memory-has-embedding, a warning: the store is
repairable with IMemoryMaintenance.GenerateEmbeddingsBatchAsync, and no write fails). Closed memories are not counted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
…41.06, 41.10)

Measured again on the fixed instrument: a judge asked whether the stored memory "stops being true now: changed, cancelled or
corrected", with past results and earlier events named as history that stays true, closed more replaced memories without
closing history. With a write gate before it, it met every condition written before the test reads on both unseen sets:
no labelled write lost, fewer unasked writes, 13 of 21 replaced memories closed, one wrong closure within the budget.
UpdateJudgeThreshold's default becomes 0.65, the bar cross-validation chose for that question. The changelog records the
gate, the update judge, the integrity warning and the two recall fixes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
…ettles

IMemoryGate, IMemoryUpdateJudge, their request and decision records, and ExtractionOptions.UpdateJudgeThreshold and
UpdateJudgeCandidates carry [Experimental("AMGATE001")], like the AgentMemory.Gate package that implements them. They ship
in the release without being locked by SemVer: the write side is still being measured, and a writer per memory type may
change what the update seam looks like. Core uses the update seam internally and suppresses the diagnostic.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
…ark router, and the memory judges

Version 1.9.0; the changelog's unreleased section is dated 2026-10-05. Two packages added on this line stay held from
NuGet (AgentMemory.Gate, AgentMemory.Validation), like the Extensibility pair.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
Copilot AI balanced review requested due to automatic review settings October 5, 2026 07:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@joslat
joslat force-pushed the phase40/core-first branch from 8766b16 to f5476be Compare October 5, 2026 08:04
joslat and others added 3 commits October 5, 2026 10:17
…lease review)

Without an owner, the judge's candidates came from a vector search with no scope, which reads every owner's memories, so
an ownerless write could have closed another person's fact or preference. Candidates are now only the writer's own
memories, or, without an owner, ownerless ones: a single-tenant store, where nothing has an owner, keeps the judge. A test
for an ownerless write and for another owner's write fails without the fix.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
…very recall, and reads its configuration section (PLAN 41.19)

Every live recall through the gate now carries a MemoryGateTrace in its metadata, whatever the mode: each memory
considered, the judges' probability and whether it went in, and why recall fell back to the floor. A memory.gate span
carries the counts (never a memory's text). Judge mode with no judge configured is the floor, said once, without paying
for a wide search. AddAgentMemoryGate(IConfiguration) binds the AgentMemory:RetrievalRouter section, and an undefined
mode fails at startup. The preferences the update judge closes are on the write's outcome, as facts are.

AgentMemory.Gate is listed for the release: it rides the core version, opt-in, with every public type experimental
(AMGATE001), and the AgentFramework consumer check composes it from the package. Docs: a section in routing.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
…tegration test, found in CI)

Its vectors were one-hot at string.GetHashCode() over the fixture's four dimensions: the hash is randomised per process,
so on some runs a quarter of unrelated texts were identical and the pack's correction read as a restatement ("Ana works
at Northwind" stayed live). It now wraps StubEmbeddingGenerator, whose vectors are seeded from the text's SHA-256 (the
40.40 lesson), and only watches for disposal. Three local runs of the validation integration tests: 21/21 each.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015c3cQexHvNzUZYcexSMaYH
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants