Type something to search...

The factory remembers, and memory never decides

Part 5: a factory writes every failure to memory with its exit code and its own words, lets counters make the decisions, and keeps the line moving when memory is down or was never installed.

Warning

Viewing a draft version — this page is not published.
The factory remembers, and memory never decides

Here is the thing we want. A factory works through a pile of tickets overnight and some of them fail. In the morning you do not want a count. You want an answer: what failed, with which exit code, what the run itself said, and whether it has happened before.

You also want one more thing, which sounds like the opposite. None of that may change what the factory does next. Memory carries the explanation. Counters carry the decision.

In this part a factory, ynf, writes to a memory, ynm, in one direction and no further. Every failure goes in. Nothing comes back out into a prompt. We cause three failures of one kind, read what they left behind, let one model call turn them into a sentence that names the cause, and then take memory away to see whether the line stops.

Memory that switches itself on

A tool that insists on a memory is a tool you cannot try. ynf looks for ynm on the PATH and uses it if there is somewhere to put things. Run it with ynm missing and it says so plainly, once, and carries on:

ynf doctor, with no ynm on the PATH

—  memory  off: ynm is not on PATH

a reason, not a refusal. nothing is configured to turn memory off.

What this makes possible. Memory you can adopt without a migration. Install ynm beside a running factory and the next failure is remembered. The memory line in ynf doctor tells you whether it is on and where.

Every failure leaves an explanation behind

A failure that leaves only a red mark teaches nothing. The sandbox lane runs a command that always exits 3, and the rule says retry twice, then escalate. That is three runs and three failures of one kind. Here is what the first one left in memory:

ynm list, the first record
sig/outcome/error  lane: breaks  occurrence 1

Failure sig/outcome/error in lane breaks: the run ended error, exit 3; it reported: “exit 3: the build step failed”. Occurrence 1. ynf then decided: outcome.error: retry 1 of 2: starting run.

the subject is built from what the run reported, with no model in the loop

The text says what happened, what the command itself said, and what ynf did about it. The exit code and the run’s words are also fields in the record, so a tool can select on them without reading prose.

What this makes possible. In the morning you ask for everything that failed with exit 3 and get the command’s own words back, not a status badge. The failure arrived with its explanation because the writer put it there.

Counters decide, memory explains

Same failures, two places that hold them. The factory’s counters are what lane rules read:

ynf items show, the counters
{“retry/outcome.error”:3,“sig/outcome/error”:3}sig/outcome/error  occurrence 1  exit 3: the build step failed
the lane rule retry: 2, then escalate was decided from the first number

The number 3 is in both, and only one is consulted.

                           one failed run
                                  │
                ┌─────────────────┴─────────────────┐
                │ decides                           │ explains
                ▼                                   ▼
 ┌──────────────────────────────┐    ┌──────────────────────────────┐
 │ COUNTER in the factory's     │    │ RECORD in memory (ynm)       │
 │ own store (ynf)              │    │                              │
 │                              │    │ exit code, the run's own     │
 │ lane rules read it:          │    │ words, the decision          │
 │ retry, or escalate           │    │                              │
 │ can be replayed              │    │ people and agents read it    │
 └──────────────────────────────┘    └──────────────────────────────┘

One failure, two readers. Only the counter is allowed to decide.

Memory can be late, summarised or rewritten by a model, and a decision made from it could not be replayed. A counter can.

What this makes possible. A division of labour you can say in one line: guards read counters, people and agents read memory. A retry limit built on counters behaves the same on a bad day, with the explanation beside it for whoever looks into why.

The factory never feeds memory into a prompt

Why not paste the earlier failures into the next attempt’s prompt? Because then the same ticket could get a different task on a different day, and a run could no longer be explained.

┌─────────────────┐                        ┌────────────────────┐
│ FACTORY (ynf)   │                        │ MEMORY (ynm)       │
│                 │                        │                    │
│ builds the task │ writes every failure   │ keeps every        │
│ file and prompt │ ─────────────────────▶ │ failure, with its  │
│                 │ ◀─── ╳ never ╳ ─────   │ explanation        │
└─────────────────┘                        └────────────────────┘
                                                      ▲
                                                      ┆ its own agent,
                                                      ┆ in the open
                                              ┌───────┴──────────┐
                                              │ HARNESS (ynh)    │
                                              └──────────────────┘

