v1.13.15-codecontext-synth: forced second-inference synthesis for codecontext overview tools
After a codecontext overview-class tool call lands (get_codebase_overview, get_framework_analysis, get_semantic_neighborhoods), the pipeline runs a second inference pass that replaces the recursive runAssistantTurn. The synth pass auto-fetches the top-N source files referenced in the codecontext output plus project docs (BOOCHAT.md, AGENTS.md, *roadmap*.md, CONTEXT.md), applies a 32k-token budget with explicit drop-priority, and streams a structured response that grounds the model in real load-bearing code rather than relying on the codecontext summary alone. Smoke #1 (default) and #2 (Architect) both cite the correct inference/turn.ts + tool-phase.ts + stream-phase.ts files; smoke #6 (fault injection) verifies the fall-through path marks the synth message status='failed' and yields cleanly to the recursive turn. ## Truncation-aware extraction codecontext's wrapper inline-truncates results at 32k chars. Without the expansion step, the top-N file selection only saw the alphabetical head of the codebase (apps/booterm/dist/*) and auto-fetched the wrong sources. The pipeline now calls in-process readTruncation(outputPath) before extracting referenced files, so top-N selection sees the full 80k+ char output. The 32k truncated head still ships to the synth model — the expansion is reference-extraction-only, preserving the token-budget contract. Graceful degradation on readTruncation null/throw: log warn, fall back to the truncated head. ## Schema deviation from dispatch The dispatch claimed no schema migration was needed for the new 'synthesis' part kind. Reality: message_parts.kind has an explicit CHECK constraint (schema.sql:54) that would reject the new value. Added a DROP CONSTRAINT IF EXISTS + DO $$ pg_constraint idempotency-guarded re-add matching the CLAUDE.md migration pattern. The inline CREATE TABLE constraint also updated so fresh installs land with the extended enum. ## User-abort marks synth-message failed Deviation from review-time spec ("user-abort path does NOT mark the message failed"). The outer abort handler in error-handler.ts operates on the parent turn's assistantMessageId, not the new synth row that runSynthesisPass created. Without explicit marking, the synth row would sit in status='streaming' until the 5-min stale-streaming sweeper (v1.13.1-cleanup-bundle), tripping the frontend's 60s no-token-activity banner in the meantime — exactly the UX bug class the v1.13.1 sweeper was added to handle. Marking failed on every catch path (including user-abort) closes the gap. Cost: one extra DB write + one publish on the rare user-abort-during-synth path. ## Race-safe synth-tool capture tool-phase.ts uses synthEntries: Array<{tc, output, error?}> with per-callback push under Promise.all. find() picks the first non-error entry by call-order (toolCalls array index). Multiple synth-tools in one batch are uncommon but handled deterministically. ## Roadmap rebase Updated boocode_roadmap.md retrospective section + cleanup-order tracker + schema-changes summary to use the new vMAJOR.MINOR.PATCH-slug tag names per the 2026-05-22 retag (CHANGELOG.md is the canonical record). v1.13.15 listed as "this batch, tag pending"; a one-line follow-up commit will remove that qualifier after the tag lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,11 @@ import type {
|
||||
// the reference is read at call time (inside an async function body), not
|
||||
// at module top-level. Node + tsc resolve this cleanly.
|
||||
import { runAssistantTurn } from './turn.js';
|
||||
// v1.13.13: synthesis pipeline — replaces the immediate recursive turn when
|
||||
// any of this batch's tool calls is in SYNTHESIS_TOOLS. Falls through to
|
||||
// recursion on synthesis failure (timeout / model error). See module header
|
||||
// in synthesisPipeline.ts for the auto-fetch + token-budget rules.
|
||||
import { SYNTHESIS_TOOLS, runSynthesisPass } from '../synthesisPipeline.js';
|
||||
|
||||
async function executeToolCall(
|
||||
projectRoot: string,
|
||||
@@ -155,6 +160,12 @@ export async function executeToolPhase(
|
||||
// batches still execute the other tools normally.
|
||||
ctx.publishUser({ type: 'chat_status', chat_id: chatId, status: 'tool_running', at: new Date().toISOString() });
|
||||
let pausingForUserInput = false;
|
||||
// v1.13.13: capture synth-tool result text so the synthesis pipeline below
|
||||
// doesn't have to re-fetch from DB. Array (not single) because a batch
|
||||
// could theoretically include multiple synthesis tools — we take the first
|
||||
// for the synthesis input. Race-free under Promise.all because each
|
||||
// callback pushes its own captured value.
|
||||
const synthEntries: Array<{ tc: ToolCall; output: unknown; error?: string }> = [];
|
||||
await Promise.all(
|
||||
toolCalls.map(async (tc) => {
|
||||
const [toolRow] = await ctx.sql<{ id: string }[]>`
|
||||
@@ -186,6 +197,9 @@ export async function executeToolPhase(
|
||||
return;
|
||||
}
|
||||
const tres = await executeToolCall(projectRoot, tc);
|
||||
if (SYNTHESIS_TOOLS.has(tc.name)) {
|
||||
synthEntries.push({ tc, output: tres.output, ...(tres.error ? { error: tres.error } : {}) });
|
||||
}
|
||||
const stored = {
|
||||
tool_call_id: tc.id,
|
||||
output: tres.output,
|
||||
@@ -233,6 +247,41 @@ export async function executeToolPhase(
|
||||
return;
|
||||
}
|
||||
|
||||
// v1.13.13: synthesis-pipeline branch. When any of this batch's tool calls
|
||||
// is a codecontext overview/analysis tool that produced a non-error result,
|
||||
// run a forced second-inference synthesis pass with auto-fetched files +
|
||||
// project docs instead of the normal recursive runAssistantTurn. Falls
|
||||
// through to the recursive call on synthesis failure (timeout, model
|
||||
// error). User-abort re-throws so the outer handler runs.
|
||||
const synthEntry = synthEntries.find((e) => !e.error && e.output != null);
|
||||
if (synthEntry) {
|
||||
// codecontext wrappers return { result: string, truncated: boolean, ... }.
|
||||
// Defensive: stringify the output if it isn't the expected shape so the
|
||||
// synthesis still has something to chew on rather than crashing on
|
||||
// missing `.result`.
|
||||
const out = synthEntry.output as { result?: unknown; truncated?: boolean; outputPath?: string };
|
||||
const toolResultText =
|
||||
typeof out?.result === 'string'
|
||||
? out.result
|
||||
: JSON.stringify(synthEntry.output);
|
||||
// v1.13.15-b: forward the wrapper's truncation flag + opaque tmpfs id so
|
||||
// synthesisPipeline can re-read the full content for reference extraction.
|
||||
const ran = await runSynthesisPass({
|
||||
ctx,
|
||||
args,
|
||||
session,
|
||||
projectRoot,
|
||||
toolName: synthEntry.tc.name,
|
||||
toolResultText,
|
||||
...(typeof out?.truncated === 'boolean' ? { truncated: out.truncated } : {}),
|
||||
...(typeof out?.outputPath === 'string' ? { outputPath: out.outputPath } : {}),
|
||||
});
|
||||
if (ran) return;
|
||||
// ran === false → synthesis failed (timeout / model error) → fall through
|
||||
// to the standard recursive turn below. The synth message (if created)
|
||||
// was already marked status='failed' inside runSynthesisPass.
|
||||
}
|
||||
|
||||
const [nextAssistant] = await ctx.sql<{ id: string }[]>`
|
||||
INSERT INTO messages (session_id, chat_id, role, content, status, created_at)
|
||||
VALUES (${sessionId}, ${chatId}, 'assistant', '', 'streaming', clock_timestamp())
|
||||
|
||||
Reference in New Issue
Block a user