oficina / suite Public

Files: prevent cleanup racing acknowledged blob commits open

Focused Files readiness design: prevent cleanup from deleting concurrently acknowledged content. Status: draft for independent Claude Fable spec review. Owner /root/docs_track, subordinate to Docs activation work. User authorized suite implementation, Amadan tracking and continuous reviewed deployments. This slice has no UI, crypto, key-management, deployment-infrastructure or retention-policy change.

Source: GitHub slopboxcloud/slopbox origin/main c88ad26f9a61771ca4ff66ec27306d17f284fd97. Isolated worktree /home/paulca/github.com/slopboxcloud/slopbox/.claude/worktrees/overnight-blob-deletion, branch overnight-blob-deletion. CLAUDE.md requires worktrees, additive migrations, tests, pure-Go build, go test ./..., go vet ./..., gofmt clean, and no loss of acknowledged bytes. Docs sealing branch is separate and preserved.

Reproduction: internal/instance/blobdeletionrace_test.go contains TestBlobCleanupCannotDeleteConcurrentAcknowledgedCommit. Temporary provisioned instance, real SQLite and real blob.Dir, wrapper pauses immediately before physical Delete. Cleanup observes an orphan; a concurrent authenticated HTTP commit records a version; deletion resumes; download must still return the acknowledged exact bytes, or the commit must have refused the blob without leaving a file. All three current-main cases fail: GC with an independent SQLite handle, discard, and purge acknowledge version 1 then download404. This is deterministic, not a production incident or timing stress test. Existing sequential TestUsageAndGC, TestBlobDiscard and TestPurgeLifecycle must remain green after the fix.

Durable lifecycle:

  1. Add blobdeletions(blobid TEXT PRIMARY KEY, claimed_at INTEGER NOT NULL) through the existing additive/idempotent migrations. Retain the blobs index row until physical deletion succeeds, preserving stored-byte accounting while retry is pending. Claims are permanent until finalized; random opaque blob IDs are never reused.
  2. Claim an indexed blob with a single atomic INSERT ... SELECT ... WHERE NOT EXISTS(version_chunks reference), optionally retaining GC age qualification in the same statement. SQL errors return errors, never become permission to delete. A duplicate existing claim means pending work, not another reclaim event. No object deletion before the claim commits.
  3. Add a BEFORE INSERT trigger on versionchunks that aborts insertion of a reference to a claimed blob. Also guard UPDATE OF blobid for completeness. This makes the invariant apply to independent CLI connections and old compatible writers, which otherwise still see the retained blobs index. New handleCommit queries only indexed blobs without a claim and reuses the existing 'unknown blob in chunk list' HTTP400 response; the trigger is defense in depth. Do not expose new copy. No acknowledged commit can reference a claimed blob: SQLite serializes claim insertion and reference insertion; whichever writes first prevents the other.
  4. Centralize physical deletion/finalization in this lifecycle. GC finds age-qualified unreferenced candidates plus existing claims, requests claims then drains them; discard retains share authorization and referenced-blob409; purge retains owner/trash checks, removes authorized history, then claims only genuinely unreferenced candidates. Failed object deletion leaves the claim and blobs row for retry. Treat object-not-found as successful deletion because a prior worker may have died after deleting it; only the backend's recognized not-found error qualifies.
  5. After physical deletion, use a DB transaction to remove the indexed blob and matching claim and record one usage_events reclaim. Multiple workers can delete the same immutable object idempotently; only the transaction that removes the extant claim records bytes/count. A crash before finalization leaves retryable claimed state; a crash after finalization cannot repeat the accounting. Never convert an unexpected reference or SQL failure into deletion permission.
  6. Already-claimed blobs are unavailable for new download/commit operations. A referenced blob cannot enter claimed state. Keeping the index until finalization counts physical pending bytes in existing usage. Existing GC reclaimed counts and purge reclaimed response count only work finalized by this invocation; failures remain visible through existing error/log paths.

