Skip to content

fix: 19 correctness bugs from deep codebase review - #268

Merged
claudiusthebot merged 2 commits into
mainfrom
claude/eager-sagan-Fv6mV
Jun 6, 2026
Merged

fix: 19 correctness bugs from deep codebase review#268
claudiusthebot merged 2 commits into
mainfrom
claude/eager-sagan-Fv6mV

Conversation

@dylanneve1

Copy link
Copy Markdown
Owner

Summary

Deep review of the full codebase (all ~250 source files) by six parallel static-analysis passes surfaced 19 confirmed bugs. All are fixed here; all 2998 tests pass after.


Critical / High

sessions.ts β€” fastestResponseMs permanently stuck at null after restart

Infinity serialises to null in JSON (JSON.stringify({x: Infinity}) β†’ {"x":null}). The migration guard at startup only checked === undefined and === 0, so after any process restart fastestResponseMs was loaded as null and stayed there forever β€” durationMs < null is always false in JS. Added === null to the guard.

claude-sdk/stream.ts β€” thinking_delta callback passed response text, not thinking text

onStreamDelta(state.currentBlockText, "thinking") fired with currentBlockText β€” which accumulates response text deltas, not thinking deltas. Thinking content was silently dropped. Added a currentThinkingText field to StreamState, accumulate thinking_delta.thinking into it, and pass that to the callback.

claude-sdk/handler.ts β€” session name overwritten with flow-violation reminder on first retry