The arrow from memory back into the task file does not exist.

So we compared the task files of the three attempts, prepared while ynm already held the earlier failures:

the three task files for one ticket
c38743fbf429  attempt 1a734d797d0d6  attempt 2a734d797d0d6  attempt 3

attempts 2 and 3: identical

mentions of memory, sig/ or occurrence: 0
attempt 2 differs from 1 by the previous run's own result, taken from ynf's log

What this makes possible. Reproducibility you can promise: the same ticket gets the same prompt every time, so a run can be re-explained from its inputs. A harness that does want memory connects its own agent to ynm, in the open, as that agent’s choice and not the factory’s. Part 6 does exactly that.

”This keeps happening” names its own cause

Three occurrences of one subject is a pattern, and ynm can say so. A dry run shows what it would do and never calls a model. Then comes the one real call, with the claude CLI as the writer. In our run it took about thirteen seconds and 645 output tokens, which is pennies at list price.

ynm dream --passes reflect, and the reflective memory
reflect: 1 candidates, changed: personal:sig/outcome/error

On 2026-10-08, the ticket in lane breaks failed with sig/outcome/error three times in a row. Each run ended in error with exit 3 and the same message: “exit 3: the build step failed”.

The failure was the same every time: exit 3 at the build step.
a model wrote this, so yours will differ. linked derives-from to the three occurrences, which are untouched.

That is the sentence this part was built to produce, and the model did not find it. It was written into the records in the second step, and the reflection could only repeat what the records carried. An earlier version of the tool recorded the decision and not the detail, and its reflection concluded there was “no underlying cause”. There was one. We had not recorded it. Writing this tutorial found that, and it is fixed.

BEFORE ┌──────────────────────┐        ┌──────────────────────┐
       │ the record held      │        │ reflection said:     │
       │ only the             │        │ "no underlying       │
       │ decision             │ ─────▶ │ cause"               │
       └──────────────────────┘        └──────────────────────┘

NOW    ┌──────────────────────┐        ┌──────────────────────┐
       │ the record holds the │        │ reflection names:    │
       │ exit code and the    │        │ "exit 3: the build   │
       │ run's words          │ ─────▶ │ step failed"         │
       └──────────────────────┘        └──────────────────────┘

Memory carries the explanation only if the writer put it there.

The reflection costs one call per pattern, not one per failure, and nothing in ynf reads it.

What this makes possible. A standing note on every recurring failure, written by a machine and grounded in records you can read: this keeps happening, and here is what it is.

Memory goes down, the line keeps moving

Memory will go away on a bad day, and the factory should not go with it. We put a stand-in ynm that always fails first on the PATH, then caused a fresh failure on a third ticket:

ynf sweep with ynm down, then ynm restored

—  memory queue  3 memory writes queued

memory remember, queued · err=“ynm: store unreachable”{“retry/outcome.error”:3,“sig/outcome/error”:3}ynm restored, one sweep later: queue line gone, ynm holds 6
the item escalated exactly as before. memory never stops a step.

The counters held all three failures while ynm still held only the first three. Restoring ynm and sweeping again sent what was waiting.

What this makes possible. A memory that can be restarted, upgraded or briefly lost without a ticket stopping. The factory’s work and its record of the work are separate, and the second catches up when it can.

Detected, never required

The other way for memory to be missing is that it was never installed. We ran ynf with a PATH that has no ynm on it, and a fourth ticket went through its three attempts as usual. Nothing in the log mentioned memory:

ynf with no ynm at all
ynm found: 1

—  memory  off: ynm is not on PATH

memory lines in the log: 0factory counters: 9 sig/outcome/error  ·  ynm holds: 6
counters are the factory's own and complete. memory is what ynm was around to be told.

What this makes possible. A factory you can hand to a team before they have chosen a memory, and add one to later. Nothing breaks when it is gone.

The Debrief

TL;DR