Scope edges and compatibility:

  • Do not change normal tombstone retention, purge authorization, share-key generations, content formats, or client-side encryption.
  • Old writer compatibility must be proven by using the pre-change SELECT and INSERT statements against the new schema: the trigger refuses a claimed blob before acknowledgement. Unclaimed uploads/commits remain compatible. Existing versions and objects are unchanged by additive schema creation.
  • A pre-fix GC/discard/purge process can still act on a stale pre-claim observation. Deployment requires replacing/stopping old cleanup-capable instance binaries and CLI jobs before considering the fix active. Rollback must retain the fixed cleanup implementation; reverting to the known-vulnerable binary reintroduces the race. No deployment or infrastructure change is proposed or authorized by this isolated test preparation.
  • Database claims handle separate processes; an in-memory mutex is insufficient.
  • Blob put's cleanup after failed index insertion is outside this race because no committed version can discover that unindexed random ID. Do not broaden into unrelated durability rewrites.

Required regression evidence after approval:

  • Existing three deterministic cases go green with a refused new commit and no file residue, or with acknowledged bytes retained.
  • Reverse interleaving: committed references before claim prevent physical deletion for GC/discard/purge.
  • Independent DB handles race claim/reference; SQL trigger blocks legacy statement path.
  • Physical deletion failure keeps pending claim, indexed bytes and zero reclaim; later successful retry finalizes exactly once.
  • Crash/reopen after claim and after successful physical delete but before DB finalization, then retry; no referenced bytes lost and no duplicate reclaim.
  • Two independent workers finishing the same claim cannot double-count.
  • Unexpected reference-query/claim error cannot call object Delete.
  • Additive migration on an old database preserves existing indexed blobs/version rows and exact downloads; normal upload/commit still succeeds.
  • Full repository build/test/vet/format gates and whole-branch Fable review precede PR/merge. No failing regression branch is deployable.

Review questions: Is the claim plus trigger invariant complete for this repository's actual SQL and store backends? Is retaining the index until finalization preferable to atomically moving it into a deletion queue? Identify any flaw in crash/idempotent accounting, rollout with an independent CLI, or source-derived interleaving. Request changes if needed; do not approve based on quota failure or assume old cleanup is safe.

Regression prepared at local Slopbox branch overnight-blob-deletion, commit e21db49 (test only; intentionally red and not deployable). TestBlobCleanupCannotDeleteConcurrentAcknowledgedCommit uses a provisioned temporary instance, real SQLite and blob.Dir, a paused Delete, and authenticated HTTP commit/download. GC uses a second SQLite handle. All three cases (GC, discard, purge) fail on every one of five repetitions: version 1 acknowledged, then blob download404. Raw output /tmp/oficina-files-race-baseline.txt. Focused design /tmp/oficina-files-deletion-spec.txt is posted above; Fable review queued with root, not invoked while quota/start gate pending. Suite task33 tracks this under plan/overnight-readiness because Slopbox lives on GitHub. No production mutation or fix implementation; Docs wiring retains priority when its reviewed activation starts.

Baseline gates: pure-Go build and go vet ./... pass. Existing tests (excluding only the new known-red regression) pass all packages except internal/agent TestBundleLayout, which requires macOS iconutil unavailable on this Linux host. Full repository gate is not green; need a real macOS run before claiming it. No fake tool or unrelated test edit.

A second intentional-red regression now proves discard fails open on reference-query errors: renaming version_chunks in the temporary fixture makes the reference query fail, but discard returns204 and physically deletes the blob. TestBlobDiscardDoesNotTreatReferenceQueryFailureAsOrphan requires500 and exact bytes retained. This is local synthetic fault injection only. It supports the draft design requirement that SQL errors never authorize deletion.

Independent Files spec review, actual claude-fable-5-1, session d57b5791-7170-4749-aed0-80e61bc4cd47. REQUEST CHANGES; amendment addressing all five requirements and smaller corrections is now under review. No fix code implemented.

REQUEST CHANGES (design is sound; five amendments needed before implementation).

Reviewer: claude-fable-5-1. Scope: spec review of /tmp/oficina-files-deletion-spec.txt against origin/main c88ad26 plus the regression tests at c161c80. Read-only. Not a whole-branch review, not deploy approval, and the Mac gate (TestBundleLayout) is still outstanding.

