fix(sdk): implement f.memory.learn so findings persist across runs
Root cause
f.memory.learn never had an implementation. authoredMemory().learn() in packages/sdk/src/authored-memory.ts was a hard-coded stub that threw unsupported_verb. ai-hist 0.4.1 only exposes reads and tag writes, with no trajectory writer, so the write slice was deferred. Recall worked; any flow that tried to remember something crashed.
Fix
The journal comes first, then the file (principal decision: ship with the journal):
learn(finding)stages the finding's compacted trajectory file under a temporary name that no read lists. Disk failures such as space or permissions happen here, before anything is journaled.- It commits the finding to the durable root run's
memory-learnstream. That isstream.append, journaled by the kernel under the run lock. - Only then is the file renamed into place. Nothing is recallable that the journal does not record.
How the file and commit relate:
- Citation. The file cites its commit (
journal: {runId, stream, id}) and is derived only from it. - Competing attempts. After appending, the ledger re-reads the stream and publishes the first commit for that finding. Attempts racing on one root therefore converge on the same bytes.
- Resume. A resumed body that re-runs
learnfinds the commit and publishes it without journaling again. - Body failure. A failed body waits for in-flight learns before handing the root back.
- Journal errors propagate unchanged, so a read interruption or lost lease leaves the root resumable. A failed stream read is not cached.
Durable root run ID. Every flows run path already drives the body under a kernel root: driveRoot passes dispatch.run_id, and the Node child entry forwards it. learn requires that root and refuses with memory_unjournaled without one, rather than write unrecorded memory.
File location. Files go to <db-dir>/flows-memory/<scope-hash>/.trajectories/compacted/<id>.json, beside the ai-hist DB, so a flow deployed from a read-only checkout, package or image can still learn. The id hashes scope + finding, so re-learning a finding atomically replaces one file. The file takes the DB's mode. The ai-hist DB itself is never modified, so there is no lock and no race with ai-hist sync.
Reads. recall and why read the DB plus the scope's learned files, so the next run sees a finding without a sync. Once ai-hist has synced a finding, the DB row is returned and the disk copy is dropped. recall honours source/tag/beforeMs/limit for unsynced entries, and a negative id marks an entry read from disk. A damaged learned file (unparseable or the wrong shape) fails the read with memory_unreachable instead of being skipped. learn only stats the DB for its mode; it never loads it.
Fails closed:
memory.script: false→unsupported_header.- Malformed finding →
memory_finding_invalid. - Staging or publish failure →
memory_unwritable; a journal failure surfaces as the journal's own error, and nothing is published. - A
learnwhose outcome the body never consumed (pending or already settled) → the run fails withunawaited_step. flows run/flows resumereport the memory codes, includingmemory_unreachable, as their own diagnostic kinds and name the failed root run.
Docs. docs/SURFACE.md, testdata/memory/README.md and the surface type comment are updated. AUTHORING.md is regenerated, which also picks up drift already on main from #636.
Tests
tests/f-memory.test.ts(29 pass)- Journal order: no published file exists at append time, and the file cites its commit.
- Resume: a file lost after the commit is republished byte-identical with no second record.
- Competing attempts converge on the first commit; concurrent learns of one finding produce one record.
- Failure paths: a journal failure passes through unchanged and publishes nothing; a failed stream read is retried, not cached; a failed body waits for an in-flight learn; with no root run →
memory_unjournaled. - Reads: deduplication after a simulated sync; recall options on unsynced entries; damaged files of four shapes fail closed.
- Placement and mode: files stay out of the source directory and take the DB's mode.
- Unawaited learns: pending and already-failed calls both fail the run, while an awaited and handled failure is accepted.
- CLI: the reporters classify all four memory codes without the duplicated prefix.
- Mutation checks confirmed five of these tests fail without the code they cover: unawaited learns, resume dedup, body-failure settle, the read-failure retry, and first-commit convergence.
tests/memory-learn-live.test.ts: the built CLI and a realrelayflowd. The journal record sits on the root run, the file cites it, and a second run of the same flow recalls the finding throughwhyandrecall.- The executor, preflight, direct-flow and memory suites (615 tests) pass locally. Two unrelated environment failures: an authored-root test that times out under load and passes when run alone, and a Bun 1.4.2 vs pinned 1.4.0 version check.
Do not merge without principal review.
🤖 Generated with Claude Code
[!NOTE]
Medium Risk
Changes durable run journaling, filesystem writes beside the history DB, and recall merge semantics; mistakes could leak unjournaled memory or lose findings on resume, though the design fails closed and is heavily tested.
Overview
f.memory.learn is implemented so authored flows can persist script-memory findings across runs. The previous stub that refused with unsupported_verb is replaced by a journal-first pipeline: stage a compacted trajectory file, append to the durable root run’s memory-learn stream, then atomically publish under <db-dir>/flows-memory/… (no recall without a journal record; resume republishes from the stream without re-journaling).
Reads and execution rules change: recall / why merge the ai-hist DB with on-disk learned trajectories (dedupe synced rows; damaged files fail as memory_unreachable). The executor tracks learn promises, settles in-flight writes on body failure, and fails with unawaited_step if learn isn’t awaited before done(). New codes (memory_finding_invalid, memory_unwritable, memory_unjournaled) surface through dedicated CLI diagnostics instead of generic protocol errors.
Docs (SURFACE.md, surface types, testdata README) and broad unit/live tests cover ordering, idempotency, races, and placement beside the DB—not the flow source tree. AUTHORING.md also documents agent resume / sessionId (regenerated drift).
Reviewed by Cursor Bugbot for commit 73806b22a8bb6b310f594d8e94a78ca311556c4d. Bugbot is set up for automated code reviews on this repo. Configure here.
Agent Relay sessions