Skip to content

fix(miner): worktree-allocator.ts's worktree_slots (has repo_full_name) is absent from purge-cli.ts's right-to-be-forgotten sweep #8320

Description

@JSONbored

Context

packages/loopover-miner/lib/worktree-allocator.ts's worktree_slots table has a repo_full_name column, populated whenever a slot is actively allocated to an attempt against a repo:

CREATE TABLE IF NOT EXISTS worktree_slots (
  slot_index INTEGER PRIMARY KEY,
  worktree_path TEXT NOT NULL UNIQUE,
  attempt_id TEXT UNIQUE,
  repo_full_name TEXT,
  status TEXT NOT NULL CHECK (status IN ('free', 'active')),
  ...
)

packages/loopover-miner/lib/purge-cli.ts's right-to-be-forgotten sweep (#5564) is meant to cover every repo-scoped local store — its REAL_PURGE_TARGETS list currently has 12 entries (claim-ledger, event-ledger, governor-ledger, prediction-ledger, portfolio-queue, run-state, contribution-profile-cache, governor-state, policy-verdict-cache, ranked-candidates, replay-snapshot, deny-hook-synthesis), and store-maintenance.ts defines a matching *_PURGE_SPEC for each. worktree-allocator.ts is in neither — there is no WORKTREE_ALLOCATOR_PURGE_SPEC and worktree-allocator.ts is absent from REAL_PURGE_TARGETS, despite worktree_slots carrying the exact same repo-identifying column every other covered store does. This is the same recurring gap class already fixed three times (#7091, #6599, #8009).

worktree_slots is NOT a plain append-only ledger, though — it's a fixed pool of maxConcurrency pre-allocated slot rows (slot_index is the primary key and every slot 0..maxConcurrency-1 always exists). The generic purgeStoreByRepo helper every other spec uses does a hard DELETE FROM table WHERE repoColumn = ?, which is the wrong shape here: deleting a slot row would shrink the pool below maxConcurrency and break ensureSlots/selectFreeSlot's invariant that every slot index exists. release() and reclaimOrphanedAllocations() already blank repo_full_name (and attempt_id/owner_pid/owner_host/allocated_at) to NULL on every path that frees a slot, so a free slot never carries a real repo name in practice; only a currently active slot (a live, in-flight attempt) can ever match a purge target.

Requirements

⚠️ Required pattern. Follow governor-state.ts's own precedent for a store needing custom (non-generic-purgeStoreByRepo) purge logic: add a purgeByRepo method directly on the WorktreeAllocator object returned by openWorktreeAllocator, using a hand-written UPDATE (mirroring release()'s/reclaimOrphanedAllocations()'s own SET status = 'free', attempt_id = NULL, repo_full_name = NULL, owner_pid = NULL, owner_host = NULL, allocated_at = NULL statement) rather than the generic LedgerPurgeSpec/purgeStoreByRepo helper, which would incorrectly DELETE a fixed pool slot.

  • Add purgeByRepo(repoFullName: string): number to the WorktreeAllocator type and openWorktreeAllocator's implementation.
  • Never delete a row. Never touch a row with status = 'active' — an active slot's repo_full_name reflects a live, currently-running attempt's real worktree checkout on disk; force-clearing it while status = 'active' would desync the allocator from that live checkout (the row would read "free" while the on-disk worktree and any in-flight process still exist). purgeByRepo against a repo with a currently-active attempt must leave that row untouched and must not count it.
  • Only clear (UPDATE ... SET repo_full_name = NULL, attempt_id = NULL, owner_pid = NULL, owner_host = NULL, allocated_at = NULL) a row where status = 'free' AND repo_full_name = ? — a defensive backstop for any row that predates this fix or was left stale by an unexpected crash path, even though every normal release/reclaim path already blanks these fields on free. State explicitly in code comments that this purge is expected to affect 0 rows in the overwhelming majority of real calls, by design.
  • Register the store in purge-cli.ts's REAL_PURGE_TARGETS ({ name: "worktree-allocator", optionKey: ..., opener: openWorktreeAllocator, resolveDbPath: resolveWorktreeAllocatorDbPath } — no spec/specs field, since this store's purge logic lives on the store object itself rather than a generic spec, exactly mirroring how governor-state's entry already carries specs instead of a single spec for its own non-uniform case).
  • --dry-run's read-only row-count path (purge-cli.ts's dry-run counting logic) must count only status = 'free' AND repo_full_name = ? rows for this store, matching the real purge's own match condition exactly — an active-slot row must never be counted as purgeable in dry-run either.

Deliverables

  • WorktreeAllocator.purgeByRepo implemented per the rules above.
  • worktree-allocator registered in purge-cli.ts's REAL_PURGE_TARGETS.
  • --dry-run row-count path covers worktree-allocator consistently with the real purge.

Test Coverage Requirements

packages/loopover-miner/** is not Codecov-gated, but npm run test:ci must stay green with full branch coverage in test/unit/miner-worktree-allocator.test.ts and test/unit/miner-purge-cli.test.ts: cover (1) a free slot directly seeded with a stale non-null repo_full_name (bypassing the normal acquire/release path, since normal operation never leaves one) — purged and counted; (2) an active slot for the target repo — left completely untouched, not counted; (3) no matching rows at all — returns 0; (4) --dry-run reports the same count the real purge would remove, for both the free-stale-row and active-row cases.

Expected Outcome

loopover-miner purge --repo <owner/repo> covers worktree-allocator's worktree_slots table like every other repo-scoped store, without risking corruption of a live, in-flight attempt's worktree allocation.

Links & Resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions