You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Browse filesBrowse the repository at this point in the historyBrowse files
authored
memory: confirm writes before storing them - #2 (#815)
* memory: confirm writes before storing them
dirge decides what is worth remembering on its own. The agent writes
mid-session, and after an idle session the background review and memory
curator write more. All of it lands in the system prompt of every later
session in the project — and under global scope, every project. Nobody
approves any of it, a wrong or trivial memory persists silently, and
something the human knows matters is never recorded unless the model
happened to notice it.
`memory.confirm_writes` (default off) puts a human in that loop. An `add`
is queued instead of stored; `/memory review` opens the queue in $EDITOR.
The file is the desired final state of the batch, not a diff — so reject
(delete the block), reword (edit the text) and add (type a new one) are one
operation, and recording what the model missed is a first-class action
rather than an afterthought. Accepted entries go in through the normal
add_entry path, so a memory you typed is indistinguishable from one the
model proposed.
A queued entry rides on `status = 'pending'`. Every read path already
filters `status = 'active'` — the snapshot, `view`, and the FTS join in
`search` — so a proposal is inert everywhere with no new filtering and no
migration. It also skips hot-tier compaction: a merely proposed memory must
not demote an accepted one before anyone agreed to keep it.
Only `add` is gated. `replace`/`supersede`/`remove` act on entries a human
already accepted, and `supersede` usually fires because the user just
corrected the agent.
An aborted edit (`:cq`) changes nothing, which is why `edit_text` returns
None rather than an empty document — confusing the two would reject the
whole queue. An unparseable file aborts the apply intact rather than
half-applying.
$EDITOR handling is extracted from `Input::open_in_external_editor` into
`ui::external_editor` and shared, rather than copied: the O_EXCL temp file,
the /dev/tty fd juggling and the git-style argv are all easy to get subtly
wrong twice.
Verified end to end against the built binary: writes queue as pending; the
agent's own `view` and `search` report zero entries; review rewords one
entry, rejects another and adds a third, leaving exactly that in the store
with an empty queue; an editor exiting non-zero leaves queue and store
untouched; and with the gate off writes go straight through as before.
* gate the /memory review editor path to unix; collapse a nested if
collect/render/parse/apply and PendingEntry/list_pending/clear_pending
are only reached from /memory review, which needs $EDITOR, so windows
flagged them dead. notify_if_queued and add_pending stay cross-platform:
the queue itself is written and counted everywhere. clippy wanted the
indent-continuation if collapsed.
---------
Co-authored-by: Wayne <wayne@grange.la>
Co-authored-by: Yogthos <yogthos@gmail.com>
Copy file name to clipboardExpand all lines: docs/config.md
+40Lines changed: 40 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -292,6 +292,46 @@ Reciprocal Rank Fusion. It needs an OpenAI-compatible embeddings endpoint.
292
292
|`embed_model`| string | Embedding model id. Default `text-embedding-3-small` — set it when pointing at a non-OpenAI endpoint. |
293
293
|`embed_api_key_env`| string | Name of the env var holding the API key (the key itself is never stored in config). Omit for a keyless local endpoint. |
294
294
|`verbatim_pre_recall`| boolean | Each turn, auto-search memory on the verbatim user message and inject the hits as a supplemental context note (separate from the frozen system-prompt snapshot — it never changes the cached prefix). Surfaces relevant memory the agent wouldn't think to look up. Works with BM25 or hybrid. Default `false`. |
295
+
|`confirm_writes`| boolean | Require human confirmation before any memory `add` is stored. See [Confirming memory writes](#confirming-memory-writes). Default `false`. |
296
+
297
+
### Confirming memory writes
298
+
299
+
dirge decides what is worth remembering on its own. The agent writes mid-session
300
+
whenever it judges something memorable, and after an idle session the background
301
+
review and memory curator — forked LLM runners — write more. All of it lands in
302
+
the system prompt of every later session in the project, and under global scope,
303
+
of every project.
304
+
305
+
`confirm_writes` puts a human in that loop:
306
+
307
+
```json
308
+
{ "memory": { "confirm_writes": true } }
309
+
```
310
+
311
+
An `add` is then *queued* rather than stored. A queued entry is inert — it is not
312
+
in the prompt snapshot, not in `memory view`, and not in `memory search` — and the
313
+
model is told plainly that it is not yet stored, so it does not treat the fact as
314
+
durable. Review them with:
315
+
316
+
```
317
+
/memory review
318
+
```
319
+
320
+
which opens the queue in `$EDITOR`. **The file is the desired final state**: delete
321
+
a block to reject it, edit the text to reword it, or type a new block to record
322
+
something the model never noticed. Saving stores exactly what is in the file;
323
+
quitting without saving (`:cq`) leaves the queue untouched for next time. Accepted
324
+
entries go in through the normal write path, so a memory you typed is
325
+
indistinguishable from one the agent proposed.
326
+
327
+
Only `add` is gated. `replace`, `supersede` and `remove` act on entries you already
328
+
accepted, and `supersede` usually fires because you just corrected the agent —
329
+
asking you to confirm your own correction would be noise.
330
+
331
+
A notice appears when the queue is non-empty, both after the post-session passes
332
+
and once at startup. Headless `-p` has no one to ask, so entries simply queue there
333
+
and wait for your next interactive session; nothing is auto-accepted and nothing is
334
+
lost.
295
335
296
336
Safe by default and on failure: with `hybrid_retrieval` off, or the endpoint
297
337
unset/unreachable/timed out, search silently falls back to BM25 — it never
Copy file name to clipboardExpand all lines: docs/features.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,7 +33,7 @@ headline differentiators, see the top-level [README](../README.md).
33
33
-**Model-aware steering**: the harness detects the active model family and tailors guidance to it. DeepSeek **chat** models (v3/v4) get an extra preamble fragment — a Plan-Execute-Verify working method, structural-constraint framing (name files/functions/order, not "be modular"), an explicit success/never contract, and an anti-repetition rule (accept an errored or truncated tool result and adapt rather than re-issuing the same call) — appended last so it sits closest to the action boundary, where rules resist drift in long tool-calling loops. Other models, and the DeepSeek reasoner (which ignores the system prompt), are unaffected. Baked-in and automatic; no config key. See [prompts.md](prompts.md#model-aware-steering).
34
34
-**Subagent support**: `task` tool spawns a subagent for research or general analysis subtasks. Optionally run the subagent under a named [agent profile](agents.md) (`task(agent="<name>")`) to give it its own model + system prompt, and opt a profile into **tooled subagents** (`subagent.tools: readonly` for read/grep/glob, or `readwrite` to also let the subagent edit/write/bash the repo directly), plus grant selected [MCP tools](mcp.md) via `subagent_mcp`.
35
35
-**Background shells** (Claude-Code-style): the `bash` tool accepts `background: true` to run a command **detached and unbounded** (for dev servers, watch builds, long-running jobs) — it returns immediately with a shell id. The model reads accumulated output incrementally with the **`bash_output`** tool and stops the shell with **`kill_shell`** (both take the id); an optional `timeout` auto-kills after N seconds. Shells are tracked in a dedicated registry, capped at 8 concurrent, listed by `/tasks`, and killed when the session ends. The status bar shows live counts when any are running: `agents:N` (background subagents) and `shells:N` (background shells).
36
-
- **Memory & self-improvement**: persistent per-project memory in the session DB (`.dirge/sessions/state.db`, `memories` table — facts under the `memory` target, anti-patterns under `pitfalls`), injected into the system prompt as a frozen snapshot. Memory is two-tiered: hot entries inline verbatim; when the inline budget fills, the least-salient entries demote to a breadcrumb index (one line of id + preview each) that the agent dereferences with `memory(action='expand')` or queries with `memory(action='search')` (FTS5). Removal archives (tombstones) rather than deletes — `restore` brings entries back. Legacy `MEMORY.md`/`PITFALLS.md` files are imported automatically on first load and parked as `*.imported`. A **global, cross-project memory tier** (a single store in the user data dir, default on) holds durable user preferences that follow you across every repo; it's injected into the prompt under its own header, and the `memory` tool writes/searches it with `scope: "global"` (default scope stays project). After an idle session, a unified post-session orchestrator runs (in order, fire-and-forget): a background review that extracts learnings into memory + skills, then a skills curator and a memory curator (stale-detection + lifecycle + LLM consolidation, with audit reports under each store's `.curator_reports/`). Procedural memories can carry a **falsifiable expectation** — a trigger plus the outcome expected when it fires (a command that should have run, or the run ending verified-green) — settled deterministically in the post-session pass against the session digest and the run's verification status, with no LLM in that path. Success, failure, and "the situation never arose" stay three distinct verdicts: collapsing the last two would make the expectation impossible to refute, and only a refutable one is worth recording. Memories without an expectation are untouched, and settling only moves the existing bounded effectiveness counters — it never deletes. Inspect what is stored with `/memory`, and change it with `/memory edit`, which opens the whole store in `$EDITOR` — reword a line to reword the memory, delete a block to forget it (archived, not destroyed), add a block to record something new. Each entry is anchored on its id so an edit is an in-place update: lineage, use counts, confidence and the procedural outcome counters survive being reworded.
36
+
- **Memory & self-improvement**: persistent per-project memory in the session DB (`.dirge/sessions/state.db`, `memories` table — facts under the `memory` target, anti-patterns under `pitfalls`), injected into the system prompt as a frozen snapshot. Memory is two-tiered: hot entries inline verbatim; when the inline budget fills, the least-salient entries demote to a breadcrumb index (one line of id + preview each) that the agent dereferences with `memory(action='expand')` or queries with `memory(action='search')` (FTS5). Removal archives (tombstones) rather than deletes — `restore` brings entries back. Legacy `MEMORY.md`/`PITFALLS.md` files are imported automatically on first load and parked as `*.imported`. A **global, cross-project memory tier** (a single store in the user data dir, default on) holds durable user preferences that follow you across every repo; it's injected into the prompt under its own header, and the `memory` tool writes/searches it with `scope: "global"` (default scope stays project). After an idle session, a unified post-session orchestrator runs (in order, fire-and-forget): a background review that extracts learnings into memory + skills, then a skills curator and a memory curator (stale-detection + lifecycle + LLM consolidation, with audit reports under each store's `.curator_reports/`). Procedural memories can carry a **falsifiable expectation** — a trigger plus the outcome expected when it fires (a command that should have run, or the run ending verified-green) — settled deterministically in the post-session pass against the session digest and the run's verification status, with no LLM in that path. Success, failure, and "the situation never arose" stay three distinct verdicts: collapsing the last two would make the expectation impossible to refute, and only a refutable one is worth recording. Memories without an expectation are untouched, and settling only moves the existing bounded effectiveness counters — it never deletes. Inspect what is stored with `/memory`, and change it with `/memory edit`, which opens the whole store in `$EDITOR` — reword a line to reword the memory, delete a block to forget it (archived, not destroyed), add a block to record something new. Each entry is anchored on its id so an edit is an in-place update: lineage, use counts, confidence and the procedural outcome counters survive being reworded. Set `memory.confirm_writes` to put a human in that loop: an `add` is then queued instead of stored — inert, absent from the prompt and from search — and `/memory review` opens the queue in `$EDITOR`, where a block can be rejected by deleting it, reworded in place, or added outright for something the model never noticed. See [config.md](config.md#confirming-memory-writes).
-**dirge as an MCP server** (`dirge mcp`): run dirge itself as an MCP server so another agent (e.g. Claude Code) can **delegate implementation tasks to dirge and review them** — the caller plans/architects, dirge implements. Keeps a persistent per-project session (`delegate` extends it, `new_session` rotates); each delegation returns a summary + the files it changed for review. Built into the binary (`mcp-server` feature, default on). See [mcp-server.md](mcp-server.md).
39
39
-**File-state rewind**: `Esc-Esc` opens the rewind picker, which rolls back the working tree, not just the conversation. Every write/edit/edit_lines/apply_patch (incl. delete/rename) snapshots the touched file's pre-mutation content keyed by the triggering user prompt; rewinding to a prompt restores all files to their pre-prompt state in lockstep with the conversation truncation, so a long autonomous run is safe to unwind. A file created in the rewound region is deleted on restore; a deleted file is recreated. Content is deduplicated through a content-addressed pool. In-memory and process-scoped (works within a live session, not across a restart).
0 commit comments