Skip to content

Commit 7ec3cdc

Browse files
authored
fix: pending_review store integrity — disk-backed queue + load-time hygiene sweep (#257)
Bug 1: Confirm/Reject/List operated purely on in-memory state, so a running serve process and a CLI process each served stale pending ids ("pending review X not found"). The pending array is now disk-backed: - List/Confirm/Reject re-read user_model.json under an advisory flock (user_model.json.lock) before lookup/mutation - applyDiff writes through immediately, keeping the on-disk array authoritative; non-pending state (style/focus) is preserved per process - read errors on List fail open to the last known snapshot Bug 2: pending entries referencing consumed atom ids accumulated forever. Load now runs a hygiene sweep: entries whose value text references atom ids that are ALL missing from the atom store are dropped (only when an atom checker is installed; transient read errors count as present — fail-open), drops are logged with ids, and identical (field, value) duplicates consolidate to the highest-confidence entry. ExtendedMemory.New wires the checker to the live atom store. Adversarial-review hardening: applyDiff write-through also runs under the flock and syncs the queue from disk first (a serve inference cycle can no longer resurrect a CLI-confirmed entry); mutate treats the full disk state as authoritative (no stale style/focus clobber); the sweep probes only the entry value and requires every referenced atom to be gone before dropping. RED-first tests in pending_store_integrity_test.go cover cross-process write-through, concurrent no-loss mutations, self-referential defect entries, sweep drop/keep/dedup, and the list-vs-disk invariant.
1 parent 86caa44 commit 7ec3cdc

6 files changed

Lines changed: 711 additions & 19 deletions

File tree

‎docs/EXTENDED_MEMORY.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ Pending reviews must be explicitly confirmed or rejected by the user. They are n
280280
- `odek memory extended confirm <pending-id>` applies the inference to the model and persists it.
281281
- `odek memory extended reject <pending-id>` removes the inference without applying it.
282282

283+
The pending queue is disk-backed: list, confirm, and reject re-read `user_model.json` under an advisory file lock before acting, so a running `odek serve` process and CLI invocations always observe the same queue — no stale ids, no cross-process "not found" errors, and concurrent mutations never overwrite each other. New pending inferences are written through to disk immediately.
284+
285+
On load, a hygiene sweep keeps the queue clean: entries whose text references an atom id that no longer exists in the atom store are dropped (the drop is logged with the entry id to the serve/CLI log), and duplicate entries with identical field and value consolidate to the highest-confidence one.
286+
283287
The agent's `memory` tool can list pending reviews (`list_pending_review`) and reject them (`reject_pending_review`), but it cannot confirm them — confirmation is deliberately reserved for the human-gated CLI to prevent a prompt-injected agent from approving its own inferences.
284288

285289
Loaded and summarized user-model values are scanned for injection patterns; any field that fails the scan is dropped so a tampered `user_model.json` cannot poison the system prompt.

‎internal/memory/extended/extended_memory.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ func New(dir string, llm LLMClient, cfg Config) *ExtendedMemory {
125125
em.recall.SetPredictor(em.predictor)
126126
em.recall.SetFollowUpSink(em.setLastFollowUps)
127127
em.recall.stats = &em.stats
128+
// Wire the hygiene sweep's atom-existence checker to the live atom store,
129+
// before Load runs it. Only a definitive not-found counts as missing —
130+
// transient read errors must not cause the sweep to drop entries.
131+
em.userModel.SetAtomChecker(func(id string) bool {
132+
_, err := store.Get(id)
133+
if err == nil {
134+
return true
135+
}
136+
return !strings.Contains(err.Error(), "not found")
137+
})
128138
_ = em.userModel.Load()
129139
em.quarantine.SetTTLDays(cfg.QuarantineTTLDays)
130140
if removed, err := em.quarantine.EvictExpired(cfg.QuarantineTTLDays); err != nil {

0 commit comments

Comments
 (0)