Skip to content

id(fileops): archive tags collide within one second — a second replace of the same name silently overwrites the first replace's archive #1612

Closed

Description

@dezren39

Problem

FileOps::rename and FileOps::copy archive a file they replace under the name <name>.archive.<unix-ts>, where the timestamp has one-second resolution and the archive write is an unconditional tag set. Two replaces of the same destination within the same second produce the same archive name, so the second archive silently overwrites the first — the first replaced version loses its only archive reference (and, once its tag is gone, its protection from blob garbage collection). The web save path has the identical pattern in archive_original_tag.

Evidence (verified on main 63ba170)

  • pkgs/id/src/fileops.rs:153-162 — archive_existing: let archive = format!("{name}.archive.{ts}"); store.tags().set(&archive, existing.hash) — no existence check, no uniquifier; set replaces an existing tag (the same replace semantics put/rename rely on for the destination name itself).
  • pkgs/id/src/fileops.rs:238 (rename) and :319 (copy) — let ts = now_unix(); taken once per call; pkgs/id/src/tags.rs:1735-1740 — now_unix() is duration_since(UNIX_EPOCH).as_secs(), i.e. second resolution.
  • pkgs/id/src/fileops.rs:248 — the rename-original archive uses the same {from}.archive.{ts} shape.
  • pkgs/id/src/web/routes.rs:2170-2176 — sibling: archive_original_tag builds format!("{name}.archive.{timestamp}") with as_secs() and store.tags().set(...) — the web document-save path, where successive saves in one second are routine.
  • pkgs/id/src/fileops.rs:388-410 — delete enumerates archives by exactly this <name>.archive.<digits> pattern, so an overwritten archive is not merely mislabelled: from the store's point of view it never existed.

Verification: verbatim-logic probe crate run ON THE LAPTOP (probe ~/git/fleet/probe-2026-10-28-laneB-archive there; fileops.rs sha256 d1e49762 and routes.rs sha256 2667e857 identical in the sandbox clone at main 63ba170 and the laptop clone; probe models tags().set with its documented replace semantics):

BUG arch1=Some("b.md.archive.1730000000") arch2=Some("b.md.archive.1730000000")
BUG archive_tag_value=Some("HASH_A") (V1 archive survives? false)
BUG archive_count=1 tags={"b.md.archive.1730000000": "HASH_A", "b.md": "HASH_C"}
CTRL archive_count=2 v1_kept=true a_kept=true

Two replaces of b.md in the same second (V1 → A → C) leave one archive, pointing at A; V1's archive is gone. The same two replaces one second apart (control) leave two archives and both versions survive.

Why it matters

The archive is the safety net the module doc promises: "rename and copy archive a file they replace" (fileops.rs:14-18). Scripted/bulk renames, REPL loops, test suites, and rapid web saves routinely replace the same name twice in one second — exactly the bursts where an operator most needs the earlier version back. The loss is silent: both operations report success, each returns an archived_replaced name, and the two names are identical strings, so even the archive.replace metadata tag cannot reveal that one archive replaced the other. Related but distinct from #1549 (web image-upload naming collision) and #1605 (mcpx exec artifact overwrite): this is the blob-tag archive namespace in FileOps plus its routes.rs sibling.

Acceptance criteria

  • Archiving a replaced file never overwrites an existing archive: if <name>.archive.<ts> is taken, a distinct archive name is chosen (e.g. a suffix/counter) while staying inside the <name>.archive.<...> enumeration shape delete and classify_tag understand — or the enumeration/classification is updated in the same change.
  • The same fix (or a shared helper) covers archive_original_tag in web/routes.rs, so the save path cannot collide either.
  • Unit/integration tests: two replaces of the same destination with the same timestamp produce two distinct archives holding the two distinct hashes; the returned archived_replaced/archived_original names are the names actually written.
  • Existing rename/copy/delete archive tests still pass; single-replace behaviour and archive naming for the common case are unchanged.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane B, new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b cron run, 2026-10-28 01:50 CDT
    • Found via: filed this run
    • Task: Triage the same-second archive-tag collision in FileOps rename/copy (and the routes.rs archive_original_tag sibling): archiving a replaced file must never overwrite an existing archive.
    • Expectation: effort/s — done looks like: distinct archive names for same-timestamp replaces in both fileops.rs and routes.rs, with tests pinning two same-second replaces keeping both versions.
    • Links: queue packet packet-1612.json (repo-health-code-pilot, triage)
  4. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  5. 75 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    on Oct 28, 2026
  7. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:donetask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions