Skip to content

Commit b3d9cd2

Browse files
fix: drain before invalidating watchers, keep cache-clear on failed cleanup
- indexer.ts: the generation bump ran before drainWatcherOps(), so a watcher that had already completed addDocs() would see itself stale and return without recording those chunks in pathChunkCounts. The rebuild then computed staleIds from counts that omitted them and they survived in the index. `indexing` already blocks new watcher work, so the drain now happens first — letting in-flight operations record what they wrote — and the generation is bumped afterwards to cover anything that slips through. - indexer.ts: discardPartialIndex() reset discardedPreviousIndex when the delete failed, which made the caller skip clearIndexCache() and leave a meta.json describing the previous documents — already deleted by this rebuild. restoreFromMeta() would then report a ready index for documents that no longer exist on the next launch, which is worse than the leftovers it was protecting. The flag now stays true so the stale cache is dropped, while pathChunkCounts is kept for a retry in the same session. This reverses the choice made in bc906da. - server.py: runner.run() can end on a transport or LLM error, reaching none of the disconnect / watchdog / lifespan paths, and the finally discarded the only session handle without cancelling grade tasks — leaving a grader subprocess and its Ollama request running until timeout, then queueing into a dead worker. The finally now awaits session.shutdown() first, suppressed so teardown cannot mask the original error. Verified: a watcher writing two chunks as a rebuild starts is stranded under bump-before-drain and cleaned up under drain-then-bump; a failed partial cleanup now still clears the cache while keeping the counts; and a runner error leaves the grader alive without the shutdown call and terminated with it, with shutdown() confirmed safe to call twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent bc906da commit b3d9cd2

2 files changed

Lines changed: 23 additions & 4 deletions

File tree

apps/moss-interview-coach/backend/server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,13 @@ async def _handshake_watchdog(current: ActiveSession) -> None:
961961
await watchdog
962962
finally:
963963
if session is not None:
964+
# runner.run() can also end on a transport or LLM error, which
965+
# reaches none of the disconnect / watchdog / lifespan paths. Tear
966+
# the session down here before dropping the only handle to it, or a
967+
# spawned grader keeps its subprocess and Ollama request alive until
968+
# timeout and then queues a result into a dead worker.
969+
with suppress(Exception):
970+
await session.shutdown()
964971
active_sessions.discard(session)
965972

966973

apps/moss-vscode/src/indexer/indexer.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,20 @@ export class CodebaseIndexer {
112112
if (this.indexing) {
113113
return;
114114
}
115+
// `indexing` is already set, so no *new* watcher work can start.
115116
this.indexing = true;
116117
this.discardedPreviousIndex = false;
117-
// Invalidate watcher work already in flight, then wait for it to settle so
118-
// no late write lands between our delete and our rewrite.
119-
this.generation += 1;
120118

121119
try {
120+
// Drain before bumping the generation, not after. A watcher that has
121+
// already called addDocs() must be allowed to record those chunks in
122+
// pathChunkCounts — marking it stale first would make it return silently,
123+
// leaving its writes out of the staleIds we compute below and stranding
124+
// them in the index. Once everything in flight has settled, the bump
125+
// covers anything that slips through afterwards.
122126
await this.drainWatcherOps();
127+
this.generation += 1;
128+
123129
const files = await scanWorkspaceFiles(token);
124130
this.setStatus({ state: "indexing", processed: 0, total: files.length });
125131

@@ -482,7 +488,13 @@ export class CodebaseIndexer {
482488
try {
483489
await this.deleteInBatches(partialIds);
484490
} catch {
485-
this.discardedPreviousIndex = false;
491+
// Deliberately leave discardedPreviousIndex true so the caller still
492+
// drops the persisted cache. That cache describes the *previous*
493+
// documents, which this rebuild already deleted — keeping it would let
494+
// restoreFromMeta() come back on the next launch reporting a ready
495+
// index for documents that no longer exist, which is worse than the
496+
// leftovers. pathChunkCounts is kept so a retry in this session can
497+
// still delete what we failed to remove here.
486498
return;
487499
}
488500
}

0 commit comments

Comments
 (0)