Debugging the omniroute-beta Docker rebuild: `npm run build` (and the
Dockerfile's own post-build verification) failed with
`Cannot find module '.../node_modules/@atjsh/llmlingua-2/dist/index.js'`.
Root cause, reproduced directly (both against a live Docker builder image
and in a unit test): Next.js's own standalone trace creates a stub
directory for `@atjsh/llmlingua-2` containing only `package.json` — it
references the package (a dynamically-imported optional dependency) but
can't fully bundle it. colocateLlmlinguaOptionals's skip checks (both the
closure-level early return and the per-package loop) only tested
`existsSync(dest)`, so that stub was indistinguishable from "already fully
co-located" — the function skipped copying the real `dist/` output
entirely, silently shipping a package with a manifest but no code.
Fix: check for the package's declared `main` entry file when it has one
(the real-world case for every actual SLM optional). Packages with no
`main` field fall back to comparing the destination's top-level entries
against the source's — correct both for genuinely multi-file packages and
for a metadata-only source (package.json is then its complete, faithfully-
copied contents), which the existing idempotency test exercises.
Covered by tests/unit/colocate-optionals.test.ts's new stub-reproduction
case (fails against the pre-fix code, passes after — confirmed directly)
plus the 6 pre-existing cases, all still green.
/api/logs/[id]'s live "Generating… / Thinking…" preview
(extractPartialAssistantText) parsed each stream-chunk-log array element
independently. Each element is one raw network read, timestamp-prefixed for
the debug display — not one complete SSE `data:` line — so a single JSON
value (a `reasoning_content` delta) routinely splits across two or more
elements. Parsing per-element in isolation silently failed JSON.parse on the
split pieces and dropped them, leaving gaps in the reconstructed text that
read as garbled/scrambled reasoning once the survivors were concatenated —
reported live via a dashboard screenshot of an in-progress conversation.
Fix: strip each element's timestamp prefix and concatenate the whole array
into one continuous string first, then split into lines and parse — so a
value split across elements rejoins correctly before JSON.parse ever sees
it. Same reconstruction technique already proven correct for the final,
completed response body; this brings the in-flight preview path in line.
Covered by tests/unit/logs-detail-partial-reasoning-chunk-split.test.ts,
including a direct demonstration that the pre-fix per-element parse fails on
each half of a split value in isolation (proving genuine TDD, not a
coincidental pass) while the fixed concatenate-first parse recovers the full
text.
A client that closes its connection right after reading a fully-completed
SSE stream can race OmniRoute's own completion bookkeeping: the bytes
already reached the client, but the transform stream's own completion
callback (onStreamComplete, which flips streamCompletionRecorded) hasn't
finished bubbling up when the disconnect handler fires, so the request gets
persisted as a false 499 with zero token usage even though it delivered its
full response.
Confirmed live on real traffic before this fix: a request whose server log
showed "disconnect: request_signal_aborted" at 18236ms was persisted with
status 200 and full token usage (82814/1292) once the grace period let the
real completion win the race, matching what the client actually received.
createClientDisconnectGraceHandler (new leaf in
streamFailureFinalization.ts) polls isStreamCompletionRecorded() for up to
STREAM_DISCONNECT_GRACE_PERIOD_MS (default 10s, env-configurable, 0
disables) before finalizing as a failure. If a real completion lands within
the window, handleStreamFailure's own guard is a no-op and the genuine 200
stands.
Covered by tests/unit/stream-disconnect-grace-period-9653.test.ts (fake-timer
driven: already-recorded completion short-circuits, disabled-grace-period
finalizes immediately, a completion landing mid-window skips finalize
entirely, and no completion ever landing finalizes once the deadline
passes).
Upstream #8894 added an import for preferAntigravityConnectionsWithStoredProject
from ../antigravityProjectPersistence.ts in quotaStrategies.ts, but the module
itself was never committed — the file doesn't exist anywhere in git history on
either side of the merge. Since Next.js's instrumentation hook must load this
import chain successfully at boot, the missing module was fatal: the dev
server crashed entirely on every fresh start (not just the affected route).
Reconstructed conservatively per the call site and the changelog note ("prefer
Antigravity connections with stored project"): reorders connections so ones
with an already-known projectId (checked via both the direct column and the
providerSpecificData.projectId fallback, matching the shape written by
antigravityProjectPersist.ts) come first. Fail-open — reorders only, never
excludes a connection, consistent with this codebase's established pattern
for capability/preference filters elsewhere in combo/.
⚠️ base-red inherited: #9298
Turbopack failed with "the name HARD_COMPAT_REASONS is defined multiple
times" — two top-level const declarations of the same name existed in
comboStructure.ts. The unused one (with an extra "output_tokens" entry,
zero references anywhere) was dead weight left over from upstream;
the active one (tools/vision/structured_output, used by
hasHardCapabilityFailure/describeCapabilityFilterExhaustion/
filterTargetsByRequestCompatibility) is unaffected.
Confirmed pre-existing on upstream/release/v3.8.50's own tip before
this branch's merge (git show 7f36b192f already has both), so this is
an inherited base-red fix, not new behavior.
⚠️ base-red inherited: #9298
providerPayloadCollector (dashboard "Provider Response" panel) was keyed on
sourceFormat (the CLIENT's wire format) instead of targetFormat (the
PROVIDER's — see createSSEStream's own @param doc: "targetFormat - Provider
format", "sourceFormat - Client format"). Whenever a request translates
between two different formats — e.g. a Responses-API client routed to a
plain-OpenAI-chat-completions upstream, the common OpenClaw/opencode-zen
shape — the reducer picked for sourceFormat could never recognize the
provider's actual raw event shape, so it stayed stuck at its empty initial
state. The dashboard's "Provider Response" panel showed a permanently empty
`output: []` while "Client Response" (built from separately-accumulated
state, unaffected by this bug) correctly showed full content — reading as
if the two panels simply disagreed about the same request.
Confirmed live via a wire-level pcap capture (scripts/sre/tcp-close-
analyzer.py) cross-referenced against the dashboard log
(1786032832181-1c6275): the actual response was complete and correct: this
was purely a logging/summary bug, never a wire-format bug.
Fix is mode-aware: TRANSLATE mode uses targetFormat (the provider's true
format); PASSTHROUGH mode keeps sourceFormat, since passthrough has no
separate provider/client format split — nothing gets translated there, and
real passthrough callers (createPassthroughStreamWithLogger) don't even
pass targetFormat.
New regression test reproduces the exact live scenario (Responses-API
source, OpenAI target, real chat.completion.chunk deltas) and asserts the
provider summary reflects them — confirmed it fails with the old
`sourceFormat`-keyed code (reproducing the live `output: []`-style
symptom) and passes with the fix.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
Real agentic CLIs with many MCP servers routinely declare 40-50+ tools in
a single request — a live OpenClaw session logged 47. The tail-24 default
silently dropped the array's earlier entries behind an
_omniroute_truncated_array marker, so investigating why a specific tool
call (apply_patch) behaved oddly turned up nothing: its declared shape
(function vs custom type) was unrecoverable from the call log across 40
recent requests, even though the calls themselves succeeded.
Bumped the configurable default to comfortably cover real large tool
lists with headroom. Updated .env.example and docs/reference/
ENVIRONMENT.md to match (env-doc-sync check passes).
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
A tool_use/tool_result turn's text is the tool call's raw JSON arguments
(or a stringified result) — slicing that raw JSON at a fixed character
offset (TEXT_PREVIEW_LENGTH) routinely landed mid-string, storing INVALID
JSON. The conversations page's toTurn() then failed to JSON.parse it and
fell back to showing the raw, still-escaped text verbatim: a large
edit/write/apply_patch-style tool call with a long content field rendered
with literal `\n` sequences visible instead of real line breaks, looking
exactly like a JSON-escaping bug rather than a big diff. Confirmed live on
omniroute-dev: 3 stored `edit` tool_use previews were sitting at exactly
8000 chars with "Unterminated string in JSON" on parse.
buildTextPreview now parses first and caps oversized string VALUES inside
the JSON instead of slicing the raw blob, so a truncated payload is always
valid, re-parseable JSON. Plain text turns are unaffected (still a simple
slice — a cut-off sentence is harmless).
Also fixes JsonViewer's string rendering to preserve line breaks
(whitespace-pre-wrap) — a correctly-parsed multi-line tool argument was
still visually squashing onto one line without it.
Unrelated cleanup found while editing: conversationTracker.ts had two
literal NUL bytes (pre-existing, not introduced by this change) sitting
where a template-literal space belonged, making the file register as
binary to grep/rg/file. Restored to plain spaces.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
Adds chevron buttons in the conversation modal to step to the adjacent
conversation in the loaded list, matching the request-detail view's
prev/next pattern.
Also surfaces the true live reply while it's still generating: turn nodes
only get written once the client resends a turn as history on its NEXT
request, so the transcript had nothing new to show mid-stream even though
the request was actively producing text. /api/conversations now exposes
activeCallLogId — the in-flight request's own id from usageHistory's
pendingById, since call_logs only gets its row on completion and the
existing lastCallLogId join always lagged one request behind. The modal
polls that id's partial text (same source/cadence as RequestLoggerDetail)
and renders it as a provisional bubble.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
A phone that previously loaded a production build on this origin (or
an old dev build from before the registration was gated) kept an
active service worker across dev restarts. It intercepted every
navigation/asset fetch, occasionally serving a JS chunk that didn't
match the running dev server, which tripped Next's dev-client
chunk-mismatch auto-reload — visible as an unexplained, unstoppable
refresh loop on that device only (confirmed via a clean private tab
on the same phone/URL not looping).
PwaRegister now actively unregisters any existing service worker
registrations and clears their caches outside production, instead of
just skipping a new registration.
Co-authored-by: Markus Hartung <markus.hartung@gmail.com>