Memory backward pass: budget, ledger, trace capture, weekly review PR #55
Labels
No labels
blocked
bug
decision
duplicate
enhancement
help wanted
invalid
landed?
question
ready-to-merge
stale
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/strategy#55
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Close the loop between what agents do in sessions and what the memory repos tell them to do: cap the always-loaded memory surface, gate new rules on evidence from more than one session, keep a distilled copy of every session transcript so it survives a VM reprovision, and run a weekly pass that reads those transcripts against the memory index and proposes at most five edits as a pull request.
Primary goals:
memory.mdexceeds a byte cap or whose index bullets carry dates; the registryhookspathmechanism installs it on every dev VM.ledger.mdwaiting room and three hygiene rules: one session's lesson goes to a topic file or the ledger, an index line needs two sessions or a stated safety reason, a removal needs evidence that following the line caused harm.allod trace distillturns each local Claude, Codex and Pi session log into a small redacted markdown trace; an hourly systemd user timer commits the traces to a private repo so they outlive the VM.memory-backpassskill reads the week's traces against the numbered index bullets, drops any finding whose quote is not in the trace, records gap sightings in the ledger, and opens one PR with at most five edits under the budget.Current state and specifics
The memory index is read on every session start and has no size limit; nothing reads the session transcripts back, and a reprovisioned dev VM discards them. Agents write to memory at the end of every session with no evidence threshold, so the index grows as a log rather than as a statement of current state. The concepts are borrowed from
backpass(github.com/kunchenguid/backpass): the always-loaded surface as a budget, quote-checked evidence, a two-session floor, harm versus non-compliance, a gap ledger, and a human gate on every write. The tool itself is not adopted: it targets a singleAGENTS.mdat a repository root, needs Node andacpx, and refuses read-only harness config, none of which fit this layout.Scope
allod/memory:hooks/pre-commitbudget check, hygiene rules,memory-backpassskillallod/inventory:hookspathforallod/memoryallod/tools:allod trace distillallod/archetypes:modules/memory-traces.nixhourly timer and its checkallod/strategy: dev plan for the weekly timer that runs the skillThe weekly timer itself follows the
forge-groomrollout: the skill is run by hand twice in report-only mode and the reports compared before any timer exists.PR bodies for the four public branches of this arc, to be used verbatim by the agent that opens each PR from a machine with public push access. Every PR carries
Refs allod/strategy#55; the final deployment change carries the closing line.allod/memory
Agents can no longer grow the always-loaded memory index without limit, and one session's lesson no longer lands straight in it. Nothing changes on a running machine: this adds a tracked pre-commit hook, a waiting-room file, five hygiene rules, and the skill a weekly job will follow to read session traces back against the index.
What changes:
.hooks/pre-commitrefuses a commit whenmemory.mdexceeds 16KB, a topic file exceeds 24KB, ormemory.mdcarries an ISO date; the fleet's global git hook already dispatches.hooks/, so no registry change is needed and it also fires insideallod changeworktrees.ledger.mdholds sightings that wait for a second session. Memory File Hygiene gains the placement rules: one session's lesson goes to a topic file or the ledger, an index line needs two sessions or a stated safety reason, a removal needs harm evidence.skills/memory-backpass/is the procedure plus two model prompts adapted from backpass (MIT, credited inUPSTREAM.md); it opens a PR and never writes a default branch.What stays identical: every existing topic file;
memory.mdis 8.6KB and already under its own cap; nothing runs on a timer yet.What proves it:
.hooks/memory-budget --worktreeexits 0 on the branch; a test commit with 20000 bytes appended tomemory.mdwas refused by the hook with the cap message; one cold cross-family review found seven issues and one fix round addressed them (silent failure of the file listing, the merged-PR filter, the prune eating fresh sightings, the missing safety exception, prose precision on the date rule).If it is wrong: revert; the hook is the only executable change and it only ever refuses a commit.
Refs allod/strategy#55
Risk
R2. A pre-commit hook that can block commits on dev VMs, plus prose. Rollback is a revert.
Validation
bash -n .hooks/memory-budget .hooks/pre-commit.hooks/memory-budget --worktree→ exit 0memory.md is 28604 bytes, cap 16384: move a section to a topic file and leave a one-line pointer, commit exit 1allod/tools
allod trace distill --out <dir>turns the local Claude Code, Codex and Pi session logs on a machine into one small redacted markdown file per session, so an hourly timer (a sibling archetypes PR) can commit them to a private repository and a weekly skill can read them back against the memory index. Nothing changes for a running machine: the command reads the harness stores and writes only under the directory it is given.What changes: a new
tracenamespace, compiled everywhere. Each trace carries a header (harness, session id, start, cwd, interactive or not, raw path), the user and assistant turns capped at 6000 characters, one line per tool call paired to its result by id, and no thinking.<system-reminder>spans are stripped. A fixed table redacts bearer tokens, OpenAI, GitHub, Slack and AWS keys, age secret keys, PEM blocks, netrc and Authorization lines, and URL-embedded credentials. A session with fewer than two user messages, or whose first user message is the weekly skill's own<!-- memory-backpass:self -->marker, is skipped. A file is rewritten only when its content changed. Output files are 0600 and the session id is sanitised before it becomes a filename.What stays identical: every other namespace; the command does no git.
What proves it:
go test ./cmd/allodwith golden fixtures per harness, the unchanged-file rule, every redaction kind, both skip rules and flag parsing;nix build .#checks.x86_64-linux.go-checkspasses; a smoke run on a dev VM's real stores distilled 34 sessions and skipped 20, and a grep for key patterns over the output found nothing. One cold cross-family review found six items; three were fixed (filename sanitisation, 0600 output, reminder stripping on every role), two were by design (the raw path in the header is what lets the skill open the original; duplicate tool ids do not occur in harness logs), one was a duplicate.If it is wrong: revert; nothing depends on the namespace yet. Traces are still private material and belong only in a private repository, which the docs say.
Refs allod/strategy#55
Risk
R2. A read-only command over local logs that writes under one directory; the privacy boundary is the redaction table plus the rule that the output goes to a private repository.
Validation
gofmt -l ./cmd/allod→ nothing;go vet ./cmd/allod→ clean;go test ./cmd/allod→ oknix build .#checks.x86_64-linux.go-checks→ passdistilled 34 sessions (0 unchanged, 20 skipped), files mode 600, leak grep empty; two Codex rollout files whose first record is notsession_metawere skipped with a named stderr lineallod/archetypes
A dev VM can now keep a redacted copy of every agent session it runs, committed hourly to a private traces repository, so the sessions outlive a reprovision. Nothing changes on any machine until a deployment sets the new
memoryTracesparameter, and theallod-toolsinput must first advance to a revision that carriesallod trace distill(its own PR in this arc).What changes:
modules/memory-traces.nixadds a systemd user service and hourly timer that pull the traces checkout with rebase, runallod trace distill --outinto it, commit astraces: <machine> <time>and push, retrying once after a rejected push;mkDevVmgains the optionalmemoryTraces = { checkout; interval ? "1h"; }parameter, null by default;checks/memory-traces.nixasserts on the generated unit and wrapper text and on the disabled case; the README documents the module and the one line for the reprovision runbook:systemctl --user start memory-traces.servicebefore wiping a VM.What stays identical: every existing machine, because the parameter defaults to null and the check proves no unit is generated then.
What proves it:
nix build .#checks.x86_64-linux.memory-tracespasses on the branch; one cold cross-family review found one defect (a fast-forward-only pull that locked the timer out after a lost push race between two VMs) and the fix round replaced it with rebase plus one retry, asserted by the check.If it is wrong: revert, or set
memoryTraces = nullin the deployment; the unit only ever commits to the traces repository.Refs allod/strategy#55
Risk
R3. A scheduled unit that pushes to a private repository from every dev VM that enables it. It writes only under its own machine subtree, sets no git identity variables, and the traces are redacted by the tools command; the checkout must be a private repository.
Validation
nix build .#checks.x86_64-linux.memory-traces→ pass, with the timer, service, wrapper and disabled-case assertions printedgit diff --checkcleanallod-toolsrevision does not yet carrytraceallod/strategy
This adds the dev plan for the last piece of the memory backward pass: a weekly timer that runs the
memory-backpassskill and opens one pull request of evidence-backed memory edits. It is a document only; nothing runs until the plan's slices land.What changes:
dev-plans/memory-backpass.md, three slices gated like the forge-groom plan: the skill is run by hand twice in report-only mode and the reports compared before the archetypes module exists, then the module, then a private enable on one VM. It scores the module R3 and says why it is not R4: the timer can only open a PR, never merge or push a default branch.What stays identical: everything; no code, no unit.
What proves it: every template section is filled; the agent gates name what only the owner can do (create the traces repository, compare the two hand-run reports, choose the VM and the schedule).
If it is wrong: edit the plan.
Refs allod/strategy#55
Risk
R0. A plan document.
Status update, and two more public branches to relay.
The timer shipped broken and this is the fix.
modules/memory-traces.nixasked the tools flake input, which holds source, forbin/allod, somemory-traces.servicefailed at that line on every hourly run on nix-dev, rust-dev and svelte-dev. The check was green throughout because it only grepped the generated unit text. Both branches below are prepared unpushed on nix-dev and need the usual relay, then a PR from allod-dev with the body given here. Afterallod/archetypesmerges,vnprc/deployneeds its archetypes pin advanced and the three VMs rebuilt.allod/archetypes
The hourly session-trace timer fails on every run on all three dev VMs that enable it, and this fixes it. The module asked a flake input holding source code for a compiled binary, so the command line it generated named a path that does not exist. Nothing else about the timer changes.
What changes for a running machine: after this lands and
vnprc/deployre-pins archetypes,memory-traces.servicerunsallod trace distillsuccessfully instead of dying at that line. The Go build expression moves out ofmodules/dev-home-shared.nixintomodules/allod-package.nix, and both the interactive PATH and the service import it, so the two copies can never diverge.What stays identical: the binary on a dev user's PATH. The factored build produces the same store path,
allod-0.1.0at05p5vq7a09wzqxp6y6q4g420kd8cs4wj, that this machine already carries from the inline expression. The unit, timer, wrapper logic and the disabled case are untouched.What proves it: the check no longer only greps the generated unit. It resolves the absolute
allodpath out of the wrapper and executes it, so a generated command line that cannot run now fails the build. Run against the pre-fix module it fails; against this one it passes and prints the store path it ran.If it is wrong: revert, or set
memoryTraces = nullfor a machine. The service only ever commits tovnprc/traces.Refs allod/strategy#55
Why the original check missed it
It asserted on text: every
grepover the unit and wrapper passed while the path inside was a source tree with nobin/. The failure surfaced an hour later on the machine, where nothing was watching.testing.mdgains the general rule in a companionallod/memorybranch.Risk
R3. A generated systemd user service on every dev VM that enables the timer. The behaviour change is that the service works; the refactor is proven byte-identical for the pre-existing consumer by the store path above.
Validation
nix build .#checks.x86_64-linux.memory-tracespasses, executing the binary the wrapper nameswrapper: names /nix/store/05p5vq7a09wzqxp6y6q4g420kd8cs4wj-allod-0.1.0/bin/allod, which runsvnprc/deploywith this archetypes substituted gives nix-dev amemory-tracesservice whose ExecStart is amemory-traceswrapper[[test on line 14; the sandbox setsTMPDIRand the missingHOMEis handled.allod/memory
Records in
testing.mdthe rule a live failure just demonstrated: a check that only reads a generated artifact witnesses its text, not its behaviour.A module in
allod/archetypesinterpolated abin/path inside a flake input that holds source rather than the built package. Every assertion over the generated systemd unit passed, the check was green, and the service failed on every timer run on three machines with nothing watching. The fix is the companionallod/archetypesbranch; this is the general lesson, placed under the creation rule it extends.What changes for a running machine: nothing, this is memory.
Refs allod/strategy#55
Risk
R0. Two lines in a topic file; the budget hook passes at 3.9KB of the 24KB topic cap.
A third defect in the trace timer, found while auditing worktrees rather than by any check.
The unit had been failing since 13 September with
ssh: Could not resolve hostname forge.anarch.diy. The wrapper reached the forge before it distilled, so every run during a network outage captured nothing at all. Nothing surfaces a failed systemd user timer, so it sat red for five days. One more branch to relay, then the archetypes pin invnprc/deployand a rebuild.allod/archetypes
The hourly trace timer captured nothing during three network outages on nix-dev over five days, and this fixes the ordering that caused it. The wrapper pulled from the forge before it distilled, so an unreachable remote aborted the run at its first line, leaving the sessions it would have kept only on local disk. That is the exact loss the timer exists to prevent when a machine is reprovisioned.
What changes for a running machine: distilling and committing happen first, because they are local and always possible. The sync runs afterwards, on every invocation rather than only when that run committed, so a commit left behind by an offline run is pushed by a later one. An unreachable forge now exits zero with a note instead of failing the unit; a rejected push still retries once and then fails loudly.
What stays identical: the timer, the unit, the disabled case, the redaction, and what a successful run produces. A run with nothing new to capture and nothing pending still does nothing.
What proves it: the check gains the ordering as a property and is proven able to fail. With the pull moved back ahead of the distil it names both line numbers and fails the build; restored, it passes. The earlier assertions, including executing the binary the wrapper names, still run.
If it is wrong: revert. The service only ever commits to
vnprc/traces.Refs allod/strategy#55
How this was found
Auditing worktrees, not by a check. The unit had been red since 13 September with
ssh: Could not resolve hostname forge.anarch.diy, and nothing surfaces a failed user timer. Worth considering a follow-up that reports a repeatedly failing user unit, since this is the second defect in this module that only a person looking would see.Risk
R3. A generated systemd user service on every dev VM that enables the timer. The change is ordering plus a tolerated network failure; no new authority, no new writes.
Validation
nix build .#checks.x86_64-linux.memory-tracespasseswrapper reaches the network at line 20 before distilling at line 21; an offline run would capture nothingsystemctl --user start memory-traces.serviceon nix-dev before this change succeeded once the network returned, pushingtraces: nix-dev 2026-09-18T20:29:45ZThe trace timer has a fourth defect, and the fix plus two companion changes are prepared for relay. The timer was declared with only monotonic triggers (
OnBootSec,OnUnitActiveSec), so the first user-manager restart after boot, which every logout causes on a machine without lingering, left itactive (elapsed)with no next trigger until reboot. On one dev VM it ran once, then sat elapsed for three days while fifteen sessions went uncaptured. Filed as allod/archetypes#116 with the evidence. PR bodies for the two public branches follow, to be used verbatim by the agent that opens each PR from a machine with public push access.allod/archetypes
The hourly session-trace timer stops after the first logout on every machine that enables it, and this makes it keep running. The timer was declared with two monotonic triggers; when the systemd user manager restarts, which every logout does on a machine without lingering, the new manager has no activation of its own to count from and marks the timer elapsed until the next reboot. It now fires on a wall-clock
OnCalendar=hourlytrigger, which any manager can compute, andPersistent=truefires a run that was missed while no manager was running.What changes for a running machine: after a rebuild on the new pin,
memory-traces.timershows aNEXTtime insystemctl --user list-timersafter every login, and a session missed during a logout is captured at the next manager start.What stays identical: the service, the wrapper, the redaction, the disabled case, and
OnBootSec=10min. The module parameter that set the interval is renamedcalendar, so a deployment that still passed a timespan such asinterval = "30min"fails at evaluation instead of rendering a timer systemd refuses to load; no deployment is known to pass it.What proves it: the check no longer greps for the trigger line. It resolves the
OnCalendarvalue out of the generated timer and executessystemd-analyze calendaron it inside the sandbox, so a missing wall-clock trigger or an expression that does not parse fails the build. A sabotage run with the calendar line removed fails withtimer names no OnCalendar trigger.If it is wrong: revert. The only behaviour change is when the timer fires.
Closes allod/archetypes#116. Refs allod/strategy#55.
Risk
R3. A generated systemd user timer on every dev VM that enables it. The change is the trigger clause and a parameter rename; no new authority, no new writes.
Validation
nix build .#checks.x86_64-linux.memory-tracespasses; its output includesOnCalendar=hourly (parses)and still executes the binary the wrapper names and asserts the distil-before-network orderingOnCalendarremoved from the module: build fails withmemory-traces: timer names no OnCalendar trigger; a monotonic-only timer (OnUnitActiveSec) does not survive a user-manager restartgit diff --checkcleanOnBootSecalready being past, does not occur once the persistent stamp exists, because systemd disables an elapsed one-shot trigger when a last trigger is known; on a stampless first start the second run is a no-op. Not applied.allod/memory
The
memory-backpassskill reads one or more traces checkouts instead of exactly one, because a deployment may keep a separate private traces repository for a machine whose forge account must not read the others' sessions. Nothing changes for a running machine: this is procedure prose, and no timer runs the skill yet.What changes: Inputs item 1 is one or more checkouts with the same
<machine>/<harness>/layout;analyzed.txtlives in each checkout and tracks that checkout's own traces; the procedure pulls every checkout, iterates traces across all of them, and commits each checkout'sanalyzed.txtit touched; the Failures line stops on any missing checkout.What stays identical: every gate, the self-exclusion marker, both prompts, the synthesis rules.
What proves it:
.hooks/memory-budget --worktreeexits 0;git diff --checkclean.If it is wrong: revert.
Refs allod/strategy#55.
Risk
R0. Skill prose.
Both public branches were revised after owner review and rebased onto current master; these bodies supersede the ones in the previous comment. The archetypes branch keeps its code comments to what the code does and lets the README carry the explanation. The memory branch grew the relay handling for public edits: sightings for public rules accumulate in the private ledger with a candidate tag, public edits become an unpushed branch plus one receive command, and the private PR carries a
To relaychecklist and label so an unrelayed edit stays visible.allod/archetypes
The hourly session-trace timer stops after the first logout on every machine that enables it, and this makes it keep running. The timer was declared with two monotonic triggers; when the systemd user manager restarts, which every logout does on a machine without lingering, the new manager has no activation of its own to count from and marks the timer elapsed until the next reboot. It now fires on a wall-clock
OnCalendar=hourlytrigger, which any manager can compute, andPersistent=truefires a run that was missed while no manager was running.What changes for a running machine: after a rebuild on the new pin,
memory-traces.timershows aNEXTtime insystemctl --user list-timersafter every login, and a session missed during a logout is captured at the next manager start.What stays identical: the service, the wrapper, the redaction, the disabled case, and
OnBootSec=10min. The module parameter that set the interval is renamedcalendar, so a deployment that still passed a timespan such asinterval = "30min"fails at evaluation instead of rendering a timer systemd refuses to load; no deployment is known to pass it.What proves it: the check no longer greps for the trigger line. It resolves the
OnCalendarvalue out of the generated timer and executessystemd-analyze calendaron it inside the sandbox, so a missing wall-clock trigger or an expression that does not parse fails the build. A sabotage run with the calendar line removed fails withtimer has no OnCalendar trigger.If it is wrong: revert. The only behaviour change is when the timer fires.
Closes allod/archetypes#116. Refs allod/strategy#55.
Risk
R3. A generated systemd user timer on every dev VM that enables it. The change is the trigger clause and a parameter rename; no new authority, no new writes.
Validation
nix build .#checks.x86_64-linux.memory-tracespasses; its output includesOnCalendar=hourly (parses)and still executes the binary the wrapper names and asserts the distil-before-network orderingOnCalendarremoved from the module: build fails withmemory-traces: timer has no OnCalendar triggergit diff --checkcleanOnBootSecalready being past, does not occur once the persistent stamp exists, because systemd disables an elapsed one-shot trigger when a last trigger is known; on a stampless first start the second run is a no-op. Not applied.allod/memory
The
memory-backpassskill reads one or more traces checkouts, and it now carries public memory edits across the private/public boundary as an unpushed branch with one receive command instead of prose. Nothing changes for a running machine: this is procedure prose, and no timer runs the skill yet.What changes: Inputs item 1 is one or more checkouts with the same
<machine>/<harness>/layout, each with its ownanalyzed.txt. Every ledger line the skill writes goes to the private ledger; a public one ends withPublic <topic>.md candidateso a second session can corroborate it before relay, and the public ledger is never written. Each public edit the synthesis proposes is applied by the procedure in a public worktree, committed with its quotes, budget-checked, and left unpushed; the private PR'sTo relaysection is a checklist of receive commands, one per public branch, and the PR is labelledrelaywhen the list is non-empty. Each run opens by listing mergedmemory-backpass:PRs whose checklist still has an unchecked line. Report-only prints the would-be public edits instead of creating worktrees.What stays identical: every gate, the self-exclusion marker, both prompts, the synthesis rules, the two-session floor.
What proves it:
.hooks/memory-budget --worktreeexits 0;git diff --checkclean.If it is wrong: revert.
Refs allod/strategy#55.
Risk
R0. Skill prose.
Qualification run 1 of
memory-backpassis done, by hand on nix-dev in--report-onlymode.memory-backpass-run-1-evidence.mdbeside it. Both are private because they quote traces.Refs allod/strategy#55
Owner decisions recorded 2026-10-08, gating slice 2:
memory-backpass-run-{1,2}-decision.md, linked from the private notes; run 2: 27 traces, 168 findings, 158 quote-verified, 0 failed traces).modeoption are dropped with it — the timer runs write mode or nothing.Consequence for the plan text: the "Mail" interface contract and the mode option paragraph are superseded; the module loses the mail options and
MEMORY_BACKPASS_MODE. Plan-file edit to follow via the relay with the module branch.Slice 2 proceeds under
delegate-impl(R3): one implementation worker, one cross-family cold review, one fix round; the branch is handed on as a prepared patch, never pushed by the qualifying agent.complete, we'll just run it manually for now