From 4398150bdc6860ba173e4e4a7862d24d98f2aa70 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 15:12:15 +0000 Subject: [PATCH] fix(cli): defer finish-reason default until after compact branch Setting handle.message.finish before the result === "compact" branch broke guardCompactionAttempt (which fills in "error" with ??=) and the overflow detection at compaction.create. Move the default to right before "continue" and gate it on result !== "compact" so the compaction paths see the original undefined value. --- packages/opencode/src/session/prompt.ts | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 5d79e000645..ca5549d13ff 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1600,21 +1600,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the toolChoice: format.type === "json_schema" ? "required" : undefined, }) - // kilocode_change start - guard against providers that end the stream - // without a terminal stop_reason (e.g. an Anthropic-style message_delta - // with stop_reason: null followed immediately by message_stop). Without - // a finishReason, the loop-exit check at the top of the next iteration - // sees a falsy `finish` (loaded from storage via filterCompactedEffect) - // and keeps stepping forever. Default to "unknown" and persist so the - // regular break condition fires when there are no tool calls — the - // tool-call path is unaffected because `!hasToolCalls` keeps the break - // gated when the assistant did emit tool calls. - if (!handle.message.finish) { - handle.message.finish = "unknown" - yield* sessions.updateMessage(handle.message) - } - // kilocode_change end - if (structured !== undefined) { handle.message.structured = structured handle.message.finish = handle.message.finish ?? "stop" @@ -1672,6 +1657,21 @@ NOTE: At any point in time through this workflow you should feel free to ask the return "break" as const } // kilocode_change end + // kilocode_change start - guard against providers that end the stream + // without a terminal stop_reason (e.g. an Anthropic-style message_delta + // with stop_reason: null followed immediately by message_stop). Without + // a finishReason, the loop-exit check at the top of the next iteration + // sees a falsy `finish` (loaded from storage via filterCompactedEffect) + // and keeps stepping forever. Default to "unknown" and persist so the + // regular break condition fires when there are no tool calls. Skipped + // for the compact path so guardCompactionAttempt can still fill in + // "error" on exhaustion. Tool-call turns already get "tool-calls" from + // the AI SDK; even without it, !hasToolCalls keeps the break gated. + if (result !== "compact" && !handle.message.finish) { + handle.message.finish = "unknown" + yield* sessions.updateMessage(handle.message) + } + // kilocode_change end return "continue" as const }).pipe(Effect.ensuring(instruction.clear(handle.message.id))) if (outcome === "break") break