Added
- Standing-order directives are now created only by an
explicit user action -- a message that begins withstanding directive:,
or the newiai capture --directivecommand -- instead of being
auto-detected from phrasing like "from now on" or "always". This removes a
class of phantom directives that could be captured from non-user text
(session summaries, tool output) and makes directive creation predictable
and intentional. A packaged/iai-directivecommand wraps the explicit
channel. - A kill-switch environment variable
(IAI_MCP_DIRECTIVES_OFF=1) fully disables the standing-orders directive
tier: none is injected at session start, and none is injected into the
per-turn context block. Off by default -- current behavior is unchanged
unless the variable is explicitly set. iai-mcp directive-sweepis a new operator
maintenance command that retires phantom standing-order directives --
live directive records with no explicit-declaration provenance. Default
mode is--dry-run;--applysnapshots the store directory first and
clears only the flag column, never touching the record's stored text.
Idempotent: explicitly-declared directives always survive, and a second
--applyretires nothing further.
Security
-
iai-mcp transcript-sweep runnow refuses to run
against this machine's own memory store unless launched by the scheduled
background sweep or passed--allow-live-homeby hand. A bare manual
invocation used to stage every discovered Claude conversation into the
real store with no confirmation step. -
The bundled MCP wrapper no longer ships a vulnerable
copy offast-uri, a transitive URI-parsing dependency. The prior version
did not validate a port value before treating a URI as trusted, letting a
crafted authority component be misparsed and potentially redirect a
lookup to an attacker-controlled host (GHSA-qw65-cvwx-89v3). Bumped to
fast-uri3.1.7, which the advisory fixes.
Fixed
-
One unreadable or corrupted transcript no longer
silently stalls the background sweep for every other Claude conversation
in the same pass. The sweep now isolates a per-file failure, logs it, and
keeps going; its summary output gains afiles_failedcount. -
iai-mcp cowork uninstallnow warns, by name, when
it finds an agent config's settings.json still referencing the retired
reachability check's hook script underhooks.SessionStart. This one
in-place mutation cannot be safely reversed automatically; the warning
names the file and asks for the array entry to be removed by hand,
instead of the command silently reporting a clean exit. -
Sweeping a transcript while its final line is still
being written no longer permanently drops that turn. The high-water mark
used to resume a later pass now only advances past lines that were read
whole; a torn final line is retried, complete, on the next pass instead
of being counted and silently skipped forever. -
A daemon-down direct write (
iai capture) that sets
an explicit directive now stamps thedirectiveflag and its provenance
in the same insert, instead of a follow-up update after the record already
committed. A failure between the two steps used to leave the record
permanently stampeddirective_source=explicit-commandwith the flag
itself stillFalse, an orphan state nothing reconciled. -
iai-mcp directive-sweepno longer aborts the whole
batch when one directive record's provenance is undecryptable or
malformed. The sweep now skips that record, counts it in a newfailed
field, and still retires every other phantom directive; the CLI reports
the failure and exits non-zero when any record could not be read.
directives_foundnow counts only successfully-read directive records
(previously always the full matching set).
Changed
-
The
memory_captureMCP tool no longer accepts a
directiveparameter. An assistant call could previously pass it to mint
a standing-order directive on its own initiative; that channel is closed.
iai capture --directive, the user-run command, still creates a
directive the same way it always has, now via a direct store write
instead of the daemon. It can report the store as busy during the
daemon's own nightly consolidation window, same asiai teach/iai uploadalready do. -
iai-mcp cowork installno longer stages a Cowork
plugin. It installs a small background process instead -- a macOS
launchd agent or a Linux systemd user timer -- that periodically reads
new Claude conversations already on disk and hands them to the memory
system. The process opens no store connection and holds no store lock.
On a platform with no supported scheduler, install declines cleanly and
points at the memory system's own overnight consolidation as a fallback. -
iai-mcp cowork statusnow prints a one-line note
that its exit status reflects only the agent-session tier, and points at
capture-hooks statusfor the code-session surface. -
iai-mcp cowork statusno longer has an UNSUPPORTED
tier or a confirmed-build gate for the agent-session surface -- it now
reads the background sweep's own receipt log, so a session captured
locally reports ACTIVE regardless of the installed Claude Desktop build.
STAGED now means the background sweep is enabled but has not captured a
session yet, replacing the previous staged-plugin-files meaning. -
New command:
iai-mcp transcript-sweep runreads
the Claude conversations already on disk and hands their new content to
the memory system. It opens no store connection and holds no store lock
while it runs. -
The nightly consolidation cycle now includes a
backstop pass that reads the Claude conversations already on disk, the
same way the background sweep does. A session missed by the background
sweep -- the machine was asleep, or the sweep process was not running
at the time -- is still picked up on the next consolidation cycle
instead of waiting indefinitely. This pass only runs when the
background sweep is installed, and running both over the same session
never double-captures it. -
iai-mcp cowork uninstallnow removes the
background sweep, its schedule, and any leftovers an older version left
behind: a previously-staged plugin marketplace, its Cowork registration,
and a spent reachability check's own artifacts. It reports every case it
finds -- including a clean machine that never ran anything -- and never
stops partway through. -
Local Cowork sessions are now captured
automatically: the background service reads the conversation transcripts
the app already saves to disk and folds them into memory, whether the
machine was awake or asleep when the session ran. Mechanism reported by
@mbusch-regis, public issue #159. -
The turn-capture and session-capture hook scripts
now resolve the transcript scan root from the Claude configuration
directory named in the hook's own environment, falling back to the
shared home when that variable is unset, equal to the shared home, or
rejected by validation (not an absolute path, not an existing directory,
a parent-directory traversal segment, or a character outside letters,
digits, space, and._/-). A rejected value is logged and never blocks
the hook, and no other file location moves. A hook running in the
ordinary shared-home configuration is unaffected. -
Every line the four packaged hook scripts append
to their daily logs now carries which channel invoked the hook -- a
fixed "plugin" or "settings" literal, never the plugin-root path itself
-- on both success and early-exit paths. Existing fields keep their
name, order, and meaning; only the new field is added, so any tooling
that already parses these lines keeps working unchanged. -
The production
W_COSINEauto-tuner clamp band is
narrowed from[0.85, 1.15]to[0.90, 1.10]. The wider band's edge no
longer covers the current live corpus: a defensive worst-case test that
forces the clamp to its edge started dropping more candidates than its
margin allows after new records and edges raised near-cutoff candidate
density. The live persisted operating point (W_COSINE = 1.0) sits well
inside the new band and is unaffected; only the defensive edge case
re-calibrates. -
An unknown or unparseable
created_aton a graph-node
payload is now represented asNonein the recall pipeline instead of
being silently substituted with the current wall-clock time. The recall
backfill step can now detect the missing value and recover the real
stored timestamp from the database, which it previously could not do
once a fabricated present-tense value had masked the gap. Age scoring
still treats an unknown timestamp with no age penalty (the same net
effect the fabricated value produced) and no longer crashes on it either.
Ranking for records that already carry a real timestamp is unchanged. -
Newly minted cluster-summary records no longer carry
a "Cluster summary (N records, lang=X): " boilerplate prefix in their
stored text -- the language and cluster fan-in count are already stored
losslessly elsewhere on the record (thelanguagefield and
consolidated_fromedges), so the prefix only spent render and embedding
budget. The session-start rich-club renderer now also caps a cluster-summary
line at 30 characters once its stored text no longer starts with the old
boilerplate prefix; a cluster-summary line minted before this change (still
carrying the historical prefix) keeps the prior 60-character cap so it is
never truncated into boilerplate mid-migration, and every other memory line
is unaffected either way. -
The live recall path now scores candidates through
the Rust hybrid scorer by default instead of the pure-Python per-candidate
loop: per-call-state terms (profile gain, tier/salience boost, temporal
match, historical-verbatim rewrite) and the served reason string now
apply to only the bounded winner set instead of every candidate. Set
IAI_MCP_RECALL_RUST_SCORER_OFF=1to force the previous Python scorer.
Ranking output (scores, hit ordering, reasons) is unchanged; recall using
a nonzero structural-similarity weight (a knob off by default) also
continues to use the Python scorer automatically. Existing accuracy,
rank-fusion, and anti-hit test suites pass unchanged. Candidate-retrieval
decrypt work (the ANN rank-view fetch upstream of scoring) is unchanged
by this entry -- it still runs at candidate-pool scale on both scorer
paths and is not addressed here. -
The live recall path's rank-feature index now
persists across calls on a store-resident builder graph instead of
rebuilding from scratch on every recall, and a hop/rich-club candidate
already hydrated by a prior recall on the same store is served from that
resident copy instead of a fresh decrypt. Existing accuracy, byte-identity,
and stage-profile test suites pass unchanged. Known caveat: a resident
candidate's cached fields (surface text, tags, tier, salience level, etc.)
reflect the record's state as of the call that first hydrated it, not
necessarily the record's very latest state if it was updated in between --
the corpus write path does not yet push updates into this builder graph.
This bounded staleness window is a known, deferred hardening item, not
fixed by this change. -
Raising a record's salience level (the ambient-capture
dedup path's "this looks more important than last time" signal) now marks
the in-memory graph and its dependent freshness-tracked structures as
dirty, the same way every other field update already does. Previously the
raise wrote its column directly and left the graph's own resident copy of
that record silently out of date until an unrelated update touched the
same record. No API or return value changed; only the freshness/staleness
timing of graph-dependent reads after a salience raise. -
The daemon now disables Python's automatic garbage
collector and freezes the boot-resident object set (corpus + runtime
graph) once boot warm-up finishes, exempting it from every later
collection scan for a tail-latency reduction. Manualgc.collect()calls
elsewhere in the daemon (sleep-pipeline relief, capture drain) are
unaffected and still run on demand. SetIAI_MCP_GC_TAMING_OFF=1to
revert to unmanaged, always-on automatic collection. -
The confidence-escalation widen (fires on a
low-confidence recall cue) now fetches a smaller candidate pool by
default: its ANN query ceiling drops from 2000 to 1800 candidates and its
over-fetch padding (headroom for tombstoned/pending rows) drops from 3x to
2x. Every returned candidate is still ranked exactly, the tombstone/
pending liveness filter is unchanged, and a corpus's own tombstone
fraction gives comfortable headroom against underfill. On the deepest
measured real-corpus cues, the widen previously surfaced load-bearing
candidates at ranks up to 1615 of 2000 — inside the new 1800 ceiling —
but a sufficiently unusual cue could in principle need a candidate ranked
deeper than 1800 that the pre-bound widen would have surfaced. Set
IAI_MCP_ESCALATION_BOUND_OFF=1to force the previous 2000/3x behavior.
Distinct from the pre-existingIAI_MCP_CONF_ESCALATE_OFF, which
disables the widen entirely. -
Recall's per-call candidate-view sweep (
records_cache)
is now cached on the graph's existing content-version stamp: a second
recall over the same graph build (no intervening insert/update/delete)
reuses the cached view instead of re-walking every node. Any write that
changes the graph invalidates it on the very next recall. The served view
is always a fresh shallow copy, so one recall's confidence-escalation
widen can never leak into another recall's results. Set
IAI_MCP_GENERATIONAL_CACHE_OFF=1to force the previous unconditional
per-recall rebuild. -
Recall's ranking degree-map sweep (the bench/graph
path, not the real-store global-degree override) is now cached on the
same content-version stamp asrecords_cache: a second recall over the
same graph build reuses the cached degree map instead of re-running the
degree sweep. Any edge add or node removal invalidates it on the very
next recall. Shares theIAI_MCP_GENERATIONAL_CACHE_OFF=1kill-switch
with the records_cache tracer. -
The profile-modulation edge boost written after each
recall (edge_type=profile_modulates) now defers off the synchronous
answer path through the same background write queue already used for
reinforcement and coactivation writes, when that queue is live (daemon
mode). The edge lands with the identical pairs and weights, just after a
short delay instead of before the response returns; a determined caller
issuing two recalls back-to-back may observe a brief eventual-consistency
window on this one edge type. Falls back to the previous synchronous write
when the queue is not running (tests, non-daemon use). Set
IAI_MCP_DEFER_PROFILE_BOOST_OFF=1to force the old synchronous behavior.
Thepask_teachback_passtelemetry event is now also buffered the same
way as the siblingrecall_dispatchedevent — a caller reading
events_query/memory_temporal_recallimmediately after a recall may see
it land one flush cycle later. -
A correction-seeking recall cue ("вспомни правильный
X", "напомни как мы делаем X") now has its trigger phrase stripped and its
intent classification stays on the default rank-fusion path instead of
falling through tohistorical_verbatim. Any such cue containing a
historical marker word (e.g. "ранее") previously classified as
historical_verbatim(stale-downweight/supersede-cap OFF for that turn);
it now classifiesintent=None(stale-downweight/supersede-cap stay ON).
Cues without a correction trigger are unaffected. -
Ambient captures now receive an automatically
classifiedepistemic_status(fact, estimate, hypothesis, or unknown) at
capture time when the caller does not supply one, instead of always
defaulting to unknown. The classifier is validated for precision on a
labelled sample before shipping on the ambient path, so a wrong fact tag
stays rarer than a safe unknown verdict. -
A newly created memory store now uses the in-tree
native storage engine instead of the legacy SQLite format. An existing
store keeps opening in whatever format wrote it -- nothing changes for a
store that already exists. Setting the storage-driver environment variable
to the legacy name still creates the legacy format for a fresh store.
Added
- A live user turn that reads as a correction of a
recently-surfaced memory (e.g. "no, actually ...", "на самом деле ...")
now queues a confirmation candidate, surfaced through the existing
curiosity_pending/get_pending_questionsreader — no new tool, no new
event kind. This path only ever queues; it never writes a contradiction
itself. A pattern match with nothing surfaced to correct within the last
10 minutes queues nothing (recency-bounded), and at most 2 unresolved
questions are pending per session at once (mirrors the existing curiosity
miner's per-session cap), so a chatty phrase cannot crowd out genuine
curiosity questions. memory_captureandmemory_contradictaccept an
optionalepistemic_statusargument (fact/estimate/hypothesis/
opinion/unknown), letting a session mark a captured or corrected
fact's confidence at the moment it is written. Omitting the argument keeps
today's behavior exactly — every existing caller defaults tounknown.memory_recallhits and anti-hits now carry the
epistemic_statustheir source record was captured with, so a host can
render a tag such as "estimate, unconfirmed" without re-parsing prose.memory_captureaccepts an optionalsalience_level
argument (unflagged/notable/critical) letting a session mark a
captured record's importance at the moment it is written. A flagged
record ranks higher at recall (a bounded, per-level multiplier); omitting
the argument keeps today's behavior exactly — every existing caller
defaults tounflaggedwith zero ranking change. A near-duplicate
capture fold raises an existing record's stored level when the incoming
value is higher and never lowers it. The mark never setsnever_mergeor
any other merge/drop lock — it only ever changes recall order.- A new operator-only
salienceverb on the brain-view
relay (mirroring the existingpinverb) lets an operator set or clear a
record'ssalience_leveldirectly, independent of capture. It is fully
reversible per-call and writes only thesalience_levelcolumn — never
pinnedornever_merge— giving a mis-flagged record a correction path
the model itself does not have. - Reconsolidation now writes the record
valence
column for the first time. Previously the column was read at recall time
as a ranking multiplier, but nothing ever wrote it. - A new retrieval-feedback loop lets a
memory_reinforcecall feed nightly profile tuning: the resulting signal
is consumed during the nightly knob-tuning step and nudges the recall
ranking's cosine-similarity weight (W_COSINE) within a narrow,
production-clamped band. - A new standing-orders (directive) tier captures
instructions phrased for all future sessions (for example "from now on" or
"always/never") at capture time and injects them unconditionally at
session start and on every prompt, in their own marked block. Directives
share a hard joint token budget with the live-state block below, can carry
an expiry, and are never silently dropped -- retirement always requires an
explicit correction. - Session continuity now re-injects a verbatim
live-session-state record on every prompt in its own marked block,
restores a registry of running background agents into a fresh context, and
distinguishes an explicit context clear from an incidental compaction. - A new
claim_checktool, the fifteenth on the
wrapper, is read-only and returns counter-evidence for a claim the
assistant just made, merging supporting and contradicting matches with a
freshness verdict in one call. - Captured events now record which model produced
them: an optional per-event model label is normalized and stored in the
captured record's provenance. - A new procedural-memory tier (internally
tier="procedural") stores chunk records minted from patterns that repeat
across sessions. Chunks are indexed and reachable by recall's retrieval
machinery but are never served as visible text, so the session-start token
budget is unaffected. - A new nightly sleep step (
PROC_MINE) mines these
repeating patterns, taking the sleep pipeline to 19 steps; the step is
resumable from the write-ahead log like every other step. - A priming cache is written nightly from the mined
patterns and read at session wake. During recall, a bounded seed-widening
nudge uses the cache to suggest candidates that typically follow a
recognized pattern, clamped so a primed candidate can never outrank the
top organically-ranked result. - A new ambient co-firing trigger fires whenever a
recall hit is actually reflected in the assistant's own generated reply,
emitting aretrieval_cofiredevent that feeds the pattern-mining
pipeline above -- distinct from the ranker's own scoring output, so it
cannot self-confirm. - A second, read-only pattern miner parses the
tool-call trailer already recorded on stored assistant turns and mines
repeated tool-call sequences (recorded undertool_sequence). Both this
miner and the pattern-mining step above write into the same new
proc_transitionstable, a schema addition. - The daemon's recall path now caches cue embeddings
so a repeated cue skips re-running the embedding model; results stay
byte-identical, and the store-versus-embedder identity guard still re-runs
on every cache hit so a model swap can never be silently masked. A
kill-switch,IAI_MCP_CUE_EMBED_CACHE, disables and purges the cache. iai-mcp migrate-to-lilligains a--swapmode
that replaces an existing store's own directory in place with a verified
native copy, instead of only copying into a separate destination. It
previews by default -- there is no separate--dry-runflag; omitting
--applyis the preview -- performing the swap requires both a
confirmation flag and an apply flag together. The previous store is kept
under a dated backup directory until removed, and the swap refuses to
run while the daemon is serving the store.iai-mcp doctorreports the store's on-disk
format, and on a store still in the legacy sqlite format names the known
defect -- an external read-only open orphans the write-ahead-log
sidecars under a running daemon -- and prints the exact command sequence
to migrate it. The check reads only the file's header and never opens
the store to answer.iai-mcp doctoralso reports when a running
daemon is on an older build than the currently installed package -- the
case where a supervised daemon kept running through an in-place upgrade
-- and names the stop-then-start remedy.- The daemon now logs the resolved store format and
store path once at boot, so a journal shows which storage engine is
active next to the first failure, if any. iai-mcp doctor's nightly-insight-mint check now
names the underlying sleep-step failure alongside the "N consecutive
nights without an insight" symptom, when one was recorded.
Fixed
iai-mcp migrate-to-lilli --swapnow actually
pre-warms the runtime graph cache when the store uses a file-based
encryption key with no passphrase set -- previously the pre-warm step
silently failed to persist a cache in that mode while still reporting
success, so the first daemon boot after such a migration always paid a
cold rebuild regardless of the pre-warm's own result.- Daemon shutdown no longer hangs until the
supervisor's stop timeout when an idle memory client is still attached
-- the daemon now shuts down promptly and completes its own close-out
work instead of being force-killed. A client with a request already in
flight now gets its response before shutdown proceeds, and the shutdown
sequence completes cleanly even if the supervisor issues a second stop
signal while it is still finishing up. - The session-start brief served over the memory
socket was losing three of its blocks -- most-recent-work continuity,
standing orders, and session-continuity state -- because the response
builder omitted them from the wire format even though the memory system
had already produced them, so a host reading the JSON result rendered
those sections empty. They are now delivered again. MemoryStore.get_batch(and the pending-marker SQL
fallback that shares its column list) decoded every record's
epistemic_statusasunknownregardless of what was actually stored —
a batch-scoped read silently dropped the field that a single-record
get()returned correctly.memory_recallanti-hits resolved through the
graph-view cache, and every hit and anti-hit on therecall_for_benchmark
path, renderedepistemic_status: nullregardless of the stored value —
the enrichment that repaired this for primarymemory_recallhits never
ran on those two surfaces. A single shared backfill now covers both hits
and anti-hits on every recall entry point.memory_capture/memory_contradictnow coerce an
out-of-enumepistemic_statusvalue tounknowninstead of raising — a
non-enum-enforcing MCP host forwarding an invalid value no longer aborts
the capture (and can no longer abort an entire batch-captured turn),
matching the read path's existing fail-open behavior.memory_recallhits and anti-hits, on every recall
entry point and JSON response surface (including the graph-view-cached
and crisis-degraded paths), now carry thesalience_leveltheir source
record was captured with instead of silently renderingnull— the same
shared backfill that repairsepistemic_statuson those surfaces now
repairssalience_levelin the same pass.- Confidence-gated candidate widening (the ANN
index re-query triggered on a low-confidence recall) silently returned
an empty widen once a store's active record count passed 1000 — the
underlying label-resolution query raised on a single-statement limit,
and the widen's caller swallowed the error. The resolution query now
chunks so widening works at any corpus size; low-confidence recall on a
large store is no longer degraded. iai-mcp doctorno longer reports a false liveness
failure after several consecutive nights where every reconsolidation write
happened to already be at maximum valence -- a saturated night is no
longer misread as a broken background process.- An explicit clear of the current focus or next
action on a still-open task no longer leaves the retracted value visibly
persisting in the session-continuity file; the clear now takes effect
immediately.
Removed
- The within-session watermark delta renderer
(render_session_delta) and its refresh helper
(session_refresh_if_stale) are removed as unused code -- no
production caller remained. This is a different code path from the
live per-turn delta and session-refresh behavior described elsewhere
in this file, which is unaffected and keeps running on every prompt. - The hypervector bit-flip decay operation
(temporal_decay) and its edge-decay counterpart
(decay_structure_edge) are removed -- both had zero production
callers. This is a different operation from the live nightly
edge-weight decay step (DREAM_DECAY), which is unaffected and keeps
running every night.
Changed
iai-mcp capture-hooks statusrelabels the Claude
Desktop line to "Claude Desktop MCP tools:" so it no longer implies
ambient capture hooks are wired when the check only reflects MCP server
registration.- The session-start "Key memories" display label
shortens: instead of the fullW:{wing}/R:{room}/E:{entities}/T:{tags}
index, each line now shows a one-character tier plus entities only when
present (S (12d): .../E ·entity,entity (7d): ...), dropping the
record-hash and tag fields (near-zero reader value, measured ~35% of the
block). The record budget was reduced by the same proportion so the same
set of memories is shown, not more of them. Set
IAI_MCP_RICH_CLUB_COMPACT_LABEL=0to restore the previous label and
budget. - The compact label's entity list is capped at 4
(with a trailing…when longer), so records with a long provenance
tag dump no longer defeat their own compaction by hitting the shared
88-char index-truncation guard as hard as the old verbose label did. - The record budget selection is now decoupled from
the display label: which records get admitted is always decided by the
cost of the full legacy (verbose) label, and only the already-selected
record's label is rendered compact. This makes the admitted set identical
to the pre-compaction render by construction, at any corpus size,
instead of depending on a budget constant hand-tuned to today's store.
Output is unchanged from the prior recalibration: rich_club tiktoken
2145 -> 1353 (36.9%), whole-payload tiktoken 2487 -> 1695 (31.8%), 0
records dropped. - The compact label's entity list is now also capped
by character length (not just count): four long entity names could still
produce a joined string long enough to hit the 88-char index-truncation
guard and read as noise. A capped-by-length entity string carries the
same trailing…marker as a capped-by-count one.
Fixed
- A forced sleep (operator-triggered REM or a
user-sleep request) could dispatch the SLEEP transition twice within the
same lifecycle tick, collapsing WAKE straight to SLEEP before the tick
ever reached the drowsy-edge drain gate — so any turn still parked in the
live capture spool at that moment was skipped and the forced sleep
consolidated a store missing recently captured turns. The drain gate now
also fires on every entry into SLEEP (not only the natural WAKE-to-DROWSY
edge), so a forced collapse drains the spool before consolidation; the
natural DROWSY-to-SLEEP re-fire stays a no-op via the drain's existing
offset-tracked idempotency. - The deferred-capture drain telemetry hard-labeled
every drain passphase: "drowsy"(event namedeferred_drain_drowsy)
even when it fired on a forced WAKE-to-SLEEP collapse, so diagnosing that
exact case showed a misleading drowsy-edge event. The drain now labels
each pass by the transition that actually fired it:phase: "drowsy"/
eventdeferred_drain_drowsyon the natural WAKE-to-DROWSY edge, and
phase: "sleep_edge"/ eventdeferred_drain_sleep_edgeon entry into
SLEEP. Counts were always accurate; only the label changes. - The at-rest deferred-capture backlog doctor check
could itself crashiai-mcp doctoron an unreadable-but-existing
deferred-captures directory (an uncaughtOSErrorout of the backlog
enumeration), instead of degrading like the sibling capture-state hygiene
check — now caught and downgraded to a WARN. Separately, the live-spool
pending count treated a torn (no trailing newline) partial final line as
a full pending event, which could push the check to FAIL by exactly one
event the drain itself does not consider drainable; the count now uses
the same completeness rule as the drain writer, so doctor's count matches
what the drain would actually drain. (Entry recorded in a follow-up commit
to the code change that required it.) - Per-turn context injection went silent for a solo
active session: turns captured by the hooks were only promoted from the
spool into the store at boot, at the drowsy edge, after a sleep cycle, or
when ANOTHER session refreshed — so a single active session never got its
working-tier snapshot, its own turns stayed invisible to recall for hours,
and a fresh session started on a session-start brief that could be hours
stale. The daemon now promotes the spool on a WAKE cadence
(IAI_MCP_WAKE_SPOOL_SWEEP_SEC, default 30 s; signature-gated so an
unchanged spool costs one directory stat per tick; never during SLEEP)
and refreshes the session-start brief after a promoting sweep, debounced
byIAI_MCP_PRECACHE_REFRESH_MIN_SEC(default 300 s).iai-mcp daemon statusreports the last sweep underwake_spool_sweep(statusis
ok,gate_busyorerror, so a dead promotion path is visible). - The per-turn "New since last turn" delta echoed the
refreshing session's own turns (stored and still-pending) back into that
session; it now carries cross-session records only. The refresh reply now
carries an explicitcaught_upflag — true only when every promotion
step ran to completion and no delta was suppressed — and the hooks
advance their freshness sidecars ONLY on that flag (a debounced or
partial reply, or an older daemon that does not report it, keeps them),
so an own-turn store advance no longer re-fires the refresh RPC on every
prompt and a still-pending cross-session turn can no longer fall below
the watermark unseen. The live-spool drain gained the same single-flight
rail as the deferred drain, so overlapping promoters no longer re-capture
the same lines; and when the store rejects a live turn (a soft insert
failure), the drain now holds its offset and reports the pass incomplete
instead of advancing past the un-stored turn, socaught_upstays false
and the caller keeps its watermark until the turn actually lands. - Local-command host lines (
<local-command-caveat>,
<local-command-stdout>,<local-command-stderr>,<command-args>) and
any transcript entry the host marksisMetaare no longer captured as
user turns — a/modelswitch used to seed the working-tier goal with the
caveat boilerplate. Both capture parsers (inline hook and daemon) drop
them and are guarded to stay in lockstep. - A reader could briefly see fewer stored memories
than were actually committed while writes were in flight — in the worst
case a whole batch of recent memories vanished from one read and
reappeared on the next. Root cause: a read-only snapshot captured its
view of the write-ahead log in a single unguarded pass; racing a live
writer could tear that pass mid-file (or at the header during a
checkpoint), silently truncating the view to an older prefix that was
then served as current — and a later capture could even roll an
already-fresher reader backward. Snapshot capture now verifies it
consumed the log to its true committed tail (bounded retry loop), a
refresh can never regress a reader onto an older view, and a capture
that cannot be verified fails loud and falls back to the
strongly-consistent writer path instead of serving a phantom snapshot.
Verified under a concurrent-write stress harness that reproduced both
the silent undercount and a page-bounds error before the fix and runs
clean after. - Defense in depth for the same class: every cached
row-count formation point (populate, publish, sidecar persist, adopt)
now re-checks its freshness fence across the count read and recounts
instead of caching when the fence moved, so a stale count can never be
paired with a current generation even if a future path widens the
window. - The working tier reflected the current prompt one
turn late: the per-turn hook fires before the host appends the prompt to
the transcript, so only the NEXT turn's walk ever saw it. The hook now
also reads the prompt straight from its own stdin and spools it
immediately, keyed by the prompt's own id; the later transcript walk
(both the inline hook and the daemon parser) keys user turns by the same
id so it reinforces the same record instead of inserting a duplicate. A
raw/commandprompt (confirmed to arrive unexpanded on stdin) and any
prompt shorter than the existing capture floor are skipped on this path,
same as the transcript walk. - Three of the recall path's five Python-side
candidate-pool caches (the embedding pool matrix, its L2-normalized copy,
and the ranking degree map) are no longer memoized on the graph across
calls -- each is recomputed fresh on every recall instead. Ranking output
is unchanged (recall-scoring differential gate: 28/28 green, both storage
drivers); the tradeoff is a small resident-memory reduction in exchange
for repeating this work every call instead of reusing it across recalls
on the same build. Two caches (the candidate records view, the warm
lexical postings index) are kept -- both have live readers this change
does not touch. - The resident rank index's bounded delta overlay
(populated by every insert/update/delete once a live consumer exists) now
folds back into the committed structure automatically once it accumulates
enough untouched entries (default 500,IAI_MCP_RANK_OVERLAY_FOLD_THRESHOLD),
triggered only from the write path -- never during a recall. Previously
nothing ever reclaimed this overlay, so it would have grown unbounded
over long daemon uptime once a production consumer existed.