This part connects the factory to memory in one direction only. Every failure is written to memory with its exit code, the run’s own words and what the factory decided. Nothing comes back out of memory into a prompt.

Three identical failures become one sentence naming the cause, written by a single model call from records the factory wrote. Then memory is taken away. Failures queue up and are written later, and with no memory installed at all the factory carries on as if nothing were missing.

The ideas

  • The factory looks for memory and uses it if it is there. It never requires it.
  • A failure carries its explanation because the writer put it in the record.
  • Counters decide and memory explains. The same failure is in both, and only the counter is consulted.
  • Memory never enters the prompt, so the same ticket always gets the same task.
  • Memory writes queue and flush later, so the line keeps moving when memory is down.

What to take away

  • Make the writer record the explanation. A reflection can only repeat what the records hold.
  • Put every decision on counters, not on memory.
  • Keep memory out of the prompt, and make it optional.

Tutorial

You will build the whole of Part 5 yourself: a sandbox factory with two lanes, a memory it writes every failure to, and the four things the article describes (counters decide, memory explains, no memory in the prompt, memory down or absent). About ten minutes, a little of it waiting on GitHub. One model call, in step 8: about thirteen seconds and 645 output tokens in our run, which is pennies at list price. ynm prints no cost for it; Claude Code’s own usage view shows the real figure. Nothing else costs anything.

1. Install the tools

brew install eyelock/tap/ynh eyelock/tap/ynm eyelock/tap/ynf
ynh --version    # tested with 0.12.0
ynm --version    # tested with 0.4.1
ynf --version    # tested with 0.2.0

On Linux, the same versions are on each project’s GitHub releases page: ynh, ynm and ynf.

You also need Docker running (the lanes run in a container, and the first run pulls golang:1.26-alpine), jq, git with an identity set, gh signed in with the repo, workflow and delete_repo scopes (gh auth refresh -s repo,workflow,delete_repo if it is not), and Claude Code signed in, for the one model call in step 8. Commands are written for bash: on macOS, run bash first.

2. Start clean

Everything lives under one folder, and the tools get their own homes there, so nothing touches your own ~/.ynm or ~/.ynf. Run this every time you start or restart the part:

export LTL="$HOME/ltl"                    # any folder you like; keep the path short
export YNM_HOME="$LTL/.home/ynm-part5" YNM_USER=ltl
export YNF_HOME="$LTL/.home/ynf"
mkdir -p "$YNM_HOME" "$YNF_HOME"

# GitHub's search index lags a new label by a few seconds to a minute. A sweep that runs before
# it catches up finds nothing, so wait until the lane's search can see the issue.
wait_indexed() {
  local n
  for _ in $(seq 1 60); do
    n=$(gh api -X GET search/issues -f q="repo:$REPO is:issue is:open label:\"$1\"" -q .total_count)
    [ "$n" -ge 1 ] && return 0
    sleep 3
  done
  echo "search never saw label $1" >&2; return 1
}

Now remove anything a previous run of this part left behind, then fetch the target repository fresh (it is public, in the ynf repository):

gh repo delete "$(gh api user -q .login)/ltl-part5-factory" --yes 2>/dev/null
rm -rf "$LTL/part5" "$YNM_HOME" && mkdir -p "$LTL/part5" "$YNM_HOME"
cd "$LTL/part5"
curl -sL https://github.com/eyelock/ynf/archive/refs/tags/v0.2.0.tar.gz | tar xz
cp -R ynf-0.2.0/sandbox/seed seed && rm -rf ynf-0.2.0 seed/.agents seed/.github
ls seed

You should see README.md cmd go.mod internal scripts, a small Go module with a format package for the lanes to work on. The gh repo delete prints nothing the first time; an error from it is fine.

Coming back later, in a fresh shell: re-run the first block of this step, then the four exports at the end of step 3 (cd, REPO, YNF_CONFIG, GITHUB_TOKEN). The sandbox repository is still on GitHub, so do not repeat the rest of step 3.

3. Create the sandbox and give it two lanes

A private repository with a CI workflow that only checks gofmt, a four-line ynf config with no memory: block (on purpose), and four issues, one for each demonstration.

