fix(truncation): priorStages slice 한도가 너무 짧아 review 가 잘린 코드 받던 버그

자기야 발견: 다랑이 review 결과에 "코드가 끊긴다" 가 반복 등장. 다랑이의
실제 review 메시지를 확인해 보니 narang 이 만든 todo-debug-mode.html 의
<script> 가 `const form = document.getElementById(` 에서 잘려서 반복 round
마다 같은 잘림 지점을 지적했음.

원인 추적:
- narang junior 의 LLM 출력 원본 (junior-01-*.md) 은 깔끔하게 </html> 로
  종료. LLM 자체는 멀쩡.
- spawn.ts buildSuccessResult 의 summary slice(0, 2000) 가 1차로 자름
- 그 위 aggregated slice(0, 6000) 가 0차로 자름
- runner extractStageText 의 review issues slice(0, 2000) 도 추가 한도
- LLM-emit reason 도 spawn.ts parseReviewVerdict 에서 1000/800 자로 잘림

todo HTML 한 파일이 5KB 정도 되니 6000 자 한도에서 3분의 1 잘려나감 →
darang 은 결과적으로 절반짜리 코드를 받음 → 영원히 REQUEST_CHANGES.

수정 (모두 LLM context 200k+ 안에서 안전한 한도):
- spawn.ts aggregated: 6_000 → 64_000
- spawn.ts buildSuccessResult.summary: 2_000 → 64_000
- spawn.ts parseReviewVerdict reason: 1_000 → 16_000
- spawn.ts parseDeployVerdict reason: 800/1000 → 16_000
- runner.ts extractStageText review issues: 2_000 → 32_000

검증: 다음 실행에서 darang 이 동일한 잘림 지점을 지적하지 않으면 OK.
This commit is contained in:
2026-04-11 01:16:49 +09:00
parent 6627ad709f
commit ae2d4b1d3e
2 changed files with 21 additions and 8 deletions

View File

@@ -223,10 +223,16 @@ export async function executeInvocation(
});
}
// Aggregate manager + all child outputs into a single text blob that
// gets passed to the next stage as priorStages. The downstream agent
// (especially the reviewer) needs to see ACTUAL CODE — not a snippet
// — to make a meaningful judgement, so the cap is generous. Cap is
// sized for full HTML/JS/CSS files; LLM context windows are 200k+ so
// 64KB stays well inside budget even after 4 stages of accumulation.
const aggregated = [managerWork.text, ...childTexts]
.filter(Boolean)
.join("\n\n---\n\n")
.slice(0, 6000);
.slice(0, 64_000);
return buildSuccessResult(
req.stage,
@@ -540,10 +546,10 @@ function parseReviewVerdict(text: string): {
// If both APPROVE and REQUEST_CHANGES appear, the LLM is uncertain —
// bias toward REQUEST_CHANGES so problems aren't silently ignored.
if (abortIdx >= 0 && (rcIdx < 0 || abortIdx < rcIdx)) {
return { verdict: "ABORT", reason: text.slice(0, 1000) };
return { verdict: "ABORT", reason: text.slice(0, 16_000) };
}
if (rcIdx >= 0) {
return { verdict: "REQUEST_CHANGES", reason: text.slice(0, 1000) };
return { verdict: "REQUEST_CHANGES", reason: text.slice(0, 16_000) };
}
if (approveIdx >= 0) {
return { verdict: "APPROVE", reason: "" };
@@ -551,7 +557,7 @@ function parseReviewVerdict(text: string): {
// No marker found — conservatively request changes rather than auto-approve
return {
verdict: "REQUEST_CHANGES",
reason: "Reviewer did not emit an APPROVE / REQUEST_CHANGES marker. Raw text:\n" + text.slice(0, 800),
reason: "Reviewer did not emit an APPROVE / REQUEST_CHANGES marker. Raw text:\n" + text.slice(0, 16_000),
};
}
@@ -574,7 +580,7 @@ function parseDeployVerdict(text: string): {
const doneIdx = upper.lastIndexOf("DEPLOY_DONE");
// Take the LAST marker (the prompt asks for it on the final line)
if (failedIdx > doneIdx) {
return { verdict: "DEPLOY_FAILED", reason: text.slice(0, 1000) };
return { verdict: "DEPLOY_FAILED", reason: text.slice(0, 16_000) };
}
if (doneIdx >= 0) {
return { verdict: "DEPLOY_DONE", reason: "" };
@@ -584,7 +590,7 @@ function parseDeployVerdict(text: string): {
verdict: "DEPLOY_FAILED",
reason:
"Deployer did not emit a DEPLOY_DONE / DEPLOY_FAILED marker. Raw text:\n" +
text.slice(0, 800),
text.slice(0, 16_000),
};
}
@@ -601,7 +607,10 @@ function buildSuccessResult(
} | null,
producedFiles: string[] = [],
): HandoffMessage {
const summary = outputText?.slice(0, 2000) ?? "";
// The summary is the payload the next stage will see as priorStages
// text. Reviewer needs to see actual code, not a snippet, so the cap
// matches the upstream aggregation (64KB).
const summary = outputText?.slice(0, 64_000) ?? "";
switch (stage) {
case "plan":
return {

View File

@@ -348,7 +348,11 @@ function extractStageText(h: HandoffMessage): string {
}
case "review": {
if (h.payload?.issues && h.payload.issues.length > 0) {
return `Review ${h.verdict}: ${JSON.stringify(h.payload.issues).slice(0, 2000)}`;
// Generous cap so the next implement loop sees the full reviewer
// critique (not just the first 2KB). Reviewer reason text can be
// multiple paragraphs and the implement junior needs all of it
// to fix the right things.
return `Review ${h.verdict}: ${JSON.stringify(h.payload.issues).slice(0, 32_000)}`;
}
return `Review verdict: ${h.verdict}`;
}