Skip to content

Give the migration's .tmp orphan a home in the operations runbook - #216

Closed
7487 wants to merge 1 commit into
Jason-Vaughan:mainfrom
7487:docs/migration-tmp-orphan-runbook
Closed

Give the migration's .tmp orphan a home in the operations runbook#216
7487 wants to merge 1 commit into
Jason-Vaughan:mainfrom
7487:docs/migration-tmp-orphan-runbook

Conversation

@7487

@7487 7487 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What

Adds a runbook entry to docs/design/operations.md for the <name>.<hex>.tmp staging file that
migrate_state_root can leave behind, plus a CHANGELOG.md entry under ### Internal.
Prose only — no behaviour change, no code touched.

Why

migrate_state_root stages each entry as <name>.<hex>.tmp beside its destination and renames it
into place. Cleanup is in a finally, so an error or Ctrl-C clears the staging entry; SIGKILL
or power loss between the copy and the rename does not, and leaves one in the operator's state
root. The docstring is explicit that this orphan is deliberately visible — an orphan is the
operator's to delete, and they cannot delete what they cannot see.

Visible only helps if there is somewhere to look it up, and there was not. The only mention was the
0.21.0/0.22.0 repair paragraph in CHANGELOG.md, which is addressed to operators upgrading across
that specific pair of releases — version-scoped history rather than a durable reference. An
operator on 0.23+ who finds usage.jsonl.<hex>.tmp next to their usage log has no reason to read a
changelog entry about versions they never ran, and grep -rn '\.tmp' docs/design/ confirmed
operations.md did not mention the file anywhere.

The new entry is symptom-shaped like its neighbours in that section and stands on its own: what the
file is, when it is left behind, that nothing reads it, and that deleting it is safe. It also says
why nothing sweeps it up later — a sweep over the destination directory is the shared-name
collision again, one directory wider — because "why isn't this cleaned up automatically" is the
next question an operator asks, and leaving it unanswered invites someone to close it as a gap.
No 0.21/0.22 framing, so the entry does not expire.

Scope is the two files the issue names. The issue's fourth acceptance box — republishing the Design
Overview Artifact to its existing URL — is yours: it is not a repository file and a fork cannot
reach that URL. Everything the Artifact would be rebuilt from is in this diff.

Test plan

make test — 791 passed, 17 skipped, ruff and mypy clean (they gate the target). Nothing here is
executable, so that is a no-regression check rather than coverage of the change; the substantive
verification was reading router.py's migrate_state_root and atomic.staging_path against the
prose, so that the entry describes what the code actually does:

  • the suffix is f"{path.name}.{uuid.uuid4().hex}.tmp" — no leading dot, hence visible;
  • staging is a sibling of the destination, so the orphan lands in the new state root;
  • _discard(staged) is in a finally, so only a signal that skips it orphans an entry;
  • the migration copies and never deletes, so the legacy original survives deleting the orphan.

Also checked the wording does not contradict the § Concurrency work #196 covers, or the
observability.md § Migrated-log integrity notice above it in the same section.

Fixes #198

🤖 Generated with Claude Code

`migrate_state_root` stages each entry as `<name>.<hex>.tmp` beside its
destination and renames it into place. Cleanup sits in a `finally`, so an error
or Ctrl-C clears the staging entry; SIGKILL or power loss between the copy and
the rename does not, and leaves one in the operator's state root. That orphan is
deliberately visible — an orphan is theirs to delete, and they cannot delete
what they cannot see — but visible only helps if there is somewhere to look it
up.

The only place that said so was the 0.21.0/0.22.0 repair paragraph in
CHANGELOG.md. That paragraph is addressed to operators upgrading across a
specific pair of releases; it is version-scoped history, not a durable
reference. An operator on 0.23+ who finds `usage.jsonl.<hex>.tmp` next to their
usage log has no reason to go reading a changelog entry about versions they
never ran, and `docs/design/operations.md` did not mention the file at all.

The new runbook entry is symptom-shaped like its neighbours and stands on its
own: what the file is, when it is left behind, that nothing reads it, and that
deleting it is safe — including why nothing sweeps it up later, since "why is
this not cleaned up automatically" is the next question an operator asks. No
0.21/0.22 framing, so it does not expire.

Descriptive only; no behaviour change.

Fixes Jason-Vaughan#198

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Jason-Vaughan

Copy link
Copy Markdown
Owner

Thanks for this — and for reading migrate_state_root rather than trusting the changelog. Your
diagnosis is exactly right: the orphan was documented only in a version-scoped repair paragraph,
which is no use to an operator arriving on 0.23+ who has never run 0.21.

Every factual claim in the entry checks out against the code — the staging format, the
destination-sibling placement, the finally, the re-run guard, and that the migration copies and
never deletes. The symptom-shaped heading matches the section's convention and ### Internal is
the right changelog tier.

The audit turned up one thing worth adding, and it is the only change to your entry: "deleting it
is safe" is true of an orphan, but an operator cannot tell an orphan from a staging file a
migration is using right now — the names are identical and every console script migrates on
startup. Deleting a live one makes that run print could not move state from … (the next startup
finishes the move, so nothing is lost). Without that clause the runbook promises an operator
something it cannot deliver, so it is carried in the reconstruction.

Per the Clean Room Reconstruction Standard in CONTRIBUTING.md we merge ideas rather than raw
bytes, so this is open as a reconstruction in
#217 with your name on it — the same way
@be-student's #137 and #144 were handled. You are credited by
@mention in the changelog entry and in the commit message. The Design Overview Artifact republish
you flagged as ours is indeed ours, and it is done; you read that boundary correctly.

Closing this one in favour of #217 — not because anything was wrong with it, but because the
standard means we cannot merge the bytes. Thank you for the contribution.

Jason-Vaughan added a commit that referenced this pull request Sep 8, 2026
Closes #198. Clean-room reconstruction of PR #216 by @7487 — both the Macro
and Micro filters cleared it, so per CONTRIBUTING.md the idea lands here and
the bytes are ours. Their diagnosis is the whole basis of this entry.

`migrate_state_root` stages each entry as `<name>.<hex>.tmp` beside its
destination and `os.replace`s it into position, so a copy killed halfway
never leaves a truncated file at the real path — which matters because the
re-run guard is "does the destination exist". Cleanup runs in a `finally`, so
an error or Ctrl-C takes the staging file with it; SIGKILL and power loss do
not. Nothing reclaims the leftover afterwards, deliberately: a name no other
process can touch is also a name no later run can find, and sweeping for the
suffix would restore the shared-name collision the unique name exists to
prevent. It stays visible because an orphan is the operator's to delete.

None of that was written anywhere an operator would look. The only mention
was the 0.21.0/0.22.0 repair paragraph in the changelog — version-scoped
history, useless to someone arriving on 0.23+ who finds the file and never
ran either release.

The reconstruction adds one thing the contribution did not have. "Deleting it
is safe" is true of an orphan and false of a staging file a migration is
using right now, and the two are indistinguishable by filename while every
console script migrates on startup. Deleting a live one makes `os.replace`
raise and the run print `could not move state from …`; nothing is lost and
the next startup finishes the move, but an operator acting on an unqualified
"safe" would have caused an alarm the runbook told them could not happen.
Verified against the failure arm in router.py rather than inferred: the
re-raise reaches the outer handler because `destination.exists()` is false.

`docs/design/operations.md` is under docs/design/, so the Design Overview
Artifact is republished in the same turn per CLAUDE.md. Its #139 entry
described the unique-name fix without ever naming the orphan that fix
deliberately creates — an accepted cost that goes unstated reads as an
oversight.

Docs only, no behaviour change. Suite 805 green, lint clean.
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.

docs: give the migration .tmp orphan a home in the operations runbook

2 participants