cd "$LTL/part5"
export REPO="$(gh api user -q .login)/ltl-part5-factory"
gh repo create "$REPO" --private --description "Loosen the Leash part 5, disposable" --clone
mv ltl-part5-factory work
cp -R seed/. work/
mkdir -p work/.github/workflows work/.ltl work/.agents/factory

cat > work/.github/workflows/ci.yml <<'EOF'
name: ci
on:
  pull_request:
jobs:
  gofmt:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: gofmt is clean
        run: test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }
EOF

cat > work/.ltl/config.yaml <<'EOF'
version: 1
repos: [__REPO__]
lease: { ttl: 30s, heartbeat: 10s }
poll: { ci: 10s, review: 1m }
EOF
sed -i.bak "s|__REPO__|$REPO|g" work/.ltl/config.yaml && rm work/.ltl/config.yaml.bak

Now the two lanes. Both run a plain command in a container with no network and no model. breaks plays a failing build: the command always exits 3, which is a stand-in, said plainly, because it makes the failure the same every time. drifts succeeds and writes an untidy Go file.

▸ Show the two lanes (61 lines, open and copy the whole block)
cat > work/.agents/factory/lanes.yaml <<'EOF'
version: 1

defaults:
  executor: docker
  attempts: 3
  retention: 30d

lanes:
  # A command that always exits 3. ynf reads that as outcome "error" and counts it under
  # sig/outcome/error. The lane retries twice and then escalates, so one issue produces three
  # occurrences of the same signature, and the third is the one that stops it.
  breaks:
    kind: originate
    intake:
      - github.search: 'repo:__REPO__ is:issue is:open label:"ynf:breaks"'
        every: 5m
    run:
      runner: command
      image: golang:1.26-alpine
      command:
        argv: ['sh', '-c', 'echo "the build step failed" >&2; exit 3']
      egress:
        allow: []
    when:
      converged: open_pr
      outcome.error: { retry: 2, then: escalate }
    pr:
      allowed_paths: ['**/*.go']
    labels:
      on_claim:    { add: [ynf:working], remove: [ynf:breaks] }
      on_escalate: { add: [ynf:needs-human], remove: [ynf:working] }

  # A command that succeeds and writes a Go file that is not gofmt-clean. The run converges, ynf
  # opens a pull request, and CI (a gofmt check) fails: the sensors passed and the real gate did
  # not. ynf calls that sig/ci-diverges/<check>.
  drifts:
    kind: originate
    intake:
      - github.search: 'repo:__REPO__ is:issue is:open label:"ynf:drifts"'
        every: 5m
    run:
      runner: command
      image: golang:1.26-alpine
      command:
        argv: ['sh', '-c', 'printf "package format\n\nfunc   Untidy( ){}\n" > ./{label.pkg}/untidy.go']
      egress:
        allow: []
    when:
      converged: open_pr
      ci_failed: escalate
    pr:
      allowed_paths: ['**/*.go']
    labels:
      on_claim:    { add: [ynf:working], remove: [ynf:drifts] }
      on_propose:  { add: [ynf:proposed], remove: [ynf:working] }
      on_review:   { add: [ynf:in-review], remove: [ynf:proposed] }
      on_escalate: { add: [ynf:needs-human], remove: [ynf:working, ynf:proposed] }
EOF
sed -i.bak "s|__REPO__|$REPO|g" work/.agents/factory/lanes.yaml && rm work/.agents/factory/lanes.yaml.bak

Commit it, create the labels and the four issues, and set up the shell for ynf:

cd "$LTL/part5/work"
printf '.ltl/\n.ynm/\n' >> .git/info/exclude
git add -A && git commit -qm "seed: Loosen the Leash target, plus CI and the two lanes" && git push -q -u origin HEAD
for l in ynf:breaks ynf:drifts ynf:working ynf:proposed ynf:in-review ynf:needs-human pkg:internal/format; do
  gh label create "$l" -R "$REPO" --force >/dev/null
done
for t in "build step fails, issue one" "untidy file, issue two" "build step fails while ynm is down" "build step fails with no ynm"; do
  gh issue create -R "$REPO" --title "$t" --label pkg:internal/format --body 'Part 5 demo issue.'
