|
| 1 | +# Spec 09 — Scratchpad |
| 2 | +# Raw transcripts -> analysis -> memories. A TUI to inspect both ends. |
| 3 | + |
| 4 | +[spec] |
| 5 | +id = "scratchpad" |
| 6 | +version = "0.2.0" |
| 7 | +status = "draft" |
| 8 | +date = "2026-08-16" |
| 9 | + |
| 10 | +[thesis] |
| 11 | +flow = "raw chat transcripts -> analyzed for memories -> official memories written" |
| 12 | +why = """ |
| 13 | +The session agent is the worst judge of its own session: it overvalues effort it |
| 14 | +spent and cannot see its own corrections as corrections, having already absorbed |
| 15 | +them. A separate reviewer reading the finished transcript sees what the |
| 16 | +participant structurally cannot. Running it out of band, over completed |
| 17 | +transcripts, means it can never collide with a live session. |
| 18 | +""" |
| 19 | +visible = """ |
| 20 | +Both ends are inspectable, so the filter is tunable by looking at it rather than |
| 21 | +by guessing. Every memory traces back to the chat that produced it. |
| 22 | +""" |
| 23 | + |
| 24 | +[raw] |
| 25 | +what = "Claude Code's own session transcripts — one JSONL per chat, already on disk" |
| 26 | +where = "${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<project-slug>/" |
| 27 | +rule = "read-only view. We never write, move, or delete them. There is no capture step." |
| 28 | +boundary = "RAW sits outside the memory root; the scratchpad opens transcripts read-only and no other crate gets transcript access" |
| 29 | + |
| 30 | +[memories] |
| 31 | +what = "ordinary memories per spec 01 — the judge writes nothing special-cased" |
| 32 | +storage = "topical namespaces, unchanged" |
| 33 | +grouping = """ |
| 34 | +Per-chat grouping is a VIEW, not a layout. Memories are stored topically and |
| 35 | +rendered grouped by their `source` field. Storing them under the originating |
| 36 | +chat would key location by WHEN instead of WHAT, and spec 02's dedup probe |
| 37 | +(namespace-scoped) would never fire across sessions. |
| 38 | +""" |
| 39 | +source = "`session <uuid>`, matching the transcript filename — the join between the two tiers" |
| 40 | + |
| 41 | +[judge] |
| 42 | +input = "the whole transcript, unmodified" |
| 43 | +context = """ |
| 44 | +Requires a 1M-context model (Gemini Flash Lite and equivalents). At that size the |
| 45 | +raw transcript fits — this project's largest sessions are ~1.2 MB, roughly 300k |
| 46 | +tokens — so there is no stripping, chunking, or multi-pass logic. Send the file. |
| 47 | +""" |
| 48 | +transport = "OpenAI-compatible chat completions; model is a base-URL and name swap (Groq, Gemini, OpenAI, local Ollama)" |
| 49 | +placement = "upstream of memory_store, which keeps spec 02's invariants untouched. The judge decides whether to call it, never how it works." |
| 50 | +fail_open = "unreachable, timed out, or malformed = nothing is written and the session is unaffected. Never blocks, never delays." |
| 51 | +scheduling = "out of band, queued on Stop and PreCompact, over completed transcripts only" |
| 52 | +egress = """ |
| 53 | +EXPLICIT OPT-IN, no default endpoint. Transcripts carry source, scrollback, and |
| 54 | +client work; today they reach Anthropic by the user's choice of Claude Code, and |
| 55 | +a third party is a new decision. A local model keeps zero egress. |
| 56 | +""" |
| 57 | +confinement = "all network code lives in memory-judge; memory-core does not depend on it, so the zero-network invariant stays enforceable by the dependency graph" |
| 58 | +ledger = """ |
| 59 | +.index/reviewed.json records which sessions were analyzed and how many memories |
| 60 | +each produced. Needed because a chat that correctly yielded nothing is otherwise |
| 61 | +indistinguishable from one never read — and the zero-count entries are exactly |
| 62 | +what shows whether the rules are too strict. |
| 63 | +""" |
| 64 | + |
| 65 | +[judge.rules] |
| 66 | +layers = "a global ruleset applies everywhere; an optional per-project file drills down. No project file is the normal case." |
| 67 | +global_path = "${XDG_CONFIG_HOME:-~/.config}/fff-memory/judge.toml" |
| 68 | +project_path = "<memory root>/judge.toml" |
| 69 | +global_owns = ["what counts as memory-worthy", "sentiment rules", "the never-store floor: secrets, credentials, routine activity", "alias quality per spec 01"] |
| 70 | +project_owns = ["extra events in this codebase's vocabulary", "suppression of named global rules", "path and topic exclusions", "default namespace"] |
| 71 | +merge = "project extends global, may suppress named rules and narrow scope, may not relax the never-store floor" |
| 72 | + |
| 73 | +[judge.criteria] |
| 74 | +principle = """ |
| 75 | +Ask for observation, not evaluation. 'Was this important?' is judgment, which |
| 76 | +small models do badly — they over-produce, the exact failure this design exists |
| 77 | +to prevent. 'Did the user correct the model?' is pattern recognition, which they |
| 78 | +do well. |
| 79 | +""" |
| 80 | +events = [ |
| 81 | + "user corrected the model, especially twice on the same point -> feedback", |
| 82 | + "user stated a preference, constraint, or convention -> preference", |
| 83 | + "a decision was made and a reason given -> decision", |
| 84 | + "an approach was tried and abandoned, with the reason -> lesson", |
| 85 | + "a durable project fact not derivable from the repo -> fact", |
| 86 | +] |
| 87 | +sentiment = """ |
| 88 | +Sentiment marks WHERE to look: correction, the same correction repeating, |
| 89 | +explicit confirmation, prohibitions ('never', 'always', 'don't'), and visible |
| 90 | +frustration. Repetition and prohibition weigh most; a lone confirmation is not |
| 91 | +enough to write anything. It marks the moment — the memory's content still comes |
| 92 | +from the substance of the exchange. |
| 93 | +""" |
| 94 | + |
| 95 | +[judge.history] |
| 96 | +requirement = "the judge sees current memories, their update history, and what was archived or deleted" |
| 97 | +why = [ |
| 98 | + "dedup — update an existing handle instead of re-deriving a stored fact", |
| 99 | + "no resurrection — deleting a memory is the user saying they do not want it; a judge blind to deletions undoes that decision every session", |
| 100 | + "reinforcement — a fact re-established in a later session is evidence the memory is right, not a reason to write a second copy", |
| 101 | +] |
| 102 | +sources = "memory root, .archive/, and the access log (spec 05)" |
| 103 | + |
| 104 | +[tui] |
| 105 | +toolkit = "ratatui — terminal UI, no browser, no socket, no daemon" |
| 106 | +hierarchy = "PROJECTS -> { RAW | MEMORIES } -> per-chat" |
| 107 | +navigation = "only the two tier labels are real tabs; the chat level is a filterable, recency-ordered list" |
| 108 | +does = ["read a transcript", "read the memories it produced and jump between them", "see chats that produced nothing — the signal for tuning rules", "edit or delete a memory"] |
| 109 | +never = "RAW is not editable, and the TUI is not an approval gate — memories are live when written. Requiring a click reintroduces the manual curation this project rejected, because people forget." |
| 110 | +launch = "fff-memory scratchpad" |
| 111 | + |
| 112 | +[crates] |
| 113 | +new = [ |
| 114 | + "memory-judge (lib) — transcript reading, OpenAI-compatible client, memory emission. The only crate with network access.", |
| 115 | + "memory-tui (lib) — ratatui views over both tiers.", |
| 116 | +] |
| 117 | +wiring = "both wired into the existing binary; one binary preserved" |
| 118 | + |
| 119 | +[supersedes] |
| 120 | +note = "where this conflicts with an earlier spec, this wins and the earlier text gets rewritten" |
| 121 | +changes = [ |
| 122 | + "00: the no-server rule covers sockets and daemons; a local TUI is in scope", |
| 123 | + "01: `source` format fixed to `session <uuid>`", |
| 124 | + "04: CLI gains `scratchpad`; the command list is not closed (the five-tool MCP limit is untouched — subcommands are not tools)", |
| 125 | + "06: workspace gains crates/memory-judge and crates/memory-tui", |
| 126 | + "07: the Stop hook agent becomes an enqueue; PreCompact added as a second enqueue point", |
| 127 | +] |
| 128 | + |
| 129 | +[testing] |
| 130 | +policy = [ |
| 131 | + "memory-judge: parsing against checked-in JSONL fixtures; the LLM client behind a trait with a fake — no test needs network", |
| 132 | + "fail-open regressions: unreachable, timeout, and malformed response each leave the store byte-identical", |
| 133 | + "a test asserts memory-core's dependency graph excludes memory-judge", |
| 134 | + "a test asserts no code path opens a transcript for writing", |
| 135 | +] |
| 136 | + |
| 137 | +[open_questions] |
| 138 | +oq_1 = """ |
| 139 | +One session per analysis, or a batch? Per-session keeps a clean link from memory |
| 140 | +to chat. Batching is where the reviewer's edge is largest — the same correction |
| 141 | +recurring across five sessions is the strongest signal a rule is missing, and it |
| 142 | +is invisible one at a time. Decide by dogfooding. |
| 143 | +""" |
0 commit comments