⚠️ 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
packages/loopover-miner/lib/loop-cli.ts's runLoop primes a cursor variable once at startup
(let sinceSeq = eventLedger.latestSeq();, around line 342) and carries it forward across every
loop cycle. Each cycle, it calls buildLoopClosureSummary (from
packages/loopover-miner/lib/loop-closure.ts) scoped to the repo that cycle just processed:
const loopSummary = buildLoopClosureSummaryFn(
{ eventLedger, portfolioQueue, runState },
{ sinceSeq, repoFullName: claimed.repoFullName },
);
sinceSeq = loopSummary.lastSeq;
buildLoopClosureSummary reads events filtered by BOTH sinceSeq AND repoFullName (loop- closure.ts, readEvents(filter) with filter.repoFullName set), and computes lastSeq as the
maximum seq among only those repo-filtered events:
const events = eventLedger.readEvents(filter); // filtered to claimed.repoFullName
let lastSeq = sinceSeq ?? 0;
for (const event of events) { if (event.seq > lastSeq) lastSeq = event.seq; }
The event ledger's seq column is a single, table-wide AUTOINCREMENT-backed counter shared
across every repo's events (event-ledger.ts's miner_event_ledger schema: seq INTEGER NOT NULL UNIQUE, computed via MAX(seq) over the whole table) — it is NOT partitioned per repo.
runLoop supports processing multiple repos in one invocation (loop repo1 repo2 ... targets, and
--search fan-out — both real, already-tested CLI modes in
test/unit/miner-loop-cli.test.ts's targets: [...] cases). When cycle N processes repo A and
cycle N+1 processes a DIFFERENT repo B, the sinceSeq value carried into cycle N+1 was computed as
repo A's lastSeq — the highest seq among repo A's own events, which (because seq is a shared
global counter) can be much higher than the seq of any of repo B's own pre-existing events. Feeding
that value into repo B's buildLoopClosureSummary call as its sinceSeq floor incorrectly excludes
any of repo B's own events that fall between the session-start seq and repo A's cycle-1 lastSeq,
even though those events are legitimately "new since repo B was last checked in this session."
The resulting loopSummary is embedded verbatim into the loop_reentry_decision event that
attemptLoopReentry (loop-reentry.ts) appends to the audit ledger — the ledger's own stated
purpose is to be a durable, independently-trustworthy trail of what each cycle actually observed, so
a cross-repo cursor mismatch corrupts that record for any run that alternates between repos.
No existing test exercises a full multi-cycle runLoop run against two different repos back to
back — test/unit/miner-loop-cli.test.ts's multi-repo targets: [...] cases only exercise
parseLoopArgs, and test/unit/miner-loop-closure.test.ts tests buildLoopClosureSummary in
isolation per single call, never the cross-cycle threading runLoop itself performs.
Requirements
- Fix
runLoop's cursor threading so that a cycle's sinceSeq cursor is never applied to a
DIFFERENT repo's buildLoopClosureSummary call than the one it was computed from. Either:
(a) track the cursor as a true global maximum seq across ALL events regardless of repo (so it
monotonically advances and is safe to apply as a floor to any repo's filtered read), or
(b) maintain a per-repo cursor map (Map<repoFullName, number>) instead of one shared sinceSeq
variable, looking up/updating the entry for claimed.repoFullName each cycle.
Pick one approach and apply it consistently; do not leave a partial fix that only handles some
code paths through runLoop.
- Preserve
buildLoopClosureSummary's existing external signature and its single-repo behavior
exactly — this issue is scoped to how runLoop threads the cursor between calls, not to
buildLoopClosureSummary's own internal filtering logic.
Deliverables
Test Coverage Requirements
This repo's Codecov patch gate is 99%+ (branch-counted) on packages/loopover-miner/lib/**. Add
the new regression test to test/unit/miner-loop-cli.test.ts (this package's tests live in the
shared root test/ directory, not packages/loopover-miner/test/**). The new/changed cursor-
threading logic must be exercised by both the multi-repo test above and confirm the existing
single-repo tests in the same file continue to pass unchanged.
Expected Outcome
A loop run targeting multiple repos computes each cycle's loopSummary/lastSeq from that
cycle's own repo's event history, never from a prior cycle's different-repo cursor. The
loop_reentry_decision audit trail is accurate for multi-repo runs, not just single-repo ones.
Links & Resources
packages/loopover-miner/lib/loop-cli.ts — runLoop, the sinceSeq variable (~line 342) and
its use around lines 598-602.
packages/loopover-miner/lib/loop-closure.ts — buildLoopClosureSummary (~lines 68-83).
packages/loopover-miner/lib/loop-reentry.ts — attemptLoopReentry, the consumer of
loopSummary that embeds it into the audit-critical loop_reentry_decision event.
packages/loopover-miner/lib/event-ledger.ts — the miner_event_ledger schema showing seq is
a single, table-wide counter (not per-repo).
Context
packages/loopover-miner/lib/loop-cli.ts'srunLoopprimes a cursor variable once at startup(
let sinceSeq = eventLedger.latestSeq();, around line 342) and carries it forward across everyloop cycle. Each cycle, it calls
buildLoopClosureSummary(frompackages/loopover-miner/lib/loop-closure.ts) scoped to the repo that cycle just processed:buildLoopClosureSummaryreads events filtered by BOTHsinceSeqANDrepoFullName(loop- closure.ts,readEvents(filter)withfilter.repoFullNameset), and computeslastSeqas themaximum
seqamong only those repo-filtered events:The event ledger's
seqcolumn is a single, table-wideAUTOINCREMENT-backed counter sharedacross every repo's events (
event-ledger.ts'sminer_event_ledgerschema:seq INTEGER NOT NULL UNIQUE, computed viaMAX(seq)over the whole table) — it is NOT partitioned per repo.runLoopsupports processing multiple repos in one invocation (loop repo1 repo2 ...targets, and--searchfan-out — both real, already-tested CLI modes intest/unit/miner-loop-cli.test.ts'stargets: [...]cases). When cycle N processes repo A andcycle N+1 processes a DIFFERENT repo B, the
sinceSeqvalue carried into cycle N+1 was computed asrepo A's
lastSeq— the highest seq among repo A's own events, which (becauseseqis a sharedglobal counter) can be much higher than the seq of any of repo B's own pre-existing events. Feeding
that value into repo B's
buildLoopClosureSummarycall as itssinceSeqfloor incorrectly excludesany of repo B's own events that fall between the session-start seq and repo A's cycle-1
lastSeq,even though those events are legitimately "new since repo B was last checked in this session."
The resulting
loopSummaryis embedded verbatim into theloop_reentry_decisionevent thatattemptLoopReentry(loop-reentry.ts) appends to the audit ledger — the ledger's own statedpurpose is to be a durable, independently-trustworthy trail of what each cycle actually observed, so
a cross-repo cursor mismatch corrupts that record for any run that alternates between repos.
No existing test exercises a full multi-cycle
runLooprun against two different repos back toback —
test/unit/miner-loop-cli.test.ts's multi-repotargets: [...]cases only exerciseparseLoopArgs, andtest/unit/miner-loop-closure.test.tstestsbuildLoopClosureSummaryinisolation per single call, never the cross-cycle threading
runLoopitself performs.Requirements
runLoop's cursor threading so that a cycle'ssinceSeqcursor is never applied to aDIFFERENT repo's
buildLoopClosureSummarycall than the one it was computed from. Either:(a) track the cursor as a true global maximum
seqacross ALL events regardless of repo (so itmonotonically advances and is safe to apply as a floor to any repo's filtered read), or
(b) maintain a per-repo cursor map (
Map<repoFullName, number>) instead of one sharedsinceSeqvariable, looking up/updating the entry for
claimed.repoFullNameeach cycle.Pick one approach and apply it consistently; do not leave a partial fix that only handles some
code paths through
runLoop.buildLoopClosureSummary's existing external signature and its single-repo behaviorexactly — this issue is scoped to how
runLoopthreads the cursor between calls, not tobuildLoopClosureSummary's own internal filtering logic.Deliverables
runLoop's cursor threading no longer applies one repo's cursor to a different repo'sbuildLoopClosureSummarycall.runLoop(or the smallest realistic harness around it thatexercises real cursor threading across cycles, not just
buildLoopClosureSummaryinisolation) across two consecutive cycles targeting two DIFFERENT repos, and asserts that the
second cycle's
loopSummaryfor repo B is computed from repo B's own event history — notinfluenced by repo A's
lastSeq. Construct the test so it would fail against the pre-fixcode (e.g. by seeding repo A's ledger with events at a higher seq than repo B's earliest
events, so an incorrectly-applied cross-repo cursor would visibly exclude events repo B's
own cycle should have counted).
Test Coverage Requirements
This repo's Codecov patch gate is 99%+ (branch-counted) on
packages/loopover-miner/lib/**. Addthe new regression test to
test/unit/miner-loop-cli.test.ts(this package's tests live in theshared root
test/directory, notpackages/loopover-miner/test/**). The new/changed cursor-threading logic must be exercised by both the multi-repo test above and confirm the existing
single-repo tests in the same file continue to pass unchanged.
Expected Outcome
A
looprun targeting multiple repos computes each cycle'sloopSummary/lastSeqfrom thatcycle's own repo's event history, never from a prior cycle's different-repo cursor. The
loop_reentry_decisionaudit trail is accurate for multi-repo runs, not just single-repo ones.Links & Resources
packages/loopover-miner/lib/loop-cli.ts—runLoop, thesinceSeqvariable (~line 342) andits use around lines 598-602.
packages/loopover-miner/lib/loop-closure.ts—buildLoopClosureSummary(~lines 68-83).packages/loopover-miner/lib/loop-reentry.ts—attemptLoopReentry, the consumer ofloopSummarythat embeds it into the audit-criticalloop_reentry_decisionevent.packages/loopover-miner/lib/event-ledger.ts— theminer_event_ledgerschema showingseqisa single, table-wide counter (not per-repo).