What holds up

  • Claim + trigger invariant is complete for this repo. The only writer of version_chunks is handleCommit (internal/instance/files.go:211); the only in-scope deleters are gc.go:72, files.go:114, purge.go:86. SQLite WAL admits one writer and a write transaction must sit on the latest snapshot, so a committed claim cannot be missed by a later reference insert and vice versa. A referenced blob cannot enter claimed state because the claim's NOT EXISTS runs inside the same serialized write.
  • Retaining the blobs row is the right call, not moving it to a queue. Commit's share authorization, handleBlobGet, handleUsage and SampleDay all read blobs; keeping the row keeps bytes-held honest until the object is gone. The cost is that old writers still see the row, which is exactly what the trigger closes. Note the trigger is the only guard for old binaries and independent handles, not merely defense in depth. It must be tested as such.
  • Exactly-once accounting via a finalization transaction gated on the claim delete is correct for concurrent workers and for crash-after-delete retry.
  • The fault test (renamed version_chunks) is satisfied by the claim statement erroring, since the claim's subquery names that table.

Required amendments

  1. Never hold a DB transaction across store.Delete. openDB sets SetMaxOpenConns(1) (db.go:138). The in-process GC loop, discard and purge share the server's single connection. A transaction open during an S3 DELETE stalls every request. Spec must state: claim commits, connection released, physical delete, then a fresh finalization transaction.
  1. Drain must not halt on the first failure. Current GC returns on the first delete error (gc.go:73). Claims are permanent, so one blob whose Delete fails permanently (for example blob.Dir.path rejecting an id, blob.go:45) would starve every later claim forever, hourly. Drain must log per-blob failures through the existing paths, continue, and return the first error at the end.
  1. Not-found mapping is backend-specific and the spec overstates it. Dir.Delete returns a *fs.PathError; qualify only errors.Is(err, fs.ErrNotExist). The S3 client already swallows 404 inside deleteObject (s3client.go:105) and S3 returns 204 for absent keys regardless, so the S3 backend can never surface not-found; its nil return is the success signal. State both explicitly so the implementer does not add a string match.
  1. Tolerate a claim whose blobs row is gone. During the mixed-version window, an old binary's discard or GC can delete the row and object after a new binary claimed the blob. Drain must LEFT JOIN, treat the missing row as size unknown, delete the object (not-found is success), remove the claim, and record no reclaim bytes because the old binary already metered. An inner join or a Scan erroring on ErrNoRows would leave that claim pending forever.
  1. Pin the refusal statuses. The new commit lookup excluding claimed blobs yields the existing 400 only when the claim committed before the commit's transaction began, which is the deterministic case the test exercises. Two other refusals exist and are acceptable: the trigger firing inside the commit transaction returns the existing 500 "chunks" (this is what the old-writer compatibility test will see, not 400), and a cross-process claim landing between the commit's read snapshot and its first write returns SQLITEBUSYSNAPSHOT, hence 500 "create" or "version" (pre-existing for any concurrent CLI write; busy_timeout does not cover it). All three roll back with no files residue. The spec should say 400 is the tested path and the 500s are accepted refusals, and the old-writer test must expect 500.

Smaller corrections

  • Discard needs an explicit three-way branch: claim inserted → drain, 204; zero rows and a claim exists → drain, 204; zero rows and no claim → existing 409. The invariant makes the second check sound because referenced and claimed are mutually exclusive.
  • Finalization must check the reclaim insert's error. Today gc.go:78 and meter ignore it. Inside the finalization transaction a failed event insert must roll back the row deletes, or bytes vanish from accounting.
  • Migration ordering: the CREATE TRIGGER IF NOT EXISTS entry must follow the blob_deletions table in migrations. RAISE(ABORT) rolls back the statement only; the handler's deferred Rollback discards the transaction, which is the intended behaviour.
  • handleBlobGet needs the same not-claimed predicate if item 6 ("unavailable for download") is to be true; reuse "no such blob".
  • GC claim statement: INSERT OR IGNORE INTO blob_deletions ... SELECT ... WHERE created_at <= ? AND NOT EXISTS (...) gives claim plus age qualification in one write; OR IGNORE only masks the primary-key conflict, which is the intended "already pending" case. Existing claims must drain regardless of age.
  • Startup: GCLoop sleeps a full interval before its first pass (gc.go:23), so a claim left by a crashed purge waits up to an hour. Acceptable, but say so rather than implying immediate recovery.

Rollout check

Three cleanup-capable code paths exist, all in one binary: serve's hourly GCLoop (main.go:155), the slopbox gc CLI run by hand on the box (deploy/README.md:114), and the discard and purge handlers. Restarting slopbox@<slug> replaces the first and third. The residual is any slopbox gc run from an old binary path. The spec's statement that the fix is inactive until old cleanup-capable processes are gone is correct, and the mixed window also produces the orphaned-claim case in amendment 4. Rollback to the old binary reintroduces the race; the spec says so and that should stand.

Nothing here touches keys, trust, UI or retention. With amendments 1 through 5 written into the spec, I would approve the design for implementation. The regression evidence list in the spec is the right set; add the orphaned-claim drain case and the 500-refusal expectation for the old-writer test.

Independent amended spec APPROVE, actual claude-fable-5-1, session d57b5791-7170-4749-aed0-80e61bc4cd47. Fix implementation not started; test-only branch c161c80 remains intentionally red, Mac gate outstanding.

APPROVE (spec scope only).

Reviewer: claude-fable-5-1. Scope unchanged: design review against origin/main c88ad26 and the c161c80 regression tests, read-only. Not implementation, branch, or deploy approval. The Mac gate remains outstanding and the branch is still intentionally red.

All five required amendments and the smaller corrections are addressed as I asked. Specifically:

  • Item 1 removes the single-connection stall risk.
  • Item 2 removes claim starvation by a poison object.
  • Items 3 and 4 make not-found handling backend-exact and make the mixed-window orphaned claim drainable without fabricated accounting. The safety argument holds: a claim cannot coexist with a reference, so deleting the object for a claim with no index row cannot lose acknowledged bytes.
  • Item 5 pins the three refusal statuses correctly, including the old-writer test expecting the existing 500.
  • Items 6 through 8 close the discard ambiguity under INSERT OR IGNORE, gate reclaim on the claim delete inside a fully checked transaction, and order the migration correctly.
  • Item 9 keeps the rollout rule honest.

No remaining blockers. Two non-blocking notes for the implementer:

  • Per-blob SQL failures inside finalization should follow the same continue-after-failure rule as object deletion in item 2, so a transient BUSY on one finalize does not abort the batch.
  • The existing sequential expectations (GC count 1 and 500 bytes in TestUsageAndGC, discard 204 then 404, purge reclaimed count) hold only because drain and finalize run within the same invocation. Keep that synchronous.

Whole-branch review of the actual implementation, the full gate set, and the Mac run are still required before merge.

Approved amendment: Amending the Files deletion-claim spec in response to your review. These requirements supersede any ambiguous wording in the first draft; no fix implementation has begun.

  1. The claim transaction commits and releases its SQL connection before store.Delete. Physical deletion never holds a DB transaction. Finalization starts a new short transaction after deletion completes.
  2. A drain attempts every selected claim, logs per-blob failures through existing paths, continues after each failure, and returns the first error only after the batch. One failing object cannot starve later claims. Existing claims drain regardless of GC age. Current hourly startup schedule is unchanged; crash-left claims may wait until the next GC pass.
  3. Only errors.Is(err, fs.ErrNotExist) is treated as successful missing-object deletion for the directory backend. S3 already maps404 to success and normally returns204 for absent objects; its nil is success. No string matching.
  4. Drain LEFT JOINs claims to blobs and tolerates a missing index row. It still deletes/observes-missing the object, then removes the claim. Unknown size produces no reclaim bytes or fabricated accounting event; this handles an old cleanup process that already deleted/indexed/metered during replacement. Test this orphaned-claim case explicitly. This tolerance does not make old cleanup safe or relax the rollout rule.
  5. Pin HTTP semantics: deterministic pre-existing-claim new commit lookup returns existing400 unknown-blob; a legacy writer blocked by the mandatory SQL trigger returns existing500 chunks; cross-process BUSY_SNAPSHOT can return existing500 create/version. All refuse acknowledgement and roll back without file/version residue. The old-writer compatibility test expects500, not400. New lookup excludes claims; trigger is essential to protect old writers/independent handles.
  6. Discard uses explicit three-way handling: new claim -> drain204; existing claim -> drain204; no claim inserted/present because referenced -> existing409. SQL errors return the existing server-error path, never authorization to delete. Blob GET excludes claimed ids and reuses existing404 no-such-blob.
  7. Finalization checks every SQL error including usage-event insert; failure rolls back index/claim deletions. Only the finalizer that removes the existing claim records reclaim, exactly once. Physical deletion can already have succeeded; claim remains for idempotent retry if finalization fails.
  8. Migration creates claim table before INSERT/UPDATE reference triggers. RAISE(ABORT) plus handler deferred rollback discards the entire attempted commit. Claim uses one INSERT OR IGNORE ... SELECT with age (GC only) and NOT EXISTS reference predicates. Existing claims are not age-filtered when drained.
  9. Replace/stop every cleanup-capable old server and any old CLI GC process before claiming the fix active. A pre-fix rollback reintroduces the known race and is not an acceptable fixed-release rollback target. No deployment infrastructure changes or production execution here.

Please confirm APPROVE or identify remaining blockers, <=300words. Actual reviewer identity and bounded spec scope as before. Do not implement or deploy.

Implementation pushed to GitHub slopboxcloud/slopbox branch overnight-blob-deletion atb56067c. Original race/fail-open regressions green; focused race-detector tests green; eight durability mutations red/restored. Actual c88ad26 old server compatibility rehearsal returns500 chunks for a claimed blob and preserves object/claim with no partial file/reference. Build/vet/format pass; full tests only fail pre-existing Mac iconutil packaging gate. Complete-diff Fable review requested from root; no merge/deploy. Evidence paths /tmp/oficina-files-deletion-mutations.txt, /tmp/oficina-files-legacy-writer-rehearsal.txt, /tmp/oficina-files-deletion-full-gate.txt.

Fable whole-branch review of b56067c: APPROVE (code), actual claude-fable-5-1, session abea0e1e-209c-402b-90ae-0353937b38f4. Atomic claims/triggers, independent SQLite writers, crash recovery, once-only finalization and owner/tombstone boundaries reviewed. Two nonblocking test gaps now strengthened locally: require concurrent workers themselves to finalize once and perform commit after legacy-schema migration. Focused race checks passed five repeats. Genuine macOS full gate still required. Preparing reviewed first-party GitHub Actions workflow on existing private slopboxcloud/slopbox, 15-minute bound; no production changes. Rollout must replace every cleanup-capable server/CLI and preserve configured blob backend. Old binary rollback reintroduces original race.

Fable followup APPROVE test changes and macOS workflow, actual claude-fable-5-1, session abea0e1e-209c-402b-90ae-0353937b38f4. Head 2e6b7c13ad9104279e3e9f643122988db25dd6ee pushed to existing private slopboxcloud/slopbox branch overnight-blob-deletion. Genuine macOS full gate queued: https://github.com/slopboxcloud/slopbox/actions/runs/34161050515 ; job macos-full-gate, macos-latest, 15-minute timeout, read-only token, SHA-pinned first-party actions, no secrets or production changes. Complete instance race suite passes locally. Merge/deploy remains gated on actual full Mac success.

Genuine macOS full gate PASSED at exact head 2e6b7c13ad9104279e3e9f643122988db25dd6ee. https://github.com/slopboxcloud/slopbox/actions/runs/34161050515 job 101862808375, macOS 26.6.2, Go 1.25.0 darwin/arm64, /usr/bin/iconutil present. Formatting, pure-Go build, vet and ALL tests passed (agent package 11.989s, instance 6.253s). Local full instance race suite also passed. Fable code and followup test/workflow approvals complete, actual claude-fable-5-1. Candidate handed to root for PR/release coordination; no production changes by this agent.

Root opened draft integration PR https://github.com/slopboxcloud/slopbox/pull/19 on the existing private repository, reusing the existing commit title. Candidate 2e6b7c1 retains the recorded Fable approval and genuine macOS full-gate evidence. No merge or deployment yet.

Fresh fetch confirms main c88ad26. Compared with observed production 2d24905, the candidate contains 49 changed files including inherited provisioning, consolidated billing and account-switching releases. New deletion-code approval does not prove that full production release difference. Root will review inherited release provenance and obtain complete release review/canary evidence before promotion. The existing deploy script restarts all instance units and router; backend environment and every old cleanup writer must be accounted for, not only a socket version check.

Read-only Files release revalidation, 2026-09-07 21:30 UTC. No service/data/config writes, no deployment, no Fable call. Existing SSH workflow is deploy/deploy.sh using ~/.ssh/slopbox to ec2-user, DNS resolves dev/prod; source-owner docs-carlos-deployment-reality4eb85bf is already merged as61e2cb9. Carlos migration is not implemented.

Actual cleanup writers: dev slopbox.service PID243143 (/data/instance) and slopbox@8oe PID256960 both report f9edec9; production slopbox@8oe PID294384, @a03 PID1903437 and @cnz PID328212 report2d24905. Production legacy slopbox.service PID237939 remains active with the same executable SHA256 as the three2d24905 processes, but its /data/run/legacy.sock pathname is missing while /proc/net/unix still lists the bound socket. Its version is therefore inferred by exact binary identity, not an HTTP response. All six serve processes run the source-defined hourly GC as well as cleanup HTTP handlers. No separate gc command was running and no Slopbox GC schedule was found in inspected systemd timers/unit files or cron files; this is an inventory snapshot, not proof against future manual commands.

All six server process bucket/prefix/region fingerprints match their shared /data/slopbox.env settings. Bare SSH ec2-user and sudo CLI environments have all three unset. GCDir calls Open, which selects local blobs when bucket is unset; it does not load the service EnvironmentFile itself. The README bare gc command therefore lacks required S3 configuration. No cleanup command was executed. A release must preserve verified backend settings for any CLI cleanup and replace every old serve/cleanup process. Existing deploy.sh restarts wildcard instances then legacy/router, but masks some restart errors and verifies only existing sockets: missing production legacy socket means socket-only success is insufficient evidence of replacement.

Candidate2e6b7c1 vs prod2d24905:49 changed files, +3258/-356; inherited merged PR5 (Keymail trials/billing),9/8 (multiple accounts),10 (sync tombstones),12 (last-used sign-in),15 (deployment prose),16 (Mac EOF). Devf9edec9 to candidate:12 files. Inspected PR5/9/12 formal review arrays are empty; bodies and linkedPR8 contain author test/browser-drive evidence, including historical Mac-gate exceptions. Our actual full Mac gate is green34161050515, but Fable cleanup approval coveredc88ad26→candidate, not the full production delta. Private draftPR19 remains unmerged.

Redacted detailed process/service/CLI/version evidence and priorPR metadata: /tmp/oficina-files-release-audit/ (directory mode0700). Root has exact process list and limitations.

Deployment-verifier draft is isolated on fix/deploy-server-verification atop approved deletion candidate2e6b7c1; original candidate and shared checkout unchanged. Reproduced original shell false success with deterministic fake commands: both restart calls fail37, existing socket reports expectedversion, old deploy exits0 and prints success; fixed wrapper exits37 before success. Remote draft inventories active legacy/template units and /proc serve/gc processes before installation, refuses unaccounted cleanup writers, restarts explicit inventoried units plus router/Litestream without masking, and verifies each replacement PID/start identity, mapped executable hash+version, original backend/data/socket settings and declared endpoint. Missing legacy socket never removes it from inventory. Fake process tests cover surviving legacy, unmanaged GC/server, vanished units, backend drift and revision mismatch. Existing selectors and operational messages retained; no new productcopy/docs prose. No actual SSH/service/deploy action. Root can review draft; Fable queued after reset. Evidence /tmp/oficina-files-verifier-review/original-false-success.log.

Verifier draft pushed427362a75f9ffc5d937fbd6e23205161d167f6bc to private slopboxcloud/slopbox fix/deploy-server-verification. Four files only: existing deployment shell delegates remote restart/verification to a stdlib Python helper over SSH stdin; deterministic Python fake process/command tests are wired into go test ./deploy. Python3 was already present in audited server tooling; no server package/config change. Tests cover all active legacy/template writers, actual /proc parser, executable hash and process version, declared socket including missing legacy, environment/argument preservation, router/Litestream replacement, unmanaged/new cleanup jobs, and exact shell failure propagation with existing selectors. Four deliberate verifier mutations went red; restored. Build/vet pass; full Linux suite hits only known iconutil macOS test. Existing macOS workflow dispatched for exact branch; status pending. Fable review not called before reset. No merge/deploy or real service/SSH mutation. All evidence /tmp/oficina-files-verifier-review/.

Genuine full macOS gate PASSED at exact verifier draft427362a75f9ffc5d937fbd6e23205161d167f6bc. Workflow https://github.com/slopboxcloud/slopbox/actions/runs/34164741913 job101873454456 completed success; formatting, pureGo build, vet and ALL tests including deployment fake commands/processes and iconutil packaging passed. Terminal JSON saved /tmp/oficina-files-verifier-review/macos-run.json. Draft remains unapproved pending independent review; Fable prompt /tmp/oficina-files-verifier-review/prompt.txt queued until root allocation after reset. No actual deployment, SSH command or service mutation performed; original deletion candidate2e6b7c1 and root shared checkout remain untouched.

Files verifier follow-up pushed as 8c938f7171e33e269f09f6c660d3543260614277 on fix/deploy-server-verification, isolated from the deletion candidate. Both independent review blockers are corrected: executable-hash inventory includes known renamed and deleted mapped binaries, preserving preinstall hashes across restart; repeated flags now match actual Go FlagSet behavior. Unix version checks require SO_PEERCRED peer PID on the same HTTP connection. TCP-only configurations fail before install. Real Linux child/unlink and Unix listener tests pass; removing peer PID, hash inclusion, old hash carry, or last-flag handling produces failing tests. Build and vet pass; full Linux tests fail only the inherited macOS iconutil fixture. Fresh full macOS gate is running for this exact head: https://github.com/slopboxcloud/slopbox/actions/runs/34165762939. Review prompt and evidence are in /tmp/oficina-files-verifier-review. Explicit scope limit: renamed arbitrary older executables whose hashes do not appear in the observed preinstall/current/candidate set are not universally discoverable. No services, backend settings, data or deployments changed. Fable review remains pending quota allocation; parent retains release ownership.

The full macOS gate completed successfully for exact verifier head 8c938f7171e33e269f09f6c660d3543260614277: https://github.com/slopboxcloud/slopbox/actions/runs/34165762939. Linux-specific peer credential and deleted mapped executable tests also passed locally. Working tree is clean. Fable review is still pending; this is not deployment approval.

Read-only rollout scope audit at 22:19–22:20 UTC: dev catalogs have distinct database inodes and zero blob-ID overlap (legacy 0 blobs, 8oe 11); production catalogs also have distinct inodes and zero pairwise overlap (legacy 4, 8oe 2, a03 0, cnz 2). All indexed blobs in these snapshots are referenced; no reference lacks its local index row. Queries used mode=ro/query_only, existing WAL and SHM, and held read transactions; no immutable mode, copies, raw IDs, content, credentials or remote mutations. These are separate live snapshots, not an atomic cross-catalog or future-state guarantee. Evidence is /tmp/oficina-files-release-audit/dev-catalogs.json and prod-mapped-catalogs.json.

A new concrete release blocker emerged: production legacy PID237939 still has the database, WAL and SHM open under /data/reset-20260730-181135/instance, while its configured /data/instance directory is absent. Restart with unchanged arguments would create a fresh instance database rather than reopen the current catalog. FD metadata is in prod-legacy-fds.json. Root has been informed; recommend refusing this path/inode mismatch before installation and resolving legacy ownership separately. No frozen branch edits or deployments occurred.

Shared bucket/prefix alone does not establish a mixed-version race between disjoint catalogs: GC selects local indexed IDs and uploads mint random IDs. Exact current schema and old GC SQL reproduce loss if old/new processes share one database; copied overlapping catalogs can lose data even with both binaries upgraded. Neither condition appeared in the audited current catalogs. Thus a global stop barrier is conservative but does not resolve the observed moved-database restart issue or copied-catalog policy by itself.

The database identity preflight is implemented on separate fix/deploy-db-identity at 6ac731970e49e35cc76a17c0d748b8b78161dc44, atop unchanged frozen verifier 8c938f7. Every server must have an existing configured instance.db whose device/inode matches its open SQLite DB descriptors before install; duplicate pool descriptors deduplicate. The database identity must remain unchanged after restart. Missing/moved catalogs, newly created replacements, ambiguous descriptors and read errors fail closed. Tests model the actual moved production catalog and assert failure before any install/restart; normal paths, relative paths, symlinks and environment fallback pass. Preflight and postrestart mutations fail meaningfully. Build/vet and Linux-specific checks pass, with only inherited macOS iconutil failure in the full Linux suite. Exact-head full macOS gate: https://github.com/slopboxcloud/slopbox/actions/runs/34166519903 (running). Independent review prompt: /tmp/oficina-files-db-identity-review/prompt.txt. No live changes, deployment, catalog repointing or recreation; root retains the legacy ownership decision.

Final DB-identity follow-up is c2d23b9476457962a2bc816fdfa6d7d8668147a9 on fix/deploy-db-identity. Added a legitimate file-symlink case as well as directory symlinks, resolving through the actual open inode. Exact-head full macOS gate passed: https://github.com/slopboxcloud/slopbox/actions/runs/34166618200. Final Linux deployment tests, including Linux-only verifier checks, passed; full Linux suite retains only its known macOS iconutil failure. Preinstall and postrestart mutations reran against this final head and failed meaningfully, then restored. Worktree clean. Fable review is pending; frozen 8c938f7 and live services remain untouched. Final prompt, diff and gate evidence: /tmp/oficina-files-db-identity-review.

Root updated serial Fable full-production-release review to finalc2d23b9476457962a2bc816fdfa6d7d8668147a9, complete delta against actual production2d24905. Includes initial blob deletion, mapped writer/flag/peer verification and configured/open DB identity preflight. Original branches preserved. Exact macOSCI34166618200 passed; Linux-specific tests/mutations passed, knownMacfixture fails only onLinux. Queue checks exactcleanHEAD at01:51start after other reviews. No fullreleaseapproval exists yet. Opened private draftPR20 https://github.com/slopboxcloud/slopbox/pull/20 against earlier blobdeletionbranch, preservingPR19. Existing commit title reused, bodyempty; no new unreviewed publicdocs/copy.

Production deployment remains blocked by real legacy topology: configured /data/instance absent, current PID237939 uses preserved /data/reset-20260730-181135/instance/instance.db. New preflight refuses BEFORE install/restart; it does not repoint, recreate, archive or stop anything. Read-only per-host catalog audit found distinctinodes/zerooverlap; this is a snapshot with statedlimits, not authority to discard the four referenced legacyblobs. Root will resolve intended legacy service ownership separately. No server/service/data mutations occurred.

Final live preflight evidence: exact candidate c2d23b9476457962a2bc816fdfa6d7d8668147a9 helper was loaded through SSH stdin under a non-main namespace and only inventory(System()) was called. Dev passed with both managed database identities. Production refused with FileNotFoundError specifically for the absent configured legacy instance.db. Harness-disabled deploy/install/restart/endpoint/version methods were never called (count 0); no SQL opens, remote files, services, config, backend values, raw IDs or content were involved. Existing DNS-resolved IP known-host checks remained strict. Helper SHA256 is 1380ff871652a7d0f2106d8c676b34590193f4ad21300e7d7a4ee19d7fbae086. Sanitized local receipts: /tmp/oficina-files-db-identity-review/dev-live-preflight.json and prod-live-preflight.json. This validates the refusal against the actual fleet; production ownership still needs resolution before deployment.