commit 42a117fd14387eea5d5e87e85df88356f679c1ab
parent df03d3151dd543329bfd4b341285c10764d3a85e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 18:29:17 -0400
common: a cached or empty leaf reports its progress over the scope it read, so "searched N/M" only climbs (re-review R-L1)
applyCached reported the parent scope as processed. Through it, the
empty-scope short-circuit made a Search in copy read as the whole parent
done, and the fold summed it with the streaming copy: "searched 40/40",
then "20/40", then climbing. It now takes the size of the scope the leaf
read: 0 of 0 when empty (skipped by the session's readout), and a cached
leaf's own effective scope. The real-driver test with the posts scope null
asserts the folded leaf's processed and fraction never fall and the empty
copy adds nothing to the total; it fails on the previous code.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 41 insertions(+), 11 deletions(-)
diff --git a/common/lib/search/searchIn.test.ts b/common/lib/search/searchIn.test.ts
@@ -267,6 +267,8 @@ function runReal(
root: GroupNode,
s: SearchIn,
scopes: { chat: ReadonlySet<string> | null; posts: ReadonlySet<string> | null },
+ // Every folded progress, in order — what the session's readout sees.
+ seen: TreeProgress[] = [],
): Promise<TreeProgress> {
const rewritten = applySearchIn(root, s);
return within(
@@ -282,7 +284,9 @@ function runReal(
concurrency: 2,
flushIntervalMs: 5,
emit: (p) => {
- if (p.done) resolve(foldSearchIn(p, rewritten));
+ const folded = foldSearchIn(p, rewritten);
+ seen.push(folded);
+ if (p.done) resolve(folded);
},
});
}),
@@ -291,12 +295,31 @@ function runReal(
test("a plain query whose posts copy has nothing to read finishes (posts scope null)", async () => {
// What the session passes under a tag filter: no posts in scope at all.
- const p = await runReal(ALPHA(), { transcripts: true, posts: true, chat: false }, {
- chat: null,
- posts: null,
- });
+ const seen: TreeProgress[] = [];
+ const p = await runReal(
+ ALPHA(),
+ { transcripts: true, posts: true, chat: false },
+ { chat: null, posts: null },
+ seen,
+ );
assert.deepEqual([...p.slugs], ["v1"]);
assert.equal(p.leafStates.get("a")?.active, false);
+ // The folded leaf's "searched N/M" only climbs (re-review R-L1): the empty
+ // copy reports 0 of 0, not its parent scope as done, so the readout does
+ // not start full and fall back as the transcripts copy streams.
+ let lastProcessed = 0;
+ let lastFraction = 0;
+ for (const q of seen) {
+ const a = q.leafStates.get("a");
+ if (!a || a.totalToProcess === 0) continue;
+ assert.ok(a.processed >= lastProcessed, `processed fell: ${lastProcessed} → ${a.processed}`);
+ const f = a.processed / a.totalToProcess;
+ assert.ok(f >= lastFraction, `searched fell: ${lastFraction} → ${f}`);
+ lastProcessed = a.processed;
+ lastFraction = f;
+ }
+ assert.equal(p.leafStates.get("a")?.processed, 3);
+ assert.equal(p.leafStates.get("a")?.totalToProcess, 3, "the empty copy adds nothing to the total");
});
test("… and with an empty posts set (a channel selection with no posts channel)", async () => {
diff --git a/common/lib/searchEval.ts b/common/lib/searchEval.ts
@@ -353,7 +353,7 @@ async function runLeaf(
if (scopeArr.length === 0) {
return applyCached(
leaf,
- parentScope,
+ 0,
{ slugs: new Set(), hits: new Map() },
ctx,
/*cached*/ false,
@@ -363,12 +363,12 @@ async function runLeaf(
const key = ctx.runtime.cache.key(canonicalHash(leaf), scopeHash);
const cachedSync = ctx.runtime.cache.getSync(key);
if (cachedSync) {
- return applyCached(leaf, parentScope, cachedSync, ctx, /*cached*/ true);
+ return applyCached(leaf, scopeArr.length, cachedSync, ctx, /*cached*/ true);
}
const cached = await ctx.runtime.cache.get(key);
if (ctx.cancelled) return new Set();
if (cached) {
- return applyCached(leaf, parentScope, cached, ctx, /*cached*/ true);
+ return applyCached(leaf, scopeArr.length, cached, ctx, /*cached*/ true);
}
// Cache miss — kick off a real pipeline. Pure-filter leaves (no hit
@@ -428,9 +428,16 @@ async function runLeaf(
return new Set(result.slugs);
}
+// `scopeSize` is the size of the scope the leaf READ — its effective scope,
+// not its parent's — so its progress is "N of N" for the slugs it covered. The
+// parent scope over-counted: a chat leaf narrowed to the videos with chat, or
+// a "Search in" copy, reported the whole parent as processed, and the folded
+// plain leaf's "searched N/M" started full and then fell back as its other
+// copies streamed. An empty scope reports 0 of 0, which the session's
+// progress readout skips.
function applyCached(
leaf: LeafNode,
- parentScope: Set<string>,
+ scopeSize: number,
cached: CachedResult,
ctx: EvalCtx,
isCacheHit: boolean,
@@ -452,8 +459,8 @@ function applyCached(
setLeafState(ctx, leaf.id, {
slugCount: slugs.size,
totalHits: countHits(hits),
- processed: parentScope.size,
- totalToProcess: parentScope.size,
+ processed: scopeSize,
+ totalToProcess: scopeSize,
capped: false,
cached: isCacheHit,
active: false,