Skip to content

senior-dev: keep the store in the run's record folder, write back a removed .senior-dev, and --state-dir - #1800

Open
ZeroPoint95 wants to merge 4 commits into
devfrom
zeropoint95/senior-dev-state-dir
Open

ZeroPoint95 wants to merge 4 commits into
devfrom
zeropoint95/senior-dev-state-dir

Conversation

@ZeroPoint95

@ZeroPoint95 ZeroPoint95 commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Keeps senior-dev's session store out of the folder it works in, and writes back a .senior-dev that a command deletes mid-run. A deleted .senior-dev, or a deleted link to one, no longer ends the run. Also adds --state-dir.

What changed

  • Why: two CyberGym runs ended unsubmitted because the model's own last command deleted .senior-dev.
    • openssl ($0.64): OpenSSL 1.1.0's make clean deletes every link in the tree, including the .senior-dev link to the store.
    • arduinojson: the rig's validate.py starts with sudo rm -rf /src.
    • The deleting step never reached the action log, because the store was destroyed before the step could be written.
  • The store goes in the run's record folder by default. A run codeaf carries keeps senior-dev.db, storage/ and projection.lock in <record>/store. It finds the record folder through CODEAF_RECORDS, and a later run of the same task gets store.1, store.2 and so on. Where it goes is decided early, but the directory is made only when the store opens, after every refusal. The store goes in .senior-dev only when there is no record folder, when the record folder is inside the workspace, or when it cannot be written.
  • The store is opened by its real path, once (filepath.EvalSymlinks), so a link that goes away no longer matters.
  • --state-dir DIR / SENIOR_DEV_STATE_DIR both win over the record folder, and the flag wins over the variable.
    • A directory inside the folder is refused before it is made or anything is spent. This holds even when it is spelled with different case on APFS.
    • The folder's own .senior-dev/ is the exception.
  • A deleted .senior-dev is written back.
    • spec.md, checklist.md, pinned.txt and steering.md are read again after every tool call and whenever senior-dev reads them itself.
    • A file that is gone is rewritten with its last-read content, and an implement · notes-rewritten record is logged.
    • A write-back never overwrites. Tool calls run concurrently, so the model can write its checklist while a write-back is in flight. The whole read holds a lock, and the copy is written to a file of its own and linked into place, which fails if anything is already there. keepSteering appends under the same lock.
    • tool-output/ needed no change: it already makes its directory before each write.
  • A removed or emptied store ends the run.
    • The store holds its directory, senior-dev.db and storage/ open, and checks all three before every write and every read of the conversation. If any is gone or replaced, the run stops with its store <dir> was removed while the run was working, with the conversation in it.
    • Reading the conversation now refuses on a removed store, so the ending names the store instead of "No user message found in stream". A write that fails re-checks the store for the same reason.
    • The open handles stop ext4 and overlayfs from giving a deleted directory's inode number to a new one.
    • Checking the database and storage/ as well catches an rm -rf that raced the store's own writes. That leaves the directory standing with its contents gone.
  • Not done, on purpose: the brief's §4.3 MkdirAll-and-retry. A new store would get the session made again under the same id, and the model would lose its history.
  • Manual:
    • The notes section and the "senior-dev stopped with projection.lock: no such file or directory" section are rewritten.
    • --state-dir is in the flags section. There are ten new probes: eight in TestTheVanishedStoreQuestionsReachTheStateDir and two in the main table.
    • The Notes comment in seniordev.go is updated.
  • Test harness: recordingHost gains Records() (it reads CODEAF_RECORDS, as codeaf's child host does), hermeticRun clears that variable, and modelAPIServer takes an optional script.

How it was checked

  1. TestDurableSurvivesTheWorkspaceLinkGoingAway fails on dev with open …/.senior-dev/projection.lock: no such file or directory, and passes here.
  2. TestANotesFolderTheWorkRemovesIsWrittenBack scripts a run whose model runs rm -rf .senior-dev.
    • Here the run hands in, the brief and checklist are written back, and the store is whole.
    • It fails without the write-back (nothing is submitted), and it fails without the record-folder default.
  3. Whole-run tests show where the store lands: TestACarriedRunKeepsItsStoreInTheRecordFolder, TestARunWithNoRecordFolderKeepsItsStoreInTheFolder, TestTheStateDirFlagKeepsTheStoreWhereItSays (the record folder stays empty) and TestTheStateDirVariableWinsOverTheRecordFolder. TestARefusedLaunchLeavesNoStoreInTheRecordFolder fails if the store is made early.
  4. TestAWriteBackNeverOverwritesAFileWrittenMeanwhile runs a write-back beside the model's write for 500 rounds under -race. The old write-back fails it in round 1.
  5. The store tests pass 3 out of 3 in alpine:3 on overlayfs. TestARemovedStoreEndsTheRunWithoutAFreshOne covers a store that is gone, replaced, made again, emptied, and replaced with the database closed. With the database closed, the old check was fooled 3 out of 3 times.
  6. These pass: gofmt, go vet ./..., race tests for internal/seniordev and its app and config packages, internal/manual and the packed manual, make test-laws, codeaf-changes check, cmd/codeaf -run 'SeniorDev|AShellRun|Carried', and internal/session -run 'Notes|SeniorDev'. make pr-ready was not run, as asked.
  7. The real bin/codeaf ran against a scripted provider on localhost:
    • The model runs rm -rf .senior-dev after writing its checklist. On dev the run crashed on projection.lock (exit 2) and forked the store (UNIQUE constraint failed: session.id). Here it finished (exit 0), wrote spec.md and checklist.md back, and kept all 6 messages in <record>/store.
    • The .senior-dev link is removed at the first call: finished (exit 0), with the store in <record>/store.
    • --state-dir: finished (exit 0), with the store where the flag said.
    • --state-dir <folder>/a/b: refused before any model call, and nothing was made in the folder.

Not in this PR

  • keepNotes already Lstats and skips a linked .senior-dev, so the brief's observation does not hold and nothing needed changing.
  • Restoring the project tree after rm -rf /src is left to the rig's backup.
  • Logging the in-flight tool call in the turn-error record is a separate follow-up.
  • Every senior-dev refusal ends as senior-dev crashed:. refused() has used StatusCrashed since senior-dev: a coding agent built into codeaf takes a whole task, in the folder itself #1488.

Checklist

  • A change entry: docs/changes/unreleased/1800-senior-dev-state-dir.md
  • The manual knows about it: internal/manual/chat/senior-dev.md
  • No new line in .github/known-red.txt
  • Only my own paths are staged

🤖 Generated with Claude Code

ZeroPoint95 and others added 2 commits October 9, 2026 10:04
…it outside the folder

A CyberGym run had anchored <folder>/.senior-dev to a store elsewhere through a
link. The projection lock was reopened through that link on every write, so when
the link stopped resolving an hour in, the next model turn ended the run
unsubmitted with `open <folder>/.senior-dev/projection.lock: no such file or
directory` while the store itself was intact.

The store's directory is now made, made absolute and resolved through every
link once when it is opened; the database, the flat records and the lock are
all rooted on that real path. `codeaf senior-dev run --state-dir DIR`, or
SENIOR_DEV_STATE_DIR, keeps the store in a directory of its own (the flag wins);
one inside the folder, other than under its own .senior-dev, is refused before
the run starts. The model's files stay in the folder's .senior-dev either way.

A store that is itself removed still ends the run, now naming it. It remembers
which directory it opened and writes into no other: a fresh empty directory in
its place (made by the next tool output under .senior-dev, or by anything else)
would let the next turn make the session again under the same id and hand the
model a conversation that starts where the removal happened.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…fore making it

Review of #1800. The store's identity check compared a Stat taken when it
opened with one taken now, and ext4 and overlayfs give a directory made
where one was just removed the old inode number back. It held only because
sqlite's pooled connection kept senior-dev.db open; with the database closed,
the check was fooled 3 out of 3 times in an alpine container and the next
write went into the new directory. The store now keeps its directory open
and compares through that handle.

--state-dir made the directory before judging it, so a refused <folder>/state
or <folder>/a/b/c was left in the tree. It is now judged from the deepest part
that exists, and made only once allowed; a folder spelled with other case on
APFS is refused as the folder. The "gone" ending no longer prints the path
twice, and the four flag bullets reworded in the manual are restored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@ZeroPoint95
ZeroPoint95 marked this pull request as ready for review October 9, 2026 16:09
@ZeroPoint95
ZeroPoint95 marked this pull request as draft October 9, 2026 17:24
ZeroPoint95 and others added 2 commits October 9, 2026 13:38
… a removed .senior-dev

Both CyberGym runs lost .senior-dev to the model's own last command: OpenSSL
1.1.0's make clean deletes every link in the tree, and the rig's validate.py
begins sudo rm -rf /src. The store is now kept out of the folder by default.

- With no --state-dir and no SENIOR_DEV_STATE_DIR, a run codeaf carries keeps
  its store in a directory of its own in the record folder codeaf names on
  CODEAF_RECORDS: <record>/store, or store.1, store.2 for a later run of the
  same task. Only with no record folder does it stay in .senior-dev.
- The brief, the checklist, the pinned command and the steering file are read
  again after every tool call and by senior-dev's own reads; one that is gone
  is written back as the run last read it, with an implement/notes-rewritten
  record and a line on the run's page.
- The removed-store check now also holds the database and the records'
  directory open: an rm -rf that raced the store's own writes left the
  directory standing with its contents gone, and the directory alone still
  matched. Conversation reads are checked too, and a write cut short by the
  removal names the store instead of the temporary file it was renaming.

Manual, the Notes comment and the change entry say where the store is now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ore is made when it opens

Review of #1800. Tool calls run at once (steploop's processor runs each in
its own goroutine but the question), so a write-back could land after the
model's own write of its checklist and put the old copy over it. readNote now
holds the kept notes' lock throughout, and the copy is written to a file of
its own and linked into place, which fails where anything already is; the
file then read is the one kept. keepSteering appends under the same lock.
Against the old write-back the new test fails in its first round.

The record folder's store was made before the config, --asked and window
refusals, so a refused launch left an empty store and the next got store.1.
Where it goes is still decided early (recordStoreBase); the directory is made
in newPipeline when the store opens (claimRecordStore).

Also: the notes section's heading says where the database is now, the change
entry says the store is checked before every write and every read of the
conversation, and the page's notes-rewritten line has a test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@ZeroPoint95 ZeroPoint95 changed the title senior-dev: open the store by its real path, and --state-dir to keep it outside the folder senior-dev: keep the store in the run's record folder, write back a removed .senior-dev, and --state-dir Oct 9, 2026
@ZeroPoint95
ZeroPoint95 marked this pull request as ready for review October 9, 2026 18:07
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.

1 participant