done

export YNF_CONFIG="$PWD/.ltl/config.yaml"
export GITHUB_TOKEN="$(gh auth token)"
ynf lanes validate --repo "$REPO" --file .agents/factory/lanes.yaml

You should see four issue URLs, then valid, 2 lanes (breaks, drifts). The last two exports matter. YNF_CONFIG points ynf at the config you just wrote. GITHUB_TOKEN puts your GitHub token in this shell for ynf: ynf normally asks gh auth token for it, but step 10 runs ynf with a stripped PATH, and with the token already in the environment ynf does not depend on finding gh. There is no agent in this part to leak it, which is why Parts 4 and 6, where an agent runs on your machine, do the opposite. It is still a live token in your shell, and Clean up unsets it. Every command from here on runs from $LTL/part5/work.

4. Memory that switches itself on

A tool that insists on a memory is a tool you cannot try. ynf looks for ynm on the PATH and uses it if there is somewhere to put things. Give the repository a memory:

cd "$LTL/part5/work"
ynm init --personal && ynm init
ynf doctor | grep -E '^(ok|--|FAIL) +(ynm|memory)'
ynm list

You should see, trimmed:

personal store created at .../.home/ynm-part5/store.git
...
ok    memory                   on: ynm on PATH, the project store at .../part5/work/.ynm
no memories

If the personal store path is not under your $LTL folder, stop: you are about to write into your own memory. Check that YNM_HOME is exported.

5. Every failure leaves an explanation behind

Label issue 1 for the breaks lane and sweep. The command exits 3 and the lane says retry twice, then escalate: three runs, three failures of one kind.

gh issue edit 1 -R "$REPO" --add-label ynf:breaks
wait_indexed ynf:breaks
ynf sweep --until-settled --timeout 3m --lane breaks
ynf items log "$REPO#1"

The sweep logs its progress as it goes, and the first run pulls the container image. You should see the item escalate, and in its log the loop three times round:

github.com/<you>/ltl-part5-factory#1  breaks  escalated      outcome.error after 2 retries
run      ...-1 command via docker: error exit 3: the build step failed, 0 changed
decision ynf.run.finished -> running: outcome.error: retry 1 of 2: starting run [run]
...
decision ynf.run.finished -> escalated: outcome.error after 2 retries [escalate]

If the sweep prints an empty table, the search index was late: run the sweep again. Now ask ynm what it was told:

ynm list --namespace factory/ --json | jq -c 'sort_by(.createdAt)[] | {subject, occurrence: .current.data.count, tags}'
ynm list --namespace factory/ --json | jq -r 'sort_by(.createdAt)[0] | .current.content'
{"subject":"sig/outcome/error","occurrence":1,"tags":["ynf","ynf.failure.v1","lane:breaks","failure","occurrence"]}
...
Failure `sig/outcome/error` on github.com/<you>/ltl-part5-factory#1 in lane `breaks`: the run ended error, exit 3; it reported: "exit 3: the build step failed". Occurrence 1, ... ynf then decided: outcome.error: retry 1 of 2: starting run.

Three records, one subject, each saying what happened, what the command itself said, and what ynf did about it. The exit code and the run’s words are also fields in the record’s data, so a tool can select on them without reading prose.

6. Counters decide, memory explains

A second kind of failure, for contrast. The drifts command succeeds and writes an untidy Go file; ynf opens a pull request and the gofmt check in CI fails.

gh issue edit 2 -R "$REPO" --add-label ynf:drifts
wait_indexed ynf:drifts
ynf sweep --until-settled --timeout 5m --lane drifts
gh pr checks "$(gh pr list -R "$REPO" --state all --json number -q '.[0].number')" -R "$REPO"

You should see the item escalate with ci_failed, and gofmt fail from the check, which exits 1 by design. If the check still says pending, wait half a minute and run the last line again. Now the same failures in the two places that hold them. First the factory’s counters, which is what lane rules read:

ynf stats --lane breaks --lane drifts | head -9
ynf items show "$REPO#1" | jq -c '.counters'
grep -n '^ *outcome.error:' .agents/factory/lanes.yaml
<you>/ltl-part5-factory breaks, top failure signatures:
     3  sig/outcome/error
