You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
⚠️ 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
checkSubmissionFreshness is the last gate before a fully-completed coding attempt opens a PR. Its first check is
the miner's own claim status, at packages/loopover-miner/lib/submission-freshness-check.ts:110-113:
listClaims({ repoFullName }) is forge-blind. Its backing statement carries no api_base_url predicate — packages/loopover-miner/lib/claim-ledger.ts:248-250:
constlistRepoStatement=db.prepare("SELECT * FROM miner_claims WHERE repo_full_name = ? ORDER BY id ASC",);
Since #5563 the claim ledger's uniqueness key is (api_base_url, repo_full_name, issue_number)
(packages/loopover-miner/lib/claim-ledger.ts:162-171), so the same owner/repo#N legitimately has one row per
forge host. An operator creates those rows through the CLI's own flag — packages/loopover-miner/lib/claim-ledger-cli.ts:7:
.find(...) therefore returns whichever host's row has the lowest id, not this attempt's row. The attempt's own
claim is always recorded against the github.com default — attempt-cli.ts:796-802 calls claimIssueWithinCap(repoFullName, issueNumber, note, undefined, cap), and normalizeApiBaseUrl(undefined)
resolves to DEFAULT_FORGE_CONFIG.apiBaseUrl (packages/loopover-miner/lib/claim-ledger.ts:129-133).
Concrete failure: an operator ran loopover-miner claim claim acme/widgets 42 --api-base-url https://forge.internal
and later claim release, leaving a released row with id = 1. A subsequent github.com attempt acme/widgets 42
records its own active row with id = 2. At submission time .find() returns the id = 1 row, sees status !== "active", and aborts with claim_superseded — discarding a completed create/iterate/self-review run
and its worktree work, for a claim on an unrelated host. The module's own header calls this out as the expensive
case: "Aborting here discards a fully-completed create/iterate loop's local work"
(packages/loopover-miner/lib/submission-freshness-check.ts:21-22).
The mirror-image failure is a false pass: if the other host's row happens to be active and this attempt's own
claim was released or expired, the check passes on a claim the attempt does not hold.
Every sibling that touches these rows already scopes by host, with explicit comments naming this exact hazard. sweepExpiredClaims (packages/loopover-miner/lib/claim-ledger-expiry.ts:47-51):
for(constclaimofexpired){// Echo the row's OWN apiBaseUrl back (#5563) rather than defaulting: two forge hosts can each have an// active claim on the same owner/repo#issue, and defaulting here would expire the wrong host's row.constupdated=store.expireClaim(claim.repoFullName,claim.issueNumber,claim.apiBaseUrl);
ensureManagedPrRow (packages/loopover-miner/lib/manage-poll.ts:159-173) does the same for the portfolio queue,
noting "listQueue(repoFullName) is forge-BLIND, so the existence check has to compare the host too".
countActiveRepoStatement (packages/loopover-miner/lib/claim-ledger.ts:257-262) is deliberately forge-blind, but
its comment scopes that decision to the concurrency cap — "the cap's MEANING is unchanged". The freshness check
is an identity check on one specific row, not a cap, so the same reasoning does not apply.
ClaimEntry already carries apiBaseUrl (packages/loopover-miner/lib/claim-ledger.ts:17-25); only the narrowed
seam type SubmissionFreshnessClaimLedger
(packages/loopover-miner/lib/submission-freshness-check.ts:46-48) drops it.
Requirements
checkSubmissionFreshness must select the claim row belonging to the candidate's own forge host, not the first
row for that repo across all hosts.
SubmissionFreshnessCandidate gains an optional apiBaseUrl. When omitted or nullish it must resolve to DEFAULT_FORGE_CONFIG.apiBaseUrl (packages/loopover-miner/lib/forge-config.ts:22-32), exactly matching the
claim ledger's own normalizeApiBaseUrl at packages/loopover-miner/lib/claim-ledger.ts:129-133, so every
existing single-forge caller behaves identically to today.
SubmissionFreshnessClaimLedger's row type must include apiBaseUrl: string, and the match must compare it —
a row whose apiBaseUrl differs from the candidate's resolved host must be ignored entirely (neither a pass nor
a claim_superseded abort).
When no row exists for the candidate's own host, the result must remain { fresh: false, reason: "claim_superseded" } with the same submission_freshness_abort event payload as today
(packages/loopover-miner/lib/submission-freshness-check.ts:157-169).
attempt-runner.ts's call site (packages/loopover-miner/lib/attempt-runner.ts:235-238) must pass the same
host the attempt's claim was recorded under, so the runner and the ledger cannot disagree.
Do NOT change claimIssueWithinCap's forge-blind countActiveRepoStatement — the cap's cross-forge meaning is
intentional and documented at packages/loopover-miner/lib/claim-ledger.ts:257-259.
Do NOT change the live-snapshot retry loop, the issue_closed / already_addressed / live_state_unavailable decisions, or their event payloads.
⚠️ Required pattern: mirror packages/loopover-miner/lib/claim-ledger-expiry.ts:47-51 and packages/loopover-miner/lib/manage-poll.ts:159-173 — resolve the host with the same
"omitted/blank → DEFAULT_FORGE_CONFIG.apiBaseUrl" rule the stores use, then compare it explicitly. What does
NOT satisfy this issue: (a) changing listRepoStatement in claim-ledger.ts to add an api_base_url predicate,
which silently rescopes listClaims/listActiveClaims for every other caller including the concurrency cap;
(b) filtering with .find(c => c.issueNumber === n && c.status === "active"), which papers over the wrong-host
match by picking any active row and reintroduces the false-pass case; (c) a test-only PR.
Deliverables
checkSubmissionFreshness in packages/loopover-miner/lib/submission-freshness-check.ts resolves the
candidate's apiBaseUrl (defaulting to DEFAULT_FORGE_CONFIG.apiBaseUrl) and matches on issueNumberand that host.
Given a claim ledger returning [{ repoFullName: "acme/widgets", issueNumber: 42, status: "released", apiBaseUrl: "https://forge.internal" }, { repoFullName: "acme/widgets", issueNumber: 42, status: "active", apiBaseUrl: "https://github.com/ghapi" }] (in that order) and a candidate with no apiBaseUrl, checkSubmissionFreshness proceeds to the live-snapshot fetch and returns { fresh: true } — asserted in
the existing test/unit/miner-submission-freshness-check.test.ts.
Given only [{ …, status: "active", apiBaseUrl: "https://forge.internal" }] and a candidate with no apiBaseUrl, the result is { fresh: false, reason: "claim_superseded" } and one submission_freshness_abort event is appended — asserted in the same test file.
Given [{ …, status: "active", apiBaseUrl: "https://forge.internal" }] and a candidate with apiBaseUrl: "https://forge.internal", the result is { fresh: true } — asserted in the same test file.
An end-to-end assertion that runMinerAttempt threads the attempt's host into the freshness candidate —
asserted in test/unit/miner-attempt-runner.test.ts.
A regression test named for this bug (e.g. REGRESSION: a released claim on another forge host does not abort this host's submission) that fails against the current code.
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example one that
adds apiBaseUrl to the candidate type but leaves attempt-runner.ts passing nothing, so the runner and ledger
still disagree on a non-default host — does not resolve this issue.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted. vitest.config.ts's coverage.include lists packages/loopover-miner/lib/**/*.ts, so submission-freshness-check.ts and attempt-runner.ts are measured and
gated. Every branch the change introduces needs both arms tested: the candidate apiBaseUrl supplied vs
omitted/nullish default; the row-host matches vs differs comparison; the resulting !claim vs claim.status !== "active" arms of the existing guard at packages/loopover-miner/lib/submission-freshness-check.ts:111; and, if a row normalizes a blank/absent apiBaseUrl, both the blank and non-blank arms.
Expected Outcome
A completed attempt is no longer discarded as claim_superseded because of an unrelated forge host's stale claim
row, and the freshness gate can no longer pass on a claim this attempt does not hold — the claim identity check
becomes host-scoped, matching the (api_base_url, repo_full_name, issue_number) key the ledger has used since #5563.
Links & Resources
packages/loopover-miner/lib/submission-freshness-check.ts:110-113 — the forge-blind .find
packages/loopover-miner/lib/submission-freshness-check.ts:46-48 — the narrowed ledger seam that drops apiBaseUrl
packages/loopover-miner/lib/claim-ledger.ts:162-171, :248-250, :257-262 — the composite key, the
forge-blind list statement, and the deliberately forge-blind cap
packages/loopover-miner/lib/claim-ledger-expiry.ts:47-51 — the host-echoing precedent
packages/loopover-miner/lib/manage-poll.ts:159-173 — the same precedent for the portfolio queue
packages/loopover-miner/lib/claim-ledger-cli.ts:7 — the --api-base-url flag that makes multi-host rows reachable
packages/loopover-miner/lib/attempt-cli.ts:796-802 — the attempt's own claim, recorded on the default host
Context
checkSubmissionFreshnessis the last gate before a fully-completed coding attempt opens a PR. Its first check isthe miner's own claim status, at
packages/loopover-miner/lib/submission-freshness-check.ts:110-113:listClaims({ repoFullName })is forge-blind. Its backing statement carries noapi_base_urlpredicate —packages/loopover-miner/lib/claim-ledger.ts:248-250:Since
#5563the claim ledger's uniqueness key is(api_base_url, repo_full_name, issue_number)(
packages/loopover-miner/lib/claim-ledger.ts:162-171), so the sameowner/repo#Nlegitimately has one row perforge host. An operator creates those rows through the CLI's own flag —
packages/loopover-miner/lib/claim-ledger-cli.ts:7:.find(...)therefore returns whichever host's row has the lowestid, not this attempt's row. The attempt's ownclaim is always recorded against the github.com default —
attempt-cli.ts:796-802callsclaimIssueWithinCap(repoFullName, issueNumber, note, undefined, cap), andnormalizeApiBaseUrl(undefined)resolves to
DEFAULT_FORGE_CONFIG.apiBaseUrl(packages/loopover-miner/lib/claim-ledger.ts:129-133).Concrete failure: an operator ran
loopover-miner claim claim acme/widgets 42 --api-base-url https://forge.internaland later
claim release, leaving areleasedrow withid = 1. A subsequent github.laiyagushi.comattempt acme/widgets 42records its own
activerow withid = 2. At submission time.find()returns theid = 1row, seesstatus !== "active", and aborts withclaim_superseded— discarding a completed create/iterate/self-review runand its worktree work, for a claim on an unrelated host. The module's own header calls this out as the expensive
case: "Aborting here discards a fully-completed create/iterate loop's local work"
(
packages/loopover-miner/lib/submission-freshness-check.ts:21-22).The mirror-image failure is a false pass: if the other host's row happens to be
activeand this attempt's ownclaim was released or expired, the check passes on a claim the attempt does not hold.
Every sibling that touches these rows already scopes by host, with explicit comments naming this exact hazard.
sweepExpiredClaims(packages/loopover-miner/lib/claim-ledger-expiry.ts:47-51):ensureManagedPrRow(packages/loopover-miner/lib/manage-poll.ts:159-173) does the same for the portfolio queue,noting "
listQueue(repoFullName)is forge-BLIND, so the existence check has to compare the host too".countActiveRepoStatement(packages/loopover-miner/lib/claim-ledger.ts:257-262) is deliberately forge-blind, butits comment scopes that decision to the concurrency cap — "the cap's MEANING is unchanged". The freshness check
is an identity check on one specific row, not a cap, so the same reasoning does not apply.
ClaimEntryalready carriesapiBaseUrl(packages/loopover-miner/lib/claim-ledger.ts:17-25); only the narrowedseam type
SubmissionFreshnessClaimLedger(
packages/loopover-miner/lib/submission-freshness-check.ts:46-48) drops it.Requirements
checkSubmissionFreshnessmust select the claim row belonging to the candidate's own forge host, not the firstrow for that repo across all hosts.
SubmissionFreshnessCandidategains an optionalapiBaseUrl. When omitted or nullish it must resolve toDEFAULT_FORGE_CONFIG.apiBaseUrl(packages/loopover-miner/lib/forge-config.ts:22-32), exactly matching theclaim ledger's own
normalizeApiBaseUrlatpackages/loopover-miner/lib/claim-ledger.ts:129-133, so everyexisting single-forge caller behaves identically to today.
SubmissionFreshnessClaimLedger's row type must includeapiBaseUrl: string, and the match must compare it —a row whose
apiBaseUrldiffers from the candidate's resolved host must be ignored entirely (neither a pass nora
claim_supersededabort).{ fresh: false, reason: "claim_superseded" }with the samesubmission_freshness_abortevent payload as today(
packages/loopover-miner/lib/submission-freshness-check.ts:157-169).attempt-runner.ts's call site (packages/loopover-miner/lib/attempt-runner.ts:235-238) must pass the samehost the attempt's claim was recorded under, so the runner and the ledger cannot disagree.
claimIssueWithinCap's forge-blindcountActiveRepoStatement— the cap's cross-forge meaning isintentional and documented at
packages/loopover-miner/lib/claim-ledger.ts:257-259.issue_closed/already_addressed/live_state_unavailabledecisions, or their event payloads.Deliverables
checkSubmissionFreshnessinpackages/loopover-miner/lib/submission-freshness-check.tsresolves thecandidate's
apiBaseUrl(defaulting toDEFAULT_FORGE_CONFIG.apiBaseUrl) and matches onissueNumberand that host.[{ repoFullName: "acme/widgets", issueNumber: 42, status: "released", apiBaseUrl: "https://forge.internal" }, { repoFullName: "acme/widgets", issueNumber: 42, status: "active", apiBaseUrl: "https://github.com/ghapi" }](in that order) and a candidate with noapiBaseUrl,checkSubmissionFreshnessproceeds to the live-snapshot fetch and returns{ fresh: true }— asserted inthe existing
test/unit/miner-submission-freshness-check.test.ts.[{ …, status: "active", apiBaseUrl: "https://forge.internal" }]and a candidate with noapiBaseUrl, the result is{ fresh: false, reason: "claim_superseded" }and onesubmission_freshness_abortevent is appended — asserted in the same test file.[{ …, status: "active", apiBaseUrl: "https://forge.internal" }]and a candidate withapiBaseUrl: "https://forge.internal", the result is{ fresh: true }— asserted in the same test file.runMinerAttemptthreads the attempt's host into the freshness candidate —asserted in
test/unit/miner-attempt-runner.test.ts.REGRESSION: a released claim on another forge host does not abort this host's submission) that fails against the current code.All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example one that
adds
apiBaseUrlto the candidate type but leavesattempt-runner.tspassing nothing, so the runner and ledgerstill disagree on a non-default host — does not resolve this issue.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted.
vitest.config.ts'scoverage.includelistspackages/loopover-miner/lib/**/*.ts, sosubmission-freshness-check.tsandattempt-runner.tsare measured andgated. Every branch the change introduces needs both arms tested: the candidate
apiBaseUrlsupplied vsomitted/nullish default; the row-host matches vs differs comparison; the resulting
!claimvsclaim.status !== "active"arms of the existing guard atpackages/loopover-miner/lib/submission-freshness-check.ts:111; and, if a row normalizes a blank/absentapiBaseUrl, both the blank and non-blank arms.Expected Outcome
A completed attempt is no longer discarded as
claim_supersededbecause of an unrelated forge host's stale claimrow, and the freshness gate can no longer pass on a claim this attempt does not hold — the claim identity check
becomes host-scoped, matching the
(api_base_url, repo_full_name, issue_number)key the ledger has used since#5563.Links & Resources
packages/loopover-miner/lib/submission-freshness-check.ts:110-113— the forge-blind.findpackages/loopover-miner/lib/submission-freshness-check.ts:46-48— the narrowed ledger seam that dropsapiBaseUrlpackages/loopover-miner/lib/claim-ledger.ts:162-171,:248-250,:257-262— the composite key, theforge-blind list statement, and the deliberately forge-blind cap
packages/loopover-miner/lib/claim-ledger-expiry.ts:47-51— the host-echoing precedentpackages/loopover-miner/lib/manage-poll.ts:159-173— the same precedent for the portfolio queuepackages/loopover-miner/lib/claim-ledger-cli.ts:7— the--api-base-urlflag that makes multi-host rows reachablepackages/loopover-miner/lib/attempt-cli.ts:796-802— the attempt's own claim, recorded on the default host