fix(wiki): cap graph reference fields to prevent prompt overflow

generateLeafPage assembled the MODULE prompt with truncated SOURCE_CODE
but uncapped INTRA_CALLS/OUTGOING_CALLS/INCOMING_CALLS/PROCESSES. In
large clusters with many cross-references, the graph data alone can
re-explode the assembled prompt past the model's input context (or hit
the output max_completion_tokens cap on smaller models like
gemini-2.5-flash with 8K output), causing deterministic module-generation
failures even with --force.

Symptom (browser-harness, 20 clusters): "Project Documentation" and
"Browser Interaction Skills" modules fail deterministically across
gemini-2.5-flash (740s, 19 page, 2 fail) and gpt-5-chat-latest (2804s,
18 page, 3 fail). SOURCE_CODE truncation alone is insufficient when
the cluster has many edges.

Fix:
- Extract generic truncateText helper from truncateSource (label-aware
  notice for clarity)
- Add DEFAULT_GRAPH_FIELD_BUDGET = 5_000 (≈1/6 of module budget)
- Apply per-field caps to INTRA/OUTGOING/INCOMING calls + PROCESSES
  in generateLeafPage

Tests: existing 51 wiki unit tests pass unchanged.
This commit is contained in:
ChasLui 2026-05-11 13:30:01 +08:00
parent d69eadfb7f
commit ca44e80c05
No known key found for this signature in database

View file

@ -94,6 +94,14 @@ export interface WikiRunResult {
// ─── Constants ────────────────────────────────────────────────────────
const DEFAULT_MAX_TOKENS_PER_MODULE = 30_000;
/**
* Per-field token cap for graph reference data (INTRA/OUTGOING/INCOMING/PROCESSES).
* Each field gets ~1/6 of the module budget. Without these caps, a large cluster
* with many cross-references can push the assembled prompt beyond the model's
* input or output context (e.g. gemini-2.5-flash 8K output cap), causing
* deterministic failures even after SOURCE_CODE itself is truncated.
*/
const DEFAULT_GRAPH_FIELD_BUDGET = 5_000;
const WIKI_DIR = 'wiki';
// ─── Generator Class ──────────────────────────────────────────────────
@ -579,13 +587,26 @@ export class WikiGenerator {
getProcessesForFiles(filePaths, 5),
]);
// Cap each graph-reference field independently. Even when SOURCE_CODE is
// already truncated to maxTokensPerModule, uncapped call edges + processes
// can re-explode the assembled prompt past the model's context (or hit the
// output max_completion_tokens cap for smaller models like gemini-2.5-flash).
const graphBudget = DEFAULT_GRAPH_FIELD_BUDGET;
const prompt = fillTemplate(MODULE_USER_PROMPT, {
MODULE_NAME: node.name,
SOURCE_CODE: finalSourceCode,
INTRA_CALLS: formatCallEdges(intraCalls),
OUTGOING_CALLS: formatCallEdges(interCalls.outgoing),
INCOMING_CALLS: formatCallEdges(interCalls.incoming),
PROCESSES: formatProcesses(processes),
INTRA_CALLS: this.truncateText(formatCallEdges(intraCalls), graphBudget, 'intra-module calls'),
OUTGOING_CALLS: this.truncateText(
formatCallEdges(interCalls.outgoing),
graphBudget,
'outgoing calls',
),
INCOMING_CALLS: this.truncateText(
formatCallEdges(interCalls.incoming),
graphBudget,
'incoming calls',
),
PROCESSES: this.truncateText(formatProcesses(processes), graphBudget, 'execution flows'),
});
const response = await this.invokeLLM(prompt, MODULE_SYSTEM_PROMPT, this.streamOpts(node.name));
@ -880,10 +901,18 @@ export class WikiGenerator {
}
private truncateSource(source: string, maxTokens: number): string {
return this.truncateText(source, maxTokens, 'source');
}
/**
* Generic token-budget truncation for prompt fields. Used by both SOURCE_CODE
* and graph reference data (INTRA/OUTGOING/INCOMING calls + PROCESSES).
*/
private truncateText(text: string, maxTokens: number, label = 'data'): string {
// Rough truncation: keep first maxTokens*4 chars and add notice
const maxChars = maxTokens * 4;
if (source.length <= maxChars) return source;
return source.slice(0, maxChars) + '\n\n... (source truncated for context window limits)';
if (text.length <= maxChars) return text;
return text.slice(0, maxChars) + `\n\n... (${label} truncated for context window limits)`;
}
private async estimateModuleTokens(filePaths: string[]): Promise<number> {