Revert "perf(contextual-retrieval): bound per-chunk synopsis concurrency (#2628)"

This reverts commit 3454dca0b4.
This commit is contained in:
Garry Tan
2026-07-23 11:24:25 -07:00
parent 68e4cebd1a
commit 8b7e30afcd
3 changed files with 96 additions and 534 deletions
+80 -165
View File
@@ -45,6 +45,7 @@ import { embedBatch } from './embedding.ts';
import { resolveContextualRetrievalMode } from './contextual-retrieval-resolver.ts';
import {
buildContextualPrefix,
extractFirstTwoSentences,
modeRequiresHaiku,
modeRequiresWrapper,
sanitizeTitle,
@@ -56,8 +57,10 @@ import {
SYNOPSIS_DOC_MAX_CHARS,
type GeneratePerChunkSynopsisResult,
} from './page-summary.ts';
import type { SynopsisFailureKind } from './audit-synopsis.ts';
import { runSlidingPool } from './worker-pool.ts';
import {
logSynopsisFailure,
type SynopsisFailureKind,
} from './audit-synopsis.ts';
import type { BrainEngine } from './engine.ts';
import type { ChunkInput, CRMode, Page } from './types.ts';
import type { SourceRow } from './sources-ops.ts';
@@ -70,24 +73,6 @@ import type { SourceRow } from './sources-ops.ts';
* corpus_generation hash.
*/
export const TITLE_WRAPPER_VERSION = 1;
const DEFAULT_HAIKU_MODEL = 'anthropic:claude-haiku-4-5-20251001';
export const DEFAULT_CONTEXTUAL_CHUNK_CONCURRENCY = 4;
export const MAX_CONTEXTUAL_CHUNK_CONCURRENCY = 16;
export function resolveContextualChunkConcurrency(
env: Record<string, string | undefined> = process.env,
): number {
const raw = env.GBRAIN_CONTEXTUAL_CHUNK_CONCURRENCY;
if (raw === undefined || raw.trim() === '') return DEFAULT_CONTEXTUAL_CHUNK_CONCURRENCY;
const n = Number(raw);
if (!Number.isFinite(n)) return DEFAULT_CONTEXTUAL_CHUNK_CONCURRENCY;
return clampContextualChunkConcurrency(n);
}
function clampContextualChunkConcurrency(n: number): number {
if (!Number.isFinite(n)) return DEFAULT_CONTEXTUAL_CHUNK_CONCURRENCY;
return Math.max(1, Math.min(MAX_CONTEXTUAL_CHUNK_CONCURRENCY, Math.trunc(n)));
}
/**
* Embedding model placeholder. The actual model name lands here from
@@ -223,16 +208,12 @@ export interface ReembedPageArgs {
* src/core/minions/rate-leases.ts here; inline callers (import-file,
* reindex command) pass undefined and rely on gateway-level retry.
*/
acquireSynopsisLease?: () => Promise<unknown>;
releaseSynopsisLease?: (lease?: unknown) => Promise<void>;
/**
* Intra-page per-chunk synopsis concurrency. 1 preserves the legacy
* sequential loop exactly; higher values only parallelize Haiku synopsis
* calls. Embedding remains one batch after all synopses succeed.
*/
chunkConcurrency?: number;
acquireSynopsisLease?: () => Promise<void>;
releaseSynopsisLease?: () => Promise<void>;
}
const DEFAULT_HAIKU_MODEL = 'anthropic:claude-haiku-4-5-20251001';
/**
* Re-embed one page through the active CR mode. Implements the D26 P0-2
* two-phase build pattern.
@@ -451,41 +432,82 @@ async function tryBuildPhase1(opts: {
}
// per_chunk_synopsis path. Read source text via fallback chain,
// generate synopsis per chunk through a bounded sliding pool, then
// generate synopsis per chunk sequentially within this page (D10),
// batch embed at the end (D27 P2-2).
const sourceText = readSourceTextWithFallback(page, chunks);
const wrappedTexts: string[] = new Array(chunks.length);
const chunkConcurrency = clampContextualChunkConcurrency(
args.chunkConcurrency ?? resolveContextualChunkConcurrency(),
);
const wrappedTexts: string[] = [];
const poolResult = await runSlidingPool({
items: chunks,
workers: chunkConcurrency,
signal: args.abortSignal,
onError: 'abort',
failureLabel: (c) => String(c.chunk_index),
onItem: async (c, i) => {
wrappedTexts[i] = await buildWrappedChunkText({
chunk: c,
sourceText,
safeTitle,
page,
args,
haikuModel,
});
},
});
for (let i = 0; i < chunks.length; i++) {
const c = chunks[i];
if (poolResult.failures.length > 0) {
const failure = [...poolResult.failures].sort((a, b) => a.idx - b.idx)[0].error;
if (failure instanceof ChunkSynopsisPhase1Error) {
return failure.result;
// Code chunks always bypass the wrapper (D20-T4) — pass through.
if (c.chunk_source === 'fenced_code') {
wrappedTexts.push(c.chunk_text);
continue;
}
throw failure;
}
if (poolResult.aborted || args.abortSignal?.aborted) {
return { kind: 'transient', cause: 'timeout', detail: 'aborted' };
// Acquire rate-lease per chunk (D26 P0-3). Inline callers pass no
// hooks; only the Minion handler wires through rate-leases.ts.
if (args.acquireSynopsisLease) {
await args.acquireSynopsisLease();
}
let synopsisResult: GeneratePerChunkSynopsisResult;
try {
synopsisResult = await generatePerChunkSynopsis({
documentText: sourceText,
chunkText: c.chunk_text,
pageTitle: page.title,
pageSlug: args.pageSlug,
sourceId: args.sourceId,
chunkIndex: c.chunk_index,
model: haikuModel,
abortSignal: args.abortSignal,
});
} finally {
if (args.releaseSynopsisLease) {
try {
await args.releaseSynopsisLease();
} catch {
// Lease release failure shouldn't abort the page; surfacing it
// would race with the synopsis result. Audit-only.
}
}
}
if (synopsisResult.kind === 'success') {
const prefix = buildContextualPrefix(safeTitle, synopsisResult.synopsis);
wrappedTexts.push(
wrapChunkForEmbedding(c.chunk_text, prefix, c.chunk_source),
);
continue;
}
// Failure classification per D27 P1-2:
// refusal | empty | malformed → page-level fall-back to title-only
// auth_failure → permanent (won't fix with retry)
// rate_limit | timeout | network | provider_5xx → transient
// source_missing → walked into fallback already; would be 'malformed'
// from generatePerChunkSynopsis if we ever propagated it here
if (
synopsisResult.kind === 'refusal' ||
synopsisResult.kind === 'empty' ||
synopsisResult.kind === 'malformed'
) {
return { kind: 'page_level_fallback_requested', cause: synopsisResult.kind };
}
if (synopsisResult.kind === 'auth_failure') {
return {
kind: 'permanent',
cause: synopsisResult.kind,
detail: synopsisResult.detail ?? 'auth failure',
};
}
return {
kind: 'transient',
cause: synopsisResult.kind,
detail: synopsisResult.detail ?? 'transient',
};
}
// All chunks synthesized successfully. Single batch embed (D27 P2-2).
@@ -506,113 +528,6 @@ async function tryBuildPhase1(opts: {
}
}
class ChunkSynopsisPhase1Error extends Error {
constructor(readonly result: Exclude<Phase1Result, Phase1Success>) {
super(`chunk synopsis failed: ${result.kind}`);
this.name = 'ChunkSynopsisPhase1Error';
}
}
async function buildWrappedChunkText(opts: {
chunk: ChunkInput;
sourceText: string;
safeTitle: string;
page: Page;
args: ReembedPageArgs;
haikuModel: string;
}): Promise<string> {
const { chunk: c, sourceText, safeTitle, page, args, haikuModel } = opts;
// Code chunks always bypass the wrapper (D20-T4) — pass through.
if (c.chunk_source === 'fenced_code') {
return c.chunk_text;
}
// Acquire rate-lease per chunk (D26 P0-3). Inline callers pass no
// hooks; only the Minion handler wires through rate-leases.ts.
let lease: unknown;
let leaseAcquired = false;
let synopsisResult: GeneratePerChunkSynopsisResult;
try {
if (args.acquireSynopsisLease) {
try {
lease = await args.acquireSynopsisLease();
} catch (err) {
if (args.abortSignal?.aborted || isAbortError(err)) {
throw new ChunkSynopsisPhase1Error({
kind: 'transient',
cause: 'timeout',
detail: 'aborted',
});
}
throw err;
}
leaseAcquired = true;
}
synopsisResult = await generatePerChunkSynopsis({
documentText: sourceText,
chunkText: c.chunk_text,
pageTitle: page.title,
pageSlug: args.pageSlug,
sourceId: args.sourceId,
chunkIndex: c.chunk_index,
model: haikuModel,
abortSignal: args.abortSignal,
});
} finally {
if (leaseAcquired && args.releaseSynopsisLease) {
try {
await args.releaseSynopsisLease(lease);
} catch {
// Lease release failure shouldn't abort the page; surfacing it
// would race with the synopsis result. Audit-only.
}
}
}
if (synopsisResult.kind === 'success') {
const prefix = buildContextualPrefix(safeTitle, synopsisResult.synopsis);
return wrapChunkForEmbedding(c.chunk_text, prefix, c.chunk_source);
}
// Failure classification per D27 P1-2:
// refusal | empty | malformed → page-level fall-back to title-only
// auth_failure → permanent (won't fix with retry)
// rate_limit | timeout | network | provider_5xx → transient
// source_missing → walked into fallback already; would be 'malformed'
// from generatePerChunkSynopsis if we ever propagated it here
if (
synopsisResult.kind === 'refusal' ||
synopsisResult.kind === 'empty' ||
synopsisResult.kind === 'malformed'
) {
throw new ChunkSynopsisPhase1Error({
kind: 'page_level_fallback_requested',
cause: synopsisResult.kind,
});
}
if (synopsisResult.kind === 'auth_failure') {
throw new ChunkSynopsisPhase1Error({
kind: 'permanent',
cause: synopsisResult.kind,
detail: synopsisResult.detail ?? 'auth failure',
});
}
throw new ChunkSynopsisPhase1Error({
kind: 'transient',
cause: synopsisResult.kind,
detail: synopsisResult.detail ?? 'transient',
});
}
function isAbortError(err: unknown): boolean {
return (
typeof err === 'object' &&
err !== null &&
(err as { name?: unknown }).name === 'AbortError'
);
}
/**
* Source-text fallback chain per D11:
* 1. read page.source_path from disk (truest "document")
@@ -40,7 +40,6 @@ import { UnrecoverableError } from '../types.ts';
import type { BrainEngine } from '../../engine.ts';
import {
reembedPageWithContextualRetrieval,
resolveContextualChunkConcurrency,
type ReembedPageResult,
} from '../../contextual-retrieval-service.ts';
import {
@@ -133,7 +132,7 @@ export function makeContextualReindexHandler(opts: MakeContextualReindexHandlerO
// call inside the service acquires/releases a lease against the
// shared key across all worker processes.
const maxConcurrent = resolveMaxConcurrent();
const chunkConcurrency = resolveContextualChunkConcurrency();
let currentLeaseId: number | null = null;
const result: ReembedPageResult = await reembedPageWithContextualRetrieval({
engine,
@@ -142,32 +141,32 @@ export function makeContextualReindexHandler(opts: MakeContextualReindexHandlerO
globalMode,
killSwitchDisabled,
abortSignal: ctx.signal,
chunkConcurrency,
acquireSynopsisLease: async () => {
// Poll-acquire with brief backoff. The service's per-chunk loop
// is bounded within a page; this guards against the cross-worker
// pile-up and remains the global rate governor.
// is sequential within a page; this guards against the cross-
// worker pile-up.
let attempts = 0;
const maxAttempts = 60; // ~1 min max wait per chunk before giving up
while (attempts < maxAttempts) {
if (ctx.signal.aborted) throw abortError();
const res = await acquireLease(engine, RATE_LEASE_KEY, ctx.id, maxConcurrent, {
ttlMs: 60_000,
});
if (res.acquired && res.leaseId != null) {
return res.leaseId;
currentLeaseId = res.leaseId;
return;
}
attempts++;
await sleepWithAbort(1000, ctx.signal);
await new Promise((r) => setTimeout(r, 1000));
}
throw new Error(
`Failed to acquire ${RATE_LEASE_KEY} lease after ${maxAttempts} attempts; ` +
`Haiku rate limit pile-up too deep.`,
);
},
releaseSynopsisLease: async (lease) => {
if (typeof lease === 'number') {
await releaseLease(engine, lease);
releaseSynopsisLease: async () => {
if (currentLeaseId != null) {
await releaseLease(engine, currentLeaseId);
currentLeaseId = null;
}
},
});
@@ -219,26 +218,6 @@ async function tryLoadPageAcrossSources(
return null;
}
function sleepWithAbort(ms: number, signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
if (signal.aborted) {
reject(abortError());
return;
}
const timer = setTimeout(resolve, ms);
signal.addEventListener('abort', () => {
clearTimeout(timer);
reject(abortError());
}, { once: true });
});
}
function abortError(): Error {
const err = new Error('aborted');
err.name = 'AbortError';
return err;
}
function classifyResult(
pageSlug: string,
result: ReembedPageResult,
+6 -338
View File
@@ -1,38 +1,20 @@
/**
* Pure-function tests for src/core/contextual-retrieval-service.ts.
*
* This file pins the service's pure helpers plus hermetic service behavior
* driven through fake engine + gateway seams. Full PGLite coverage lives in
* test/e2e/contextual-retrieval-pglite.test.ts.
* The full service test (PHASE 1 + PHASE 2 happy path, refusal restart,
* transient error propagation) needs a real PGLite + gateway stub seam.
* That lands in test/e2e/contextual-retrieval.test.ts. This file pins
* the service's pure helpers: corpus_generation hash composition + the
* expectedMode helper used by the T9 reindex sweep predicate.
*/
import { afterEach, describe, test, expect } from 'bun:test';
import { describe, test, expect } from 'bun:test';
import {
computeCorpusGeneration,
computeSourceTextHash,
expectedModeForPageSourceOnly,
reembedPageWithContextualRetrieval,
resolveContextualChunkConcurrency,
TITLE_WRAPPER_VERSION,
} from '../src/core/contextual-retrieval-service.ts';
import {
__setChatTransportForTests,
__setEmbedTransportForTests,
configureGateway,
resetGateway,
type ChatOpts,
type ChatResult,
} from '../src/core/ai/gateway.ts';
import type { ChunkInput } from '../src/core/types.ts';
import { withEnv } from './helpers/with-env.ts';
const TEST_DIMS = 1536;
afterEach(() => {
__setChatTransportForTests(null);
__setEmbedTransportForTests(null);
resetGateway();
});
describe('computeCorpusGeneration', () => {
test('returns 16-char hex hash', () => {
@@ -156,317 +138,3 @@ describe('expectedModeForPageSourceOnly (T9 reindex sweep helper)', () => {
}
});
});
describe('resolveContextualChunkConcurrency', () => {
test('defaults to 4 and reads the process env', async () => {
await withEnv({ GBRAIN_CONTEXTUAL_CHUNK_CONCURRENCY: undefined }, async () => {
expect(resolveContextualChunkConcurrency()).toBe(4);
});
await withEnv({ GBRAIN_CONTEXTUAL_CHUNK_CONCURRENCY: '7' }, async () => {
expect(resolveContextualChunkConcurrency()).toBe(7);
});
});
test('clamps to [1, 16] and ignores invalid values', () => {
expect(resolveContextualChunkConcurrency({
GBRAIN_CONTEXTUAL_CHUNK_CONCURRENCY: '0',
})).toBe(1);
expect(resolveContextualChunkConcurrency({
GBRAIN_CONTEXTUAL_CHUNK_CONCURRENCY: '-3',
})).toBe(1);
expect(resolveContextualChunkConcurrency({
GBRAIN_CONTEXTUAL_CHUNK_CONCURRENCY: '99',
})).toBe(16);
expect(resolveContextualChunkConcurrency({
GBRAIN_CONTEXTUAL_CHUNK_CONCURRENCY: '1.9',
})).toBe(1);
expect(resolveContextualChunkConcurrency({
GBRAIN_CONTEXTUAL_CHUNK_CONCURRENCY: 'not-a-number',
})).toBe(4);
});
});
describe('per-chunk synopsis concurrency', () => {
test('concurrency > 1 preserves chunk-order embed input', async () => {
const chunks = makeChunks(['alpha', 'beta', 'gamma', 'delta']);
const delays: Record<string, number> = { alpha: 30, beta: 5, gamma: 20, delta: 1 };
const sequential = await runWithChatStub({
chunks,
concurrency: 1,
delayForChunk: (chunk) => delays[chunk] ?? 1,
});
const parallel = await runWithChatStub({
chunks,
concurrency: 4,
delayForChunk: (chunk) => delays[chunk] ?? 1,
});
expect(parallel.result.kind).toBe('success');
expect(parallel.embedInputs).toEqual(sequential.embedInputs);
expect(parallel.embeddedChunks.map((c) => c.chunk_text)).toEqual(
chunks.map((c) => c.chunk_text),
);
});
test('concurrency is bounded', async () => {
let active = 0;
let maxActive = 0;
let leaseActive = 0;
let maxLeaseActive = 0;
let acquired = 0;
let released = 0;
const chunks = makeChunks(Array.from({ length: 8 }, (_, i) => `chunk-${i}`));
const out = await runWithChatStub({
chunks,
concurrency: 3,
acquireSynopsisLease: async () => {
acquired++;
leaseActive++;
maxLeaseActive = Math.max(maxLeaseActive, leaseActive);
return acquired;
},
releaseSynopsisLease: async () => {
released++;
leaseActive--;
},
chat: async (opts) => {
active++;
maxActive = Math.max(maxActive, active);
try {
await delay(20, opts.abortSignal);
return chatSuccess(`Synopsis for ${extractChunk(opts)}`);
} finally {
active--;
}
},
});
expect(out.result.kind).toBe('success');
expect(maxActive).toBeGreaterThan(1);
expect(maxActive).toBeLessThanOrEqual(3);
expect(maxLeaseActive).toBeLessThanOrEqual(3);
expect(acquired).toBe(8);
expect(released).toBe(8);
expect(leaseActive).toBe(0);
});
test('one chunk failure aborts queued work and falls back at page level', async () => {
let started = 0;
const chunks = makeChunks(Array.from({ length: 9 }, (_, i) => `chunk-${i}`));
const out = await runWithChatStub({
chunks,
concurrency: 3,
chat: async (opts) => {
started++;
const chunk = extractChunk(opts);
if (chunk === 'chunk-0') return chatSuccess('');
await delay(30, opts.abortSignal);
return chatSuccess(`Synopsis for ${chunk}`);
},
});
expect(out.result.kind).toBe('page_fallback');
expect(started).toBeLessThanOrEqual(3);
});
test('fenced code chunks bypass synopsis calls and leases', async () => {
let chatCalls = 0;
let leaseCalls = 0;
const chunks: ChunkInput[] = [
{ chunk_index: 0, chunk_text: 'intro', chunk_source: 'compiled_truth' },
{ chunk_index: 1, chunk_text: 'const x = 1;', chunk_source: 'fenced_code' },
{ chunk_index: 2, chunk_text: 'outro', chunk_source: 'compiled_truth' },
];
const out = await runWithChatStub({
chunks,
concurrency: 3,
acquireSynopsisLease: async () => {
leaseCalls++;
},
releaseSynopsisLease: async () => {},
chat: async (opts) => {
chatCalls++;
return chatSuccess(`Synopsis for ${extractChunk(opts)}`);
},
});
expect(out.result.kind).toBe('success');
expect(chatCalls).toBe(2);
expect(leaseCalls).toBe(2);
expect(out.embedInputs[1]).toBe('const x = 1;');
});
test('abortSignal cancels in-flight and queued synopsis work promptly', async () => {
const controller = new AbortController();
let started = 0;
const chunks = makeChunks(Array.from({ length: 20 }, (_, i) => `chunk-${i}`));
const startedAt = Date.now();
const promise = runWithChatStub({
chunks,
concurrency: 4,
abortSignal: controller.signal,
chat: async (opts) => {
started++;
await delay(1000, opts.abortSignal);
return chatSuccess(`Synopsis for ${extractChunk(opts)}`);
},
});
setTimeout(() => controller.abort(), 20);
const out = await promise;
expect(out.result.kind).toBe('transient_error');
if (out.result.kind === 'transient_error') {
expect(out.result.cause).toBe('timeout');
}
expect(started).toBeLessThanOrEqual(4);
expect(Date.now() - startedAt).toBeLessThan(300);
});
});
function makeChunks(texts: string[]): ChunkInput[] {
return texts.map((text, i) => ({
chunk_index: i,
chunk_text: text,
chunk_source: 'compiled_truth',
}));
}
async function runWithChatStub(opts: {
chunks: ChunkInput[];
concurrency: number;
abortSignal?: AbortSignal;
delayForChunk?: (chunk: string) => number;
chat?: (opts: ChatOpts) => Promise<ChatResult>;
acquireSynopsisLease?: () => Promise<unknown>;
releaseSynopsisLease?: (lease?: unknown) => Promise<void>;
}) {
configureGateway({
embedding_model: 'openai:text-embedding-3-large',
embedding_dimensions: TEST_DIMS,
env: { OPENAI_API_KEY: 'sk-test' },
});
const embedInputs: string[][] = [];
__setEmbedTransportForTests(async ({ values }: any) => {
embedInputs.push([...values]);
return {
embeddings: values.map((_: string, i: number) =>
Array.from({ length: TEST_DIMS }, () => 0.001 + i * 0.001),
),
usage: { tokens: 0 },
} as any;
});
__setChatTransportForTests(opts.chat ?? (async (chatOpts) => {
const chunk = extractChunk(chatOpts);
await delay(opts.delayForChunk?.(chunk) ?? 1, chatOpts.abortSignal);
return chatSuccess(`Synopsis for ${chunk}`);
}));
const engine = makeServiceEngine(opts.chunks);
const result = await reembedPageWithContextualRetrieval({
engine,
pageSlug: 'wiki/concepts/concurrency-test',
sourceId: 'default',
globalMode: 'per_chunk_synopsis',
chunkConcurrency: opts.concurrency,
abortSignal: opts.abortSignal,
...(opts.acquireSynopsisLease && { acquireSynopsisLease: opts.acquireSynopsisLease }),
...(opts.releaseSynopsisLease && { releaseSynopsisLease: opts.releaseSynopsisLease }),
});
return {
result,
embedInputs: embedInputs.flat(),
embeddedChunks: engine.embeddedChunks as ChunkInput[],
};
}
function makeServiceEngine(chunks: ChunkInput[]) {
const engine: any = {
embeddedChunks: [] as ChunkInput[],
async getPage() {
return {
id: 1,
slug: 'wiki/concepts/concurrency-test',
source_id: 'default',
type: 'concept',
title: 'Concurrency Test',
compiled_truth: chunks.map((c) => c.chunk_text).join('\n\n'),
timeline: '',
frontmatter: {},
created_at: new Date('2026-01-01T00:00:00Z'),
updated_at: new Date('2026-01-01T00:00:00Z'),
deleted_at: null,
};
},
async executeRaw() {
return [{
id: 'default',
name: 'Default',
local_path: null,
last_commit: null,
last_sync_at: null,
config: {},
created_at: new Date('2026-01-01T00:00:00Z'),
contextual_retrieval_mode: null,
trust_frontmatter_overrides: false,
}];
},
async getChunks() {
return chunks;
},
async transaction(fn: (tx: any) => Promise<void>) {
await fn({
upsertChunks: async (_slug: string, embedded: ChunkInput[]) => {
engine.embeddedChunks = embedded;
},
updatePageContextualRetrievalState: async () => {},
});
},
async updatePageContextualRetrievalState() {},
};
return engine;
}
function extractChunk(opts: ChatOpts): string {
const content = String(opts.messages[0]?.content ?? '');
return content.match(/<chunk>\n([\s\S]*?)\n<\/chunk>/)?.[1] ?? '';
}
function chatSuccess(text: string): ChatResult {
return {
text,
blocks: [],
stopReason: 'end',
usage: {
input_tokens: 0,
output_tokens: 0,
cache_read_tokens: 0,
cache_creation_tokens: 0,
},
model: 'stub:chat',
providerId: 'stub',
};
}
function delay(ms: number, signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(abortError());
return;
}
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(abortError());
}, { once: true });
});
}
function abortError(): Error {
const err = new Error('aborted');
err.name = 'AbortError';
return err;
}