fix(sdk): implement f.memory.learn so findings persist across runs

Merged
KH
17d ago
+1013-38

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):

  1. 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.
  2. It commits the finding to the durable root run's memory-learn stream. That is stream.append, journaled by the kernel under the run lock.
  3. 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 learn finds 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 learn whose outcome the body never consumed (pending or already settled) → the run fails with unawaited_step.
  • flows run / flows resume report the memory codes, including memory_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 real relayflowd. The journal record sits on the root run, the file cites it, and a second run of the same flow recalls the finding through why and recall.
  • 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

  • claude session b6cf20aa · opened via gh pr create · last active 2026-10-10
  • claude session aee3b740 · contributor · ran gh pr commands · last active 2026-10-10
  • claude session 5863fddb · contributor · ran gh pr commands · last active 2026-10-10