...
{"retry/outcome.error":3,"sig/outcome/error":3}
26:      outcome.error: { retry: 2, then: escalate }

Then ynm, for the same signatures:

ynm list --namespace factory/ --json | jq -r 'sort_by(.createdAt)[] | "\(.subject)  occurrence \(.current.data.count)  \(.current.data.excerpt // .current.data.failed_checks[0])"'
sig/outcome/error  occurrence 1  exit 3: the build step failed
...
sig/ci-diverges/gofmt  occurrence 1  gofmt

The number 3 is in both, but only one is consulted: the rule retry: 2, then escalate was decided from the counter retry/outcome.error in ynf’s own store. Steps 9 and 10 take memory away and the decisions do not change.

7. The factory never feeds memory into a prompt

Every run has a task file. Compare the three for issue 1, prepared while ynm already held the earlier failures:

(cd "$LTL"/part5/work/.ltl/work/steps/item_*_issues_1
 set -- */run/task.md
 shasum "$@" | awk '{print substr($1,1,12), $2}'
 diff "$1" "$2"
 diff "$2" "$3" && echo "attempts 2 and 3: identical"
 echo "mentions of memory, sig/ or occurrence: $(cat "$@" | grep -ci 'memory\|sig/\|occurrence')")
c38743fbf429 ...-1/run/task.md
a734d797d0d6 ...-2/run/task.md
a734d797d0d6 ...-3/run/task.md
...
> ## Feedback from the previous attempt
...
> The previous run ended error: exit 3: the build step failed
attempts 2 and 3: identical
mentions of memory, sig/ or occurrence: 0

The second attempt differs from the first by one block, the previous run’s own result from ynf’s log. Attempts 2 and 3 are identical, and none mentions memory, though ynm held records when they were prepared. (The hashes will differ on your machine; what matters is that the last two match.)

8. “This keeps happening” names its own cause

A dry run shows what ynm would do, and never calls a model:

ynm dream --dry-run --namespace factory/
...
dedupe: 0/0 would change
contradict: 0/0 would change
reflect: 1 candidates (not judged; add --judge)
...

Now the one model call. With the claude CLI on your PATH, ynm uses it as its writer. Skip this block and nothing else changes.

ynm dream --passes reflect --namespace factory/ --json | jq -c '{judge:.full.judge,writer:.full.writer,reflect:.passes.reflect}'
ynm list --type reflective --json | jq -r '.[] | "\(.subject)\n\(.current.content)"'
ynm export --type reflective | jq -c 'select(.op=="create") | {type,subject,links:[.links[]|.rel]|group_by(.)|map({(.[0]):length})|add}'
ynm list --type episodic --json | jq -c '[.[]|{subject,tombstoned,versions}] | group_by(.subject) | map({subject:.[0].subject, n:length, tombstoned:(map(.tombstoned)|any)})'
{"judge":{"name":"writer-emulated:claude-cli","calibrated":false},"writer":"claude-cli","reflect":{"candidates":1,"changed":["personal:sig/outcome/error"]}}

A model wrote the reflective memory, so yours will read differently, but it should name exit 3 and “the build step failed”:

On 2026-10-08, github.com/<you>/ltl-part5-factory#1 in lane `breaks` failed with `sig/outcome/error` three times in a row, ... Each run ended in error with exit 3 and the same message: "exit 3: the build step failed".
...
{"type":"reflective","subject":"sig/outcome/error","links":{"derives-from":3}}
[{"subject":"sig/ci-diverges/gofmt","n":1,"tombstoned":false},{"subject":"sig/outcome/error","n":3,"tombstoned":false}]

The model did not find the cause; it was written into the records in step 5, and the reflection could only repeat it. The reflection links derives-from the three occurrences, which are untouched. sig/ci-diverges/gofmt has one occurrence and so no reflection; it takes three. The judge is the writer checking itself and calibrated is false, so treat its checks as advisory. Nothing in ynf reads any of this.

9. Memory goes down, the line keeps moving

Put a stand-in ynm that always fails first on the PATH, cause a failure, and watch ynf keep the write.

mkdir -p .ltl/down
printf '#!/bin/sh\necho "ynm: store unreachable" >&2\nexit 69\n' > .ltl/down/ynm
chmod +x .ltl/down/ynm
gh issue edit 3 -R "$REPO" --add-label ynf:breaks
wait_indexed ynf:breaks
PATH="$PWD/.ltl/down:$PATH" ynf sweep --until-settled --timeout 3m --lane breaks 2>sweep.log
grep -c WARN sweep.log; grep WARN sweep.log | head -1
PATH="$PWD/.ltl/down:$PATH" ynf doctor | grep -E '^(ok|--|FAIL) +(ynm|memory)'
ynf items show "$REPO#3" | jq -c '.counters'
ynm list --namespace factory/ --json | jq '[.[]|select(.subject=="sig/outcome/error" and .type=="episodic")]|length'

You should see issue 3 escalate as issue 1 did, and, trimmed:

3
time=... level=WARN msg="memory remember, queued" subject=sig/outcome/error err="ynm remember: exit status 69: ynm: store unreachable"
--    memory queue             3 memory writes queued since 2026-10-08T12:29:19+01:00
--    ynm                      not found or not working (exit status 69)
{"retry/outcome.error":3,"sig/outcome/error":3}
3

The counters hold all three failures, doctor counts three writes waiting, and ynm still holds only the three from issue 1. Now put ynm back by removing the stand-in, and sweep again. The sweep sends what is waiting:

rm -rf .ltl/down
ynf sweep --until-settled --timeout 1m --lane breaks
ynf doctor | grep -E '^(ok|--|FAIL) +(ynm|memory)'
ynm list --namespace factory/ --json | jq '[.[]|select(.subject=="sig/outcome/error" and .type=="episodic")]|length'

You should see the memory queue line gone from doctor, and the count at 6: the three from issue 1 and the three that waited.

10. Detected, never required

The other way for memory to be missing: it was never installed. Run ynf with a PATH that has no ynm on it, only the few tools ynf needs:

mkdir -p .ltl/nopath
for t in ynf docker git; do ln -sf "$(command -v $t)" .ltl/nopath/$t; done
NOYNM="$PWD/.ltl/nopath:/usr/bin:/bin"
PATH="$NOYNM" command -v ynm; echo "ynm found: $?"
gh issue edit 4 -R "$REPO" --add-label ynf:breaks
wait_indexed ynf:breaks
PATH="$NOYNM" ynf doctor | grep -E '^(ok|--|FAIL) +(ynm|memory)'
PATH="$NOYNM" ynf sweep --until-settled --timeout 3m --lane breaks 2>sweep.log
echo "memory lines in the log: $(grep -ci memory sweep.log)"
ynf items show "$REPO#4" | jq -c '.counters'
ynm list --namespace factory/ --json | jq -r '[.[]|select(.type=="episodic")] | group_by(.subject)[] | "\(length)  \(.[0].subject)"'
ynf stats --lane breaks --lane drifts | head -9

If ynm found is not 1, your machine keeps ynm in /usr/bin or /bin: remove that directory from NOYNM and symlink what ynf needs. You should see, trimmed:

ynm found: 1
--    memory                   off: ynm is not on PATH
memory lines in the log: 0
{"retry/outcome.error":3,"sig/outcome/error":3}
1  sig/ci-diverges/gofmt
6  sig/outcome/error
...
     9  sig/outcome/error

Doctor says plainly why memory is off, the item runs its three attempts as before, and nothing in the log mentions memory. The counters say the signature has happened nine times across three items; ynm holds six, from items 1 and 3. Item 4 was never told.

Clean up

cd "$LTL"
gh repo delete "$REPO" --yes
rm -rf "$LTL/part5" "$YNM_HOME"
unset GITHUB_TOKEN YNF_CONFIG

This deletes ltl-part5-factory from GitHub (it needs the delete_repo scope: gh auth refresh -s delete_repo), the local folder and this part’s memory home, and removes the token from your shell. Your own ~/.ynm, ~/.ynf and ~/.ynh were never touched.

Next: all three together, and then taken apart, in Part 6.