Skip to content

fix: memory store and odek memory CLI contract (2026-10-13 audit) - #319

Merged
jkyberneees merged 13 commits into
mainfrom
fix/memory-contract-2026-10-13
Oct 13, 2026
Merged

jkyberneees merged 13 commits into
mainfrom
fix/memory-contract-2026-10-13

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Summary

Audit of the memory store and the odek memory CLI contract (2026-10-13). Nine findings fixed, one commit each, RED test first.

# Finding Fix
1 Sub-agents stored their task text as user_approved atoms. odek subagent ran per-turn atom extraction on its incoming message — the task written by the parent model — so model-authored text (337 of 4,106 atoms in the audited store) got the principal's trust label and was recalled into every later session. agent.Config.Subagent disables user-message extraction (recall stays on); odek subagent sets it.
2 Episode extraction continued the chat instead of summarising it. The summariser got the raw history (one case: 730 KB, 93% tool output, ending on an assistant message). 4 of 27 stored episodes were conversation turns, 6 were cut mid-word at 1 KiB. Bounded transcript (32 KiB, head+tail, tool results replaced by a size note), data-only prompt with NONE, conversational/declined replies not stored, sentence-boundary trim under 2 KiB.
3 episodes_vectors.gob / episodes_embedder.gob were 0644 beside 0600 files. chmod 0600 after save.
4 TTL-expired goals/questions only hidden at recall; they accumulated (1,540 in the audited store) and fed stale-goal nudges; 6 orphan chunk files; atoms had no session id (Telegram never set it). Prune (dry-run), Triage (regexp → quarantine, the recovery path for finding 1), PersistedIndex; session/project stamped on atoms; Telegram wired.
5 promote on an unknown id leaked the filesystem path; promote on a trusted episode reported success. Promote judges the index first, same answers as discard.
6 CLI contract: help was "unknown subcommand", discard undocumented, stray args ignored, extended stats always said 0 (dirty), extended compact did nothing (background work died with the process), no way to see all episodes or edit facts. Docs described a facts/+episodes/ layout that does not exist and called episodes "facts". Rewritten memory_cmd.go: help, strict args, list --all, bulk promote/discard, facts [user|env] / add / replace / remove, honest stats, waiting compact, prune, triage. CLI.md, MEMORY.md, EXTENDED_MEMORY.md, CHEATSHEET.md, AGENTS.md corrected.
7–9 Review fixes (three Sonnet reviewers): word-boundary bug in the decline regex, you/your false positives, orphan-sweep race against a concurrent Add, triage rollback, facts field bound + guard sidecar, -- end-of-flags. Folded in.

Not changed (by design / follow-up): delegation sessions stay pending review (delegate_tasks taints the episode — security rule); the two "user impersonation" quarantine entries were produced by a rule that no longer exists (the texts scan clean today) and can be promoted; Telegram chats never produce episodes (chat-scoped sessions never end) — separate design question.

Coverage

package before after
internal/memory 85.1% 85.3%
internal/memory/extended 90.9% 90.9%
cmd/odek 77.1% 77.2%
internal/agent — 83.6% (one wiring test added)

go test -race on internal/memory, internal/agent, the new extended and CLI tests: clean. golangci-lint on touched packages: 0 issues.

E2E with the head binary (scratch copy of ~/.odek)

  • odek subagent --goal "I plan to deliver a read-only adversarial review…" → live atoms unchanged (2563 → 2563); three parent turns afterwards added 3 atoms.
  • 3-turn session → 414-byte third-person episode ("The session consisted of unverified Q&A about odek's memory tool … Nothing was implemented or changed.").
  • Every odek memory / odek memory extended subcommand exercised: strict args, list --all, bulk promote/discard, facts, stats (persisted index), compact (waits, 0.2 s for 2.5k atoms), prune (1,540 expired atoms, 6 orphan chunks), triage --quarantine (3 sub-agent briefs).

Test plan

  • RED → GREEN per finding (TestRED_* in each package)
  • go test -count=1 on internal/memory, internal/memory/extended, internal/agent, cmd/odek
  • -race on changed packages
  • Sonnet adversarial review per area; findings fixed
  • Opus full-branch adversarial review
  • CI green → squash-merge → tag

🤖 Generated with Claude Code

jkyberneees and others added 8 commits October 13, 2026 03:27
…r task message

A sub-agent's incoming user message is the task its parent model wrote,
possibly carrying text the parent ingested from tools. Per-turn atom
extraction stores every atom as user_approved and recalls it into every
later session, so delegation laundered model-authored text into the
principal's trust class. agent.Config.Subagent now switches extraction
off (recall stays on); odek subagent sets it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tinuing it

The session-end summariser received the raw history — one observed
case was 730 KB, 93% tool output, ending on the assistant's own
message — and the model answered by continuing the chat. The transcript
is now bounded (32 KiB, head and tail kept, tool results replaced by a
size note), the prompt treats it as data and allows NONE, replies that
read as a conversation turn or a record of nothing are not stored, and
stored summaries are cut at a sentence end under a 2 KiB cap instead of
mid-word at 1 KiB.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
go-vector writes vectors and the embedder vocabulary with os.Create, so
episodes_vectors.gob and episodes_embedder.gob were 0644 beside the
0600 summaries they reconstruct. The extended index already chmods its
files; the episode index now does the same.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… atoms record their session

TTL-expired goals and questions were only hidden at recall and kept
accumulating (787 goals, 433 questions observed); Prune deletes them,
sweeps orphan chunk files and expired quarantine entries, with a dry
run. Triage finds live atoms by pattern and moves them to quarantine
for review — the recovery path for sub-agent briefs stored as
user_approved. PersistedIndex reports the on-disk vector index without
rebuilding it. Per-turn atoms now carry the shared session id and
project (the Telegram surface never set them).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Promoting an unknown id leaked a filesystem error with the full path,
and promoting a trusted episode reported success. Promote now reads the
index first: not found, already approved, or not pending review (the
same answers discard gives); auto-approved episodes stay promotable so
a human approval outlives the auto_approve_episodes setting.
FactStats exposes a target's entries, usage and cap for operator
surfaces.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…bulk promote/discard, facts, honest extended stats/compact, prune and triage

- help/--help/-h print usage; discard is documented; stray arguments
  are errors instead of being ignored
- list --all shows every episode with its trust state; promote and
  discard take several ids and report each
- facts [user|env] / add / replace / remove edit the fact files from
  the CLI with the memory tool's filters
- extended stats reports the persisted vector index instead of the
  never-built in-process one and drops the process-local recall
  counters; compact waits for the rebuild and reports it
- extended prune [--dry-run] and triage [--quarantine] <regexp>
- docs: CLI.md rows corrected (episodes, not facts), MEMORY.md layout
  matches the flat on-disk store, EXTENDED_MEMORY.md command table

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lock, CLI bounds and guard

- looksLikeEpisode: word-bounded decline check (Native…/Navigated… pass),
  you/your/I'll judged on the first sentence outside quotes, later
  first-person sentences and curly apostrophes still caught; combined
  extraction prompt carries the same NONE / no-address rules; the
  no-sentence fallback stays within the cap
- Prune: orphan sweep re-reads atoms.json under the store locks and skips
  chunks younger than 10 minutes (another process may be mid-Add); one
  failing atom no longer aborts the pass; quarantine counts use one clock
- Triage: a failed store removal undoes the quarantine copy
- facts: 2048-byte field bound and the configured guard sidecar, as the
  memory tool; help and typos no longer open the extended store;
  triage accepts -- before a pattern starting with --; list --help

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek ebb3d8b Commit Preview URL

Branch Preview URL
Oct 13 2026, 02:34 AM

jkyberneees and others added 5 commits October 13, 2026 04:06
…s not stored

Seen e2e: the model hedged with a prose record of absence instead of
NONE; the trivial-reply check now matches inflected forms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ded config, inert maintenance open, atoms.json flock, summary-shape precision

- odek memory extended opens the store with memory.ExtendedConfigFor
  (the agent's resolution: TTLs, embedding backend); prune therefore
  applies the configured ephemeral_ttl_days and compact rebuilds in the
  same embedding space as recall
- extended.NewForMaintenance: no quarantine eviction on open or on
  listing, so stats/quarantine/prune --dry-run change nothing; Prune
  evicts explicitly
- AtomStore takes a cross-process flock on atoms.json for every
  read-modify-write (prune/triage beside a running daemon)
- looksLikeEpisode: declines must be the whole reply, records of absence
  need 'there is nothing …' or a short reply, openers need a
  conversational continuation, second-person checks are explicit forms;
  quoted and code spans never count
- list output sanitises session ids; odek subagent's Subagent flag is
  pinned by a test; continue summarises the reloaded session (the turn
  that just ran was left out)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…be created

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s rejected, records of absence judged on the last sentence

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jkyberneees
jkyberneees merged commit 0915711 into main Oct 13, 2026
10 checks passed
@jkyberneees
jkyberneees deleted the fix/memory-contract-2026-10-13 branch October 13, 2026 02:44
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.

1 participant