Skip to content

miner(claim): wire reclaimExpiredClaims to a real CLI — it has zero production callers #9686

Description

@JSONbored

⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.

Context

ClaimLedger.reclaimExpiredClaims(maxAgeMs?) in packages/loopover-miner/lib/claim-ledger.ts exists to expire claims orphaned by a crashed/killed process, and its doc comment names its intended shape:

/** Expire claims orphaned by a crashed/killed process, returning the transitioned rows (#6156). The explicit
 *  counterpart to the sweep claimIssue runs on its own, mirroring reclaimStuckItems (portfolio-queue-manager.js). */

Its named model, reclaimStuckItems, genuinely is wired: it runs automatically inside claimNextBatch (portfolio-queue-manager.ts:174) and has an operator-facing manual escape hatch, loopover-miner queue release (portfolio-queue-cli.ts:398-430).

reclaimExpiredClaims has neither. grep -rn reclaimExpiredClaims packages/ test/ src/ apps/ returns only its own declaration, its own implementation, and test/unit/miner-claim-ledger-sweep-wiring.test.ts. The claim CLI dispatches exactly three subcommands — claim, release, list (claim-ledger-cli.ts:365-367) — with no reclaim/expire path, and neither cli.ts's help text nor the docs mention one.

The operator-visible gap: the automatic sweep inside claimIssue/claimIssueWithinCap hardcodes DEFAULT_MAX_CLAIM_AGE_MS = 14 days (claim-ledger-expiry.ts:5) and only ever runs when a new claim is attempted on some repo. A claim stranded active by a SIGKILLed attempt therefore counts against that repo's maxConcurrentClaims cap (claim-ledger.ts:341-370) for up to two weeks, and there is no command an operator can run to clear it — while the exactly analogous portfolio-queue lease is both swept at 30 minutes and manually releasable.

Requirements

  • Add a reclaim subcommand to the claim CLI: loopover-miner claim reclaim [--max-age-ms <n>] [--dry-run] [--json].
  • It must call ClaimLedger.reclaimExpiredClaims(maxAgeMs), passing the parsed --max-age-ms when supplied and omitting it (so the ledger's own DEFAULT_MAX_CLAIM_AGE_MS default applies) when not.
  • --max-age-ms must accept only a finite integer at or above 0; anything else returns the subcommand's usage string through reportCliFailure.
  • --dry-run must print what would be reclaimed and return 0 without opening the claim ledger at all, matching runQueueRelease's dry-run shape (portfolio-queue-cli.ts:404-412).
  • Non-dry-run output: --json prints { "reclaimed": [...] } (the returned ClaimEntry[]); plain text prints one line per reclaimed claim plus a count, and none when nothing was reclaimed. Exit 0 in both cases — reclaiming nothing is not a failure.
  • Register reclaim in runClaimCli's dispatch alongside claim/release/list, and add the new usage line to printHelp in packages/loopover-miner/lib/cli.ts next to the other claim lines.

⚠️ Required pattern: mirror runQueueRelease in packages/loopover-miner/lib/portfolio-queue-cli.ts:396-430 — the same parse -> dry-run short-circuit -> withStore -> reportCliFailure(describeCliError(error)) structure and the same argsWantJson parse-error handling used by every other subcommand in claim-ledger-cli.ts. It does NOT satisfy this issue to call reclaimExpiredClaims from runClaimClaim/runClaimList as a side effect, to add it as a doctor check, to add an MCP tool, or to shorten DEFAULT_MAX_CLAIM_AGE_MS instead of adding the command.

Deliverables

  • packages/loopover-miner/lib/claim-ledger-cli.ts exports a parseClaimReclaimArgs and a runClaimReclaim, and runClaimCli dispatches subcommand === "reclaim" to it.
  • printHelp in packages/loopover-miner/lib/cli.ts lists loopover-miner claim reclaim [--max-age-ms <n>] [--dry-run] [--json].
  • New tests in test/unit/miner-claim-ledger-cli.test.ts cover: a reclaim that transitions an over-age active claim to expired; a reclaim that returns none when nothing is over-age; --max-age-ms honoured (a claim inside the default window but outside an explicit smaller window is reclaimed); an invalid --max-age-ms returning the usage error; and --dry-run returning 0 without opening the ledger (assert via an injected opener that is never called).
  • A test asserts runClaimCli("reclaim", ...) is reachable from the dispatcher.

All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding runClaimReclaim without registering it in runClaimCli's dispatch and in printHelp, leaving it as unreachable as reclaimExpiredClaims is today — does not resolve this issue.

Test Coverage Requirements

packages/loopover-miner/lib/**/*.ts IS inside Codecov's coverage.include in vitest.config.ts, so the 99%+ branch-counted codecov/patch gate applies exactly as for src/**. Every new branch needs both arms: --max-age-ms supplied / omitted, valid / invalid, --dry-run on / off, --json on / off, and reclaimed-nonempty / reclaimed-empty.

Expected Outcome

An operator whose attempt process was hard-killed can clear the stranded soft-claim with one command instead of waiting out the 14-day expiry window, and reclaimExpiredClaims has the real production caller its own doc comment already claims it mirrors.

Links & Resources

packages/loopover-miner/lib/claim-ledger.ts:316-320, packages/loopover-miner/lib/claim-ledger-cli.ts:360-370, packages/loopover-miner/lib/portfolio-queue-cli.ts:396-430, packages/loopover-miner/lib/claim-ledger-expiry.ts, packages/loopover-miner/lib/cli.ts:43-45.

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:featureGittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions