@@ -1180,9 +1180,11 @@ export const BROAD_REACH_SHARE = 0.25
11801180/**
11811181 * How long a caller's saturated reach is remembered. Reach counts the documents a caller's tokens
11821182 * touch in the bases, which moves slowly, and an unbounded set only means the legs search the
1183- * index with the full access predicate, so a stale answer costs speed, never access.
1183+ * index with the full access predicate, so a stale answer costs speed, never access. Counting it
1184+ * is the one read of a search that scales with the caller's reach rather than the query, so it is
1185+ * remembered for long.
11841186 */
1185- const SATURATED_REACH_TTL_MS = 5 * 60 * 1000
1187+ const SATURATED_REACH_TTL_MS = 60 * 60 * 1000
11861188
11871189/** A saturated reach, and whether it is broad enough to walk the whole graph for. */
11881190const saturatedReach = new LRUCache < string , { broad : boolean } > ( {
@@ -1253,35 +1255,48 @@ async function estimateFilteredDocuments(
12531255 * documents. Counted once against that bound and remembered, so the first search after the
12541256 * window pays for it and the rest do not. A caller whose probe already saturated is known to
12551257 * reach past the probe's limit, so a bound inside that limit is met without counting.
1258+ *
1259+ * The count reads as many index entries as the caller reaches, so on a large index it can cost
1260+ * more than the leg it serves; it gets the probe's share of the deadline, never the whole leg's.
1261+ * A count that runs out of that share answers `null`: the leg keeps its time and its deadline
1262+ * intact, and the caller decides this search alone without remembering anything.
12561263 */
12571264async function reachIsBroad (
12581265 knowledgeBaseIds : string [ ] ,
12591266 access : KnowledgeAccessScope ,
12601267 budget : SearchBudget | undefined ,
12611268 plan : SearchAccessPlan | undefined ,
12621269 saturated : boolean
1263- ) : Promise < boolean > {
1270+ ) : Promise < boolean | null > {
12641271 if ( access . kind !== 'user' ) return true
1265- const total =
1266- ( await indexDocumentCounts . fetch ( [ ...knowledgeBaseIds ] . sort ( ) . join ( ',' ) , {
1267- context : budget ,
1268- } ) ) ?? 0
1269- const bound = Math . ceil ( total * BROAD_REACH_SHARE )
1270- if ( saturated && bound <= VECTOR_PROBE_DOCUMENT_LIMIT ) return true
1271- const [ row ] = await runSearchQuery ( budget , 'permitted_documents' , ( executor ) =>
1272- executor . execute < { n : number } > ( sql `
1273- SELECT count(*) AS n FROM (
1274- SELECT 1 FROM ${ document }
1275- WHERE ${ and (
1276- isNull ( document . deletedAt ) ,
1277- knowledgeAclOverlapCondition ( access ) ,
1278- inArray ( document . knowledgeBaseId , knowledgeBaseIds ) ,
1279- planSourceCondition ( plan )
1280- ) }
1281- LIMIT ${ bound }
1282- ) reached` )
1283- )
1284- return Number ( row ?. n ?? 0 ) >= bound
1272+ const countBudget = budget ?. capped ( VECTOR_PROBE_BUDGET_MS )
1273+ try {
1274+ const total =
1275+ ( await indexDocumentCounts . fetch ( [ ...knowledgeBaseIds ] . sort ( ) . join ( ',' ) , {
1276+ context : countBudget ,
1277+ } ) ) ?? 0
1278+ const bound = Math . ceil ( total * BROAD_REACH_SHARE )
1279+ if ( saturated && bound <= VECTOR_PROBE_DOCUMENT_LIMIT ) return true
1280+ const [ row ] = await runSearchQuery ( countBudget , 'permitted_documents' , ( executor ) =>
1281+ executor . execute < { n : number } > ( sql `
1282+ SELECT count(*) AS n FROM (
1283+ SELECT 1 FROM ${ document }
1284+ WHERE ${ and (
1285+ isNull ( document . deletedAt ) ,
1286+ knowledgeAclOverlapCondition ( access ) ,
1287+ inArray ( document . knowledgeBaseId , knowledgeBaseIds ) ,
1288+ planSourceCondition ( plan )
1289+ ) }
1290+ LIMIT ${ bound }
1291+ ) reached` )
1292+ )
1293+ return Number ( row ?. n ?? 0 ) >= bound
1294+ } catch ( error ) {
1295+ if ( ! budget || ! countBudget ?. isTimeout ( error ) ) throw error
1296+ /** Only the count's share was spent; the leg's own deadline still governs. */
1297+ budget . remaining ( )
1298+ return null
1299+ }
12851300}
12861301
12871302/**
@@ -1331,15 +1346,11 @@ export async function resolveReach(
13311346 const key = reachKey ( knowledgeBaseIds , access , plan )
13321347 const remembered = key ? saturatedReach . get ( key ) : undefined
13331348 if ( remembered ) return { kind : 'unbounded' , broad : remembered . broad }
1334- try {
1335- const broad = await reachIsBroad ( knowledgeBaseIds , access , budget , plan , false )
1336- if ( key ) saturatedReach . set ( key , { broad } )
1337- return { kind : 'unbounded' , broad }
1338- } catch ( error ) {
1339- if ( ! budget ?. isTimeout ( error ) ) throw error
1340- /** A count that ran out of time decides this search only; the next one counts again. */
1341- return { kind : 'unbounded' , broad : true }
1342- }
1349+ const broad = await reachIsBroad ( knowledgeBaseIds , access , budget , plan , false )
1350+ /** A count that ran out of time decides this search only; the next one counts again. */
1351+ if ( broad === null ) return { kind : 'unbounded' , broad : true }
1352+ if ( key ) saturatedReach . set ( key , { broad } )
1353+ return { kind : 'unbounded' , broad }
13431354}
13441355
13451356/**
@@ -1392,18 +1403,17 @@ export async function resolvePermittedDocuments(params: {
13921403 probe = { kind : 'timed_out' }
13931404 }
13941405 if ( probe . kind === 'saturated' ) {
1395- try {
1396- broad = await reachIsBroad (
1397- params . knowledgeBaseIds ,
1398- params . access ,
1399- params . budget ,
1400- params . accessPlan ,
1401- true
1402- )
1406+ const counted = await reachIsBroad (
1407+ params . knowledgeBaseIds ,
1408+ params . access ,
1409+ params . budget ,
1410+ params . accessPlan ,
1411+ true
1412+ )
1413+ /** A count that ran out of time decides this search only; the next one counts again. */
1414+ if ( counted !== null ) {
1415+ broad = counted
14031416 if ( key ) saturatedReach . set ( key , { broad } )
1404- } catch ( error ) {
1405- if ( ! params . budget ?. isTimeout ( error ) ) throw error
1406- /** A count that ran out of time decides this search only; the next one counts again. */
14071417 }
14081418 }
14091419 }
@@ -2658,9 +2668,7 @@ export async function retrieveKnowledgeSearch(
26582668 * readable documents enumerated ahead of ranking: its reach alone chooses between one
26592669 * walk over the whole graph and a search of each source.
26602670 */
2661- await measureSearchStage ( 'permitted_documents' , ( ) =>
2662- resolveReach ( knowledgeBaseIds , access , budgets . vector , accessPlan )
2663- )
2671+ await resolveReach ( knowledgeBaseIds , access , budgets . vector , accessPlan )
26642672 : await resolvePermittedDocuments ( {
26652673 knowledgeBaseIds,
26662674 access,
0 commit comments