Skip to content

fix(goal-loop): one unreadable loop no longer loses every goal loop (#1248) - #1258

Merged
Juliusolsson05 merged 13 commits into
mainfrom
fix/goal-loop-store-invalid-loop
Sep 25, 2026
Merged

Juliusolsson05 merged 13 commits into
mainfrom
fix/goal-loop-store-invalid-loop

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Closes #1248.

Verified failure

One loop that fails validation (a newer build's phase or reason, after a downgrade) made GoalLoopStore.read() move the whole goal-loop.json to .corrupt and throw. GoalLoopService.start() then ran empty, and its next write made every loop's loss permanent. An existing test pinned that whole-file behaviour. Plan: docs/plans/2026-09-25-goal-loop-store-invalid-loop.md (plan-only commit first).

Fix

  • Unreadable loop: set aside; every valid loop is read.
  • Preserved bytes: the original is copied to the existing .corrupt name before the next write can drop the unreadable loop. It is not moved, because the file still holds the loops just read.
  • Malformed container: unchanged; it still moves aside and throws.

Evidence

  • Two real loops from the owner's store, with prompts redacted: testing/fixtures/goal-loop/real-loops-2026-09-25.json.
  • Tests are red on main.
  • Mutants killed: whole-file refusal, no copy, move instead of copy.
  • tsc -b is clean; src/main/goalLoop passes (79 tests).

🤖 Generated with Claude Code

Juliusolsson05 and others added 10 commits September 25, 2026 04:18
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loop

One loop this build cannot read (a newer build's phase or reason, then a
downgrade) moved the whole goal-loop.json aside; the service started
empty and its next write made every loop's loss permanent. Valid loops
are now read, the original bytes are copied to the existing .corrupt
name before any write can drop the unreadable one, and a malformed
container still moves aside and throws.

Closes #1248

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ive file

Review A / steering q18: the set-aside copy was written inside the parse
try, so a failed copy (an unwritable or directory `.corrupt`) was handled
as an unreadable document. The live file was moved away and start()
persisted `{loops:{}}`, losing every loop and an older quarantine.
Preservation now runs outside that catch, to a digest-named atomic copy
that never replaces `.corrupt`. A failed copy, or a failed whole-file
move, makes the store refuse writes until a read succeeds, so the
service's start-empty persist cannot replace the only copy. The size
limit now counts readable loops, and a partial copy left by a crash is
not trusted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er guards; name set-aside ids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of lasting the process

The service reads the store only at start, so "refuse writes until a read
succeeds" disabled persistence for the whole process. Each write now first
retries the owed preservation (the digest copy, or the whole-file move to
.corrupt) and proceeds once it succeeds; it is still refused while it fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is owed (steering q19)

On a per-loop copy failure read() threw, so the service started empty.
Once the obstruction cleared, the retried write persisted that empty map
and dropped every valid loop. read() now returns the valid loops and owes
the copy: every write waits for it (the live file keeps the unreadable
loop until its bytes are safe), and the first write after it succeeds
carries the valid loops forward. The service-path test follows the
whole handoff: obstructed start, refused persist, repair, then a write
holding the valid loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Review disposition (2 rounds, plus steering q18/q19; merge on green)

  • Round 1: preservation failure could cost the live file → fixed (2127e48). Fields and container guards unpinned; set-aside ids unnamed → fixed (9bc52f8). A write refusal lasted for the process → fixed: it retries the owed copy (dc27fb0).
  • Steering q18/q19 (data-loss blockers): a failed copy dropped every valid loop → fixed: valid loops stay live while the copy is owed (b78119b).
  • Round 2: A MERGE (no new defects), B MERGE (verified on the real store; all five handoff mutants killed).

CI quality-gate is green on a head that contains origin/main. Closes #1248.

🤖 Generated with Claude Code

@Juliusolsson05
Juliusolsson05 merged commit c13de39 into main Sep 25, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/goal-loop-store-invalid-loop branch September 25, 2026 14:41
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
…ate rule, #1309

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
#1293 needs-evidence

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(goal-loop): one loop with an unknown phase moves goal-loop.json aside and loses every loop

1 participant