session.turns === 0 is still true on the first flow-violation retry (because incrementTurns hasn't fired yet). The recursive call passes text: violation.reminder, so extractSessionName(violation.reminder) would produce a session name like "[FLOW VIOLATION] Your previous...". Guard with !_internal.flowRetries.

kilo/models.ts β€” isFreeModel uses || instead of && for cost check

// BEFORE (bug): a model with costInput=0 but costOutput=$5 is labelled free
model.costInput === 0 || model.costOutput === 0
// AFTER (fix):
(model.costInput === 0 && model.costOutput === 0)

openai-agents/discovery.ts β€” discoveryAt not set on failed discovery

state.discoveryAt = Date.now() was only in the .then() path. On a network failure, discoveryAt stayed null, causing hasAttemptedDiscovery() to return false on every subsequent call and awaitDiscovery() to incur a 3-second soft timeout on every model-picker open when the endpoint is unreachable. Moved to .finally().

telegram/callbacks.ts β€” ctx.from.id crashes on channel post callbacks

Channel posts have no from field β€” ctx.from is undefined for them. Added optional chaining: ctx.from?.id.

telegram/formatting.ts β€” Markdown link label not HTML-escaped

// BEFORE (bug): [<b>bold</b>](url) β†’ <a href="…"><b>bold</b></a>  (HTML injection)
`<a href="${escapeHtml(url)}">${text}</a>`
// AFTER (fix):
`<a href="${escapeHtml(url)}">${escapeHtml(text)}</a>`

Medium

discord/actions.ts β€” edit_message guard allowed 4000-char edits

The early-rejection guard compared against DISCORD_MAX_TEXT * 2 (4000), but Discord's API limit for edits is DISCORD_MAX_TEXT (2000). Text between 2001–4000 chars passed the guard but was silently truncated at the send step. Changed the guard to DISCORD_MAX_TEXT.

cli.ts β€” isConfigured always returned false for Discord frontend

// BEFORE: default case catches Discord β†’ always returns false
return false;
// AFTER:
if (fe === "discord") return !!config.discord?.botToken;
return false;

errors.ts β€” bare overflow regex too broad

/overflow/i matched Maximum call stack size exceeded, numeric overflow, buffer overflows, etc., silently misclassifying them as non-retryable context_length errors and suppressing retries that might otherwise succeed. Tightened to context.{0,10}overflow so only context-related overflow messages match.

cron.ts β€” warnedBadSchedule.clear() caused a re-warning burst at the cap

When the 200-entry cap was hit, clear() removed all entries. On the very next tick, every previously-warned job was no longer in the set and re-logged a warning β€” producing a burst of 200 log lines. Now evicts only the oldest entry via Set insertion-order iteration.

gateway.ts β€” Number("") === 0 could match chat context 0

When _chatId is absent, rawChatId = "" and Number("") === 0. !isNaN(0) is true, so if any chat with numeric ID 0 ever appeared in chatContexts, a bare tool call with no _chatId would route to it. The explicit-routing branch already guards with rawChatId !== ""; added the same guard to the ambient-context branch.

dream.ts β€” dreaming lock held forever if backend ignores abort signal

await agentPromise.catch(() => {}) with no timeout meant a backend that ignored its abort signal would block executeDream indefinitely. Since dreaming = false is in executeDream's finally block, it would never reset, silently killing all future dream runs until a process restart. Added a DREAM_ABORT_GRACE_MS (30 s) bounded wait, mirroring the pattern in heartbeat.ts.

remote-server/lifecycle.ts β€” health-check fetch had no timeout

A server that accepted TCP connections but never sent an HTTP response would stall reuseExistingServer β€” and therefore ensureRemoteServer, called on every chat turn β€” indefinitely. Added AbortSignal.timeout(5_000).

opencode/model-provider.ts β€” getProviderModels was 0-indexed while all other backends use 1-indexed

page = 0 with start = page * pageSize meant a caller passing page=1 (the standard from other backends) skipped the first pageSize models entirely. Changed to page = 1 / start = (page - 1) * pageSize.


Low

triggers.ts β€” commandForBash() re-probed bash on every trigger spawn

spawnSync(cmd, ["-lc", "exit 0"]) was called synchronously on the hot path of every bash trigger invocation. The result is the same every time (bash either exists or it doesn't). Cached it in a module-level variable after the first probe.

kilo/handler.ts / opencode/handler.ts β€” redundant dynamic import() of setSessionId

Both files already statically import from ../../storage/sessions.js. setSessionId was the one export not included in the static import, causing a await import(...) at runtime. Added setSessionId to the static import and removed the dynamic call.

remote-server/session-helpers.ts β€” confusing variable name messageId for the info blob

The intermediate variable was named messageId but held message.info (the whole info object). Renamed to messageInfo to match what it actually holds and eliminate a misleading hint that it contained just an ID.


Test plan

  • npm test β€” 2998 tests pass, 34 skipped (same skipped count as main)
  • npm run typecheck β€” clean

https://claude.ai/code/session_01M8utiWqUBerWtQ45jdeXgH


Generated by Claude Code

@claudiusthebot claudiusthebot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 19 bugs verified β€” fixes correct. Pushed a prettier commit (421d63b) on top to clear the Code Quality check (3 files: stream.ts, errors.ts, gateway.ts).

Highlights worth noting:

  • thinking_delta β†’ currentThinkingText fix is the sharpest one: the old code passed currentBlockText (response accumulator) to the thinking callback, meaning thinking content was silently dropped in real time. Clean fix.
  • isFreeModel || β†’ && is a correctness trap: any model with free input but paid output (or vice versa) would have been labelled free. Good catch.
  • dreaming lock bounded wait is the right shape β€” mirrors the heartbeat pattern. A backend ignoring abort indefinitely would have killed the dream loop permanently.
  • fastestResponseMs === null guard: Infinity β†’ null in JSON round-trips is a classic JS footgun. Good.

Potential merge-order note:

  • src/backend/remote-server/session-helpers.ts is also touched by PR #260 (adds id?: string to RemoteAssistantInfo). The changes are on nearby but different lines β€” should merge cleanly, but worth a rebase check if #260 lands first.
  • src/core/cron.ts is also touched by PR #252 (the timer! non-null assertion fix on a different function). Same situation β€” different lines, should be fine.
  • src/frontend/telegram/formatting.ts is also touched by PR #252 (URL double-escaping fix, different line). These two fixes are complementary β€” both should land.

CI will re-run on the prettier commit. 33/34 checks were green already; Code Quality should flip to βœ… shortly.

@claudiusthebot

Copy link
Copy Markdown
Collaborator

Merge order note (from heartbeat #564, 07:45Z)

8 open PRs, some with overlapping fixes. Recommended merge sequence:

  1. fix: 19 correctness bugs from deep codebase reviewΒ #268 (this PR β€” 19 bugs) β€” no conflicts with any other open PR; all 19 fixes are distinct. 34/34 βœ…, approved. Land first.
  2. fix: thread fallback model through params instead of setChatModelΒ #265 (model-wipe via params.model) β€” cleanest fix for the critical setChatModel/modelByBackend wipe. 34/34 βœ…. After fix: 19 correctness bugs from deep codebase reviewΒ #268 merges, rebase needed but no conflict.
  3. fix(agent-runtime): three correctness bugs in adapter and legacy-bridgeΒ #263 (3 agent-runtime bugs) β€” after fix: thread fallback model through params instead of setChatModelΒ #265 merges, drop Bug 3 (dead saw variable, same fix as fix: thread fallback model through params instead of setChatModelΒ #265 Bug 3). Small rebase.
  4. fix: five correctness bugs from deep code reviewΒ #252 (5 deep audit bugs) β€” standalone, no known conflicts with the above. Will need rebase.
  5. feat(agent-runtime): finish architecture unification plan (Phases 3-7)Β #258 (arch unification phases 3-7) β€” large structural PR (+4283/-187). Should come after correctness fixes land.
  6. fix: four correctness bugs from deep code reviewΒ #257 (4 bugs) β€” Bugs 1+2 overlap with fix: thread fallback model through params instead of setChatModelΒ #265 Bug 1. After fix: thread fallback model through params instead of setChatModelΒ #265 merges, this needs significant rebase or can be closed if fix: thread fallback model through params instead of setChatModelΒ #265 covers the same ground.
  7. fix: five correctness bugs (model-wipe, infinite retry loop, type gap)Β #260 (5 bugs) β€” Bug 1 overlaps fix: thread fallback model through params instead of setChatModelΒ #265. If fix: thread fallback model through params instead of setChatModelΒ #265 lands first, this needs rebase to drop Bug 1.
  8. fix: flag intervening group context before vague repliesΒ #251 (group context feature) β€” standalone feature, oldest open PR (May 23). Needs rebase to current main.

TL;DR: #268 β†’ #265 β†’ #263 (rebase) β†’ #252 β†’ #258 β†’ close/rebase #257+#260

PRs #257 and #260 both fix the same setChatModel model-wipe as #265 but with slightly different approaches. Once #265 lands, those two lose their main unique fix and may not be worth rebasing β€” you'd be landing the openai-agents infinite retry loop fix (#260 Bug 2) and the mcp-launcher batch response fix (#257 Bug 4) as the only remaining unique items. Those could be cherry-picked into a single small follow-up PR if you want.

claude and others added 2 commits June 6, 2026 15:18
## Critical / High

**`sessions.ts` β€” `fastestResponseMs` permanently stuck after restart**
`Infinity` serializes to `null` in JSON. The migration guard only checked
`=== undefined` and `=== 0`, so after any restart the field stayed `null`
and was never updated again (`durationMs < null` is always false).
Added `=== null` to the guard.

**`claude-sdk/stream.ts` β€” `thinking_delta` sent wrong text to callback**
`onStreamDelta(state.currentBlockText, "thinking")` passed the accumulated
*response* text rather than the thinking text. Added `currentThinkingText`
to `StreamState`, accumulate thinking deltas there, and pass it to the
callback.

**`claude-sdk/handler.ts` β€” session name overwritten on flow-violation retry**
`session.turns === 0` is still true on the first retry (incrementTurns
hasn't fired). The recursive call with `text = violation.reminder` would
extract a session name from the reminder string. Guard with
`!_internal.flowRetries`.

**`kilo/models.ts` β€” `isFreeModel` uses `||` instead of `&&`**
A model with one zero cost and one non-zero cost was incorrectly labeled
free. Changed cost check to `(costInput === 0 && costOutput === 0)`.

**`openai-agents/discovery.ts` β€” `discoveryAt` not set on failed discovery**
Only set in `.then()`, not `.finally()`. On a network failure, `discoveryAt`
stayed `null` so `awaitDiscovery()` was called again on every model-picker
open, each time incurring a 3-second soft timeout. Moved to `.finally()`.

**`telegram/callbacks.ts` β€” `ctx.from.id` crashes on channel post callbacks**
Channel posts have no `from` field. Added optional chaining: `ctx.from?.id`.

**`telegram/formatting.ts` β€” link label text not HTML-escaped**
`[<b>bold</b>](url)` would inject raw HTML into Telegram messages.
Applied `escapeHtml()` to the link label as well as the URL.

## Medium

**`discord/actions.ts` β€” `edit_message` guard allowed 4 000-char edits**
The guard compared against `DISCORD_MAX_TEXT * 2` (4000) but Discord's
actual edit limit is 2000. Changed to `DISCORD_MAX_TEXT`.

**`cli.ts` β€” `isConfigured` always returned `false` for Discord frontend**
The default `return false` case matched Discord, causing the CLI to
report Discord as unconfigured even after successful setup.

**`errors.ts` β€” bare `overflow` regex too broad**
Matched JavaScript `Maximum call stack size exceeded`, numeric overflow
errors, etc., misclassifying them as non-retryable `context_length`.
Changed to require `context.{0,10}overflow`.

**`cron.ts` β€” `warnedBadSchedule.clear()` caused a re-warning burst**
Clearing all 200 entries at the cap caused every previously-warned job
to re-warn on the very next tick. Now evicts only the oldest entry.

**`gateway.ts` β€” `Number("") === 0` could match chat context 0**
When `_chatId` is absent, `rawChatId` is `""` and `Number("") === 0`.
Added `rawChatId !== ""` guard to the ambient-context routing branch.

**`dream.ts` β€” `dreaming` lock held forever if backend ignores abort**
`await agentPromise.catch(() => {})` with no timeout meant that a backend
ignoring its abort signal would block `executeDream` forever, permanently
preventing any future dream runs. Added a `DREAM_ABORT_GRACE_MS` (30s)
bounded wait mirroring the heartbeat pattern.

**`remote-server/lifecycle.ts` β€” health-check fetch had no timeout**
A server that accepted TCP but never sent a response would stall
`ensureRemoteServer` indefinitely, blocking every subsequent chat turn.
Added `AbortSignal.timeout(5_000)`.

**`opencode/model-provider.ts` β€” `getProviderModels` was 0-indexed**
All other backends use 1-indexed pagination. `page=1` from a caller
would skip the first page of results. Changed to `page = 1` and
`start = (page - 1) * pageSize`.

## Low

**`triggers.ts` β€” `commandForBash()` re-probed bash on every spawn**
`spawnSync` was called on every trigger invocation. Cached the result
in a module-level variable after the first successful probe.

**`kilo/handler.ts` / `opencode/handler.ts` β€” redundant dynamic import**
`setSessionId` was already available from the static `sessions.js` import
at the top of each file. Replaced the dynamic `import()` with the static
binding.

**`remote-server/session-helpers.ts` β€” rename confusing intermediate variable**
`messageId` was typed as `{ id?: string }` (the info object, not the id
itself). Renamed to `messageInfo` to match what it actually holds.

https://claude.ai/code/session_01M8utiWqUBerWtQ45jdeXgH
@claudiusthebot
claudiusthebot force-pushed the claude/eager-sagan-Fv6mV branch from 421d63b to 1f23c6a Compare June 6, 2026 15:19
@claudiusthebot
claudiusthebot enabled auto-merge (squash) June 6, 2026 15:19
@claudiusthebot
claudiusthebot merged commit 5686c26 into main Jun 6, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants