mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a23e8319e |
@@ -391,12 +391,17 @@ class ProposeTakesPhase extends BaseCyclePhase {
|
||||
// Write proposals to take_proposals. Each row is a separate INSERT
|
||||
// because the composite idempotency key is on the per-page tuple — a
|
||||
// bulk UPSERT would collapse a same-page-multi-claim run into one row.
|
||||
for (const p of proposals) {
|
||||
if (proposals.length === 0) {
|
||||
// #2106 negative cache: insert a sentinel row when the extractor
|
||||
// found nothing, so the idempotency check skips this page on future
|
||||
// cycles. Without it, pages with no gradeable claims (the common
|
||||
// case) re-spend LLM tokens every cycle. Same composite key rules:
|
||||
// an edited page (new content_hash) or a prompt bump re-extracts.
|
||||
await engine.executeRaw(
|
||||
`INSERT INTO take_proposals
|
||||
(source_id, page_slug, content_hash, prompt_version, proposal_run_id,
|
||||
claim_text, kind, holder, weight, domain, dedup_against_fence_rows, model_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
VALUES ($1, $2, $3, $4, $5, '__no_proposals__', 'take', 'brain', 0, NULL, $6, $7)
|
||||
ON CONFLICT (source_id, page_slug, content_hash, prompt_version) DO NOTHING`,
|
||||
[
|
||||
sourceId,
|
||||
@@ -404,16 +409,35 @@ class ProposeTakesPhase extends BaseCyclePhase {
|
||||
ch,
|
||||
promptVersion,
|
||||
proposalRunId,
|
||||
p.claim_text,
|
||||
p.kind,
|
||||
p.holder,
|
||||
p.weight,
|
||||
p.domain ?? null,
|
||||
JSON.stringify(existingTakes),
|
||||
modelId,
|
||||
],
|
||||
);
|
||||
result.proposals_inserted += 1;
|
||||
} else {
|
||||
for (const p of proposals) {
|
||||
await engine.executeRaw(
|
||||
`INSERT INTO take_proposals
|
||||
(source_id, page_slug, content_hash, prompt_version, proposal_run_id,
|
||||
claim_text, kind, holder, weight, domain, dedup_against_fence_rows, model_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
ON CONFLICT (source_id, page_slug, content_hash, prompt_version) DO NOTHING`,
|
||||
[
|
||||
sourceId,
|
||||
page.slug,
|
||||
ch,
|
||||
promptVersion,
|
||||
proposalRunId,
|
||||
p.claim_text,
|
||||
p.kind,
|
||||
p.holder,
|
||||
p.weight,
|
||||
p.domain ?? null,
|
||||
JSON.stringify(existingTakes),
|
||||
modelId,
|
||||
],
|
||||
);
|
||||
result.proposals_inserted += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -334,6 +334,22 @@ New prose appended here.`;
|
||||
expect((details.warnings as string[])[0]).toContain('LLM timeout');
|
||||
});
|
||||
|
||||
test('zero-proposal page writes a __no_proposals__ sentinel so the next cycle cache-hits (#2106)', async () => {
|
||||
const pages = [buildPage({ slug: 'wiki/no-claims', body: 'reference prose with nothing takeable' })];
|
||||
const { engine, captured } = buildMockEngine({ pages });
|
||||
const extractor: ProposeTakesExtractor = async () => [];
|
||||
const result = await runPhaseProposeTakes(buildCtx(engine), { extractor });
|
||||
|
||||
expect(result.status).toBe('ok');
|
||||
const details = result.details as Record<string, unknown>;
|
||||
expect(details.proposals_inserted).toBe(0); // sentinel is not a proposal
|
||||
const inserts = captured.filter(c => c.sql.includes('INSERT INTO take_proposals'));
|
||||
expect(inserts).toHaveLength(1);
|
||||
expect(inserts[0]!.sql).toContain("'__no_proposals__'");
|
||||
expect(inserts[0]!.sql).toContain('ON CONFLICT (source_id, page_slug, content_hash, prompt_version)');
|
||||
expect(inserts[0]!.params).toHaveLength(7); // model_id rides as the last param
|
||||
});
|
||||
|
||||
test('pages with empty compiled_truth are skipped silently (no extractor call)', async () => {
|
||||
const pages = [
|
||||
buildPage({ slug: 'wiki/empty', body: '' }),
|
||||
|
||||
Reference in New Issue
Block a user