The #7786 squash accidentally committed its worktree copy
(.claude/worktrees/feat-7786/**, since untracked) and leaked probe tests
(repro-8522/probe-9033/repro-8956 — each now green via #9355/#9385/#9354)
plus a stray changelog.d/fixes/9159-fix.plan.md describing an UNMERGED fix
(would fabricate a changelog entry at release time — removed; #9159's own
PR ships its fragment).
This restores the PR's actual deliverable at the right paths: the
management-auth terminology guide (now with the required MDX frontmatter),
its docs test (3/3 green) and its changelog fragment.
A Next.js standalone build writes a synthetic .build/next/package.json
({"type":"commonjs"}) that lacks a "name" field. The resolveProjectRoot()
walk-up was stopping at this marker instead of continuing to the real repo
root, making PROJECT_ROOT point at .build/next where no .git exists, which
caused the source-mode validation to report "Not a git repository."
Fix: only accept a package.json as a project-root marker when its parsed
content has a non-empty "name" field. Keep .git as a hard marker.
Add isValidPackageMarker() helper for testability.
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
The 2026-08-05 TS7 rebaseline wrote 5000 measured WITHOUT the suppressions
file, but the PR gate (quality.yml lint:json + quality:collect) measures WITH
suppressions applied and reads 0 - so require-tighten failed every code PR
with delta 5000 > slack. Measured 0 on the pure tip ed122b2caf after the
stale-suppression prune (#9509). TS7 debt remains tracked in
config/quality/eslint-suppressions.json; any NEW warning outside it is an
immediate red, which is the policy.
release/v3.8.50 fails its own "No new ESLint warnings" gate right now,
independent of what any PR changes. Measured directly: a worktree
checked out at the current tip alone, no PR merged in, exits 2 with
"There are suppressions left that do not occur anymore." Cross-checked
against two unrelated open PRs (#9499, #9497) hitting the identical
failure, ruling out anything content-specific.
The mass-freeze commit that regenerated config/quality/eslint-suppressions.json
for the TypeScript 7 migration left one entry pointing at a violation
that no longer exists: src/lib/usage/providerLimits.ts no longer
triggers no-restricted-imports, but the suppression entry for it does.
ESLint's own suppression bookkeeping treats an unmatched entry as a
hard failure, separate from and in addition to real unsuppressed
errors.
--prune-suppressions removes exactly that one entry. It also drops the
informal "_comment" key documenting the freeze's origin, since ESLint's
suppression writer only round-trips file-keyed entries it manages
itself -- that context is not lost, it is still readable at the
mass-freeze commit (6b0e11e37) in git history.
This is one of two independent problems behind the same gate failure,
not the whole fix. Two files (tests/unit/issue-9407-gemini-web-validation-false-positive.test.ts,
tests/unit/v1-models-auth-leak-9320.test.ts) carry real, currently
unsuppressed no-explicit-any errors with no entry covering them at
all -- pruning cannot add what was never there. #9484 fixes those at
the source. Verified here that after this change alone, the gate
moves from exit 2 (stale suppressions) to the ordinary exit 1 those
two remaining errors cause -- both this and #9484 need to land before
the gate is green again.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Missing title/version/lastUpdated frontmatter broke the production build
(fumadocs-mdx requires title on every docs/**/*.md file).
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(tests): clear the two base-reds on release/v3.8.50
Both sat on the release itself and turned every open PR red as soon as it
merged the release, independently of the PR's own content.
- tests/snapshots/provider/translate-path.json: #9064 (b0501642dd) added the
code-execution-2025-08-25 and skills-2025-10-02 beta flags to the Anthropic
header but did not regenerate the golden. provider-translate-path-golden
failed on the bare release tip — 2 pass / 1 fail with zero PRs boarded.
Regenerated; the diff is 24 lines, all the same header in 6 variants.
- tests/unit/v1-models-auth-leak-9320.test.ts:83: shipped a (k: any) in a file
new enough that eslint-suppressions.json does not cover it. With
@typescript-eslint/no-explicit-any as error under tests/, that one cast
failed 'No new ESLint warnings' for every PR. The callback parameter infers
correctly, so the cast was redundant.
Verified on the fix branch: eslint exit 0, typecheck:core exit 0, and both
test files green (5/5).
* fix(tests): drop a third unsuppressed any (gemini-web validation test)
A full-repo lint on this branch surfaced one more file in the same class:
tests/unit/issue-9407-gemini-web-validation-false-positive.test.ts:50 casts
(executor as any).testConnection. The file entered the release at f1ea77fd04
(23:28), newer than the frozen eslint-suppressions.json, so the cast is not
covered — and it alone kept 'No new ESLint warnings' red on this very PR.
testConnection is a declared public method on GeminiWebExecutor
(open-sse/executors/gemini-web.ts:359), so the cast was redundant rather than
load-bearing; removed outright.
eslint exit 0, typecheck:core exit 0, 15/15 across the three touched tests.
- Updated the quality baseline to set eslintWarnings value to 5000, reflecting the migration to TypeScript 7 and the new warning thresholds.
- Modified the test masking allowlist to account for removed tests and sources, ensuring proper tracking of deprecated features.
- Enhanced ESLint configuration to ignore additional directories containing non-source files.
- Removed the .npmignore file as its contents are now managed in package.json.
- Adjusted KimiWeb model configuration to correctly map K3 to the K2D5 scenario, reflecting changes in the underlying logic.
- Updated artifact packing policy to prevent nested node_modules from being published, ensuring a leaner package size.
- Added tests to verify the exclusion of node_modules from published artifacts and to ensure the integrity of the package.json files array.
Acquire admission once after API-key policy, preserve lazy raw-request snapshots, and bind lease settlement to JSON, SSE, abort, deadline, and failure lifecycles. Expose a low-cardinality health summary and preserve non-SSE Ollama errors unchanged.
Add bounded weighted admission with fair queuing, deadline and cancellation handling, exact lease accounting, and a default-shadow runtime. Keep asynchronous resource-pressure shedding as an independent safety fuse and bound request feature estimation.
Return a typed HTTP 504 for OmniRoute's per-target timer, keep fallback active, and classify the local timeout as request-scoped so it cannot degrade provider connection health.
Closed#9183 in favor of this branch at the operator's request. Folds in
the exact PR diff (verified byte-identical via `gh pr diff 9183`, applied
cleanly with `git apply`):
- Fixed output_index collisions between reasoning/message/tool-call items
in Responses API streaming, which caused clients to ignore tool calls.
- Fixed a race in chunk processing where tool calls and finish signals
were lost when they arrived in the same chunk as reasoning/text content.
- Enabled full processing of multi-choice chunks (previously truncated to
a single choice).
- Added reasoning_content capture/replay for DeepSeek-based models
(big-pickle), keyed off the real position in the next turn's replayed
messages array instead of a hardcoded index 0 — prevents history
corruption that caused models to lose context and stop prematurely.
- Added big-pickle to models recognized for native textual reasoning tags
so <think> blocks convert to reasoning items correctly.
- Improved Responses-to-Chat translation to group reasoning, content, and
tool calls into a single assistant turn, as strict OpenAI-compatible
upstreams require.
- Added a descriptive placeholder for encrypted Responses API reasoning
blocks so downgraded Chat API requests keep context.
Test plan:
- All 82 tests across reasoning-cache.test.ts, translator-helper-branches.test.ts,
translator-request-openai-responses.test.ts, and the 3 new test files
(responses-api-truncation.test.ts, responses-replay-fixes.test.ts,
responses-request-translation.test.ts) pass
- npm run typecheck:core — clean
- npm run check:file-size — chatCore.ts (5024->5032) and
translator/response/openai-responses.ts (1174->1180) rebaselined,
both cohesive additions at the two reasoning-cache capture call sites and
the turn-grouping fix; tests/unit/reasoning-cache.test.ts rebaselined at
1035 (crosses the 1000-line new-file test cap, entirely this fold's diff)
lint-staged's prettier pass on the prior commit reformatted a pre-existing
multi-line ternary (title attribute) past the 100-char print width,
growing the file 1256->1260 lines with no functional change. CI's
check:file-size gate caught the drift (frozen cap can only shrink).
The 8KB cap on logged request/response bodies (open-sse/handlers/chatCore/
logTruncation.ts::truncateForLog()) was hardcoded — trivially exceeded by
any real multi-turn agentic conversation, meaning the dashboard's "Full
Conversation" panel could only ever show a placeholder instead of the
actual messages for nearly every logged row of any conversation with real
substance.
- Added CHAT_LOG_MAX_BODY_KB env var (src/lib/logEnv.ts::
getChatLogMaxBodyBytes()), default 1024 KB (1MB) — a 128x bump from the
old hardcoded 8KB — following the same configurable-limit pattern as the
sibling CHAT_LOG_TEXT_LIMIT/CHAT_LOG_ARRAY_TAIL_ITEMS/etc. vars.
- Documented in .env.example and docs/reference/ENVIRONMENT.md (both
required — tests/unit/issue-7793-env-doc-sync-repro.test.ts and
check-env-doc-sync.test.ts enforce this pairing).
Found and fixed a real bug while wiring this up: estimateSize.ts's
estimateSizeFast() had its own hardcoded 256KB early-exit optimization
("stop walking once bytes clearly exceeds the caller's threshold"), so it
could never report a size above ~256KB regardless of the object's true
size — meaning any caller threshold configured above 256KB (like the new
1MB default) was silently unreachable; every payload would look "under
threshold" and truncation would never fire, letting arbitrarily large
bodies through unbounded (the opposite of intended, and a real memory-
protection regression). Fixed by giving estimateSizeFast() a parameterized
earlyExitAt (default unchanged at 262144, so isSmallEnoughForSemanticCache's
existing behavior is untouched), with truncateForLog() now passing its own
configured getChatLogMaxBodyBytes() value through.
Also adds a "Conversation" field to the request detail panel's metadata
grid (last, after "Combo"), showing the request's conversation id
(sessionTag) for quick reference/copy.
Test plan:
- New TDD tests for both fixes (Responses API messageCount capture — from
the previous commit — and the estimateSizeFast earlyExitAt parameter),
confirmed failing before each fix and passing after
- Bumped the two truncateForLog test fixtures that were sized against the
old 8KB threshold so they still genuinely exceed the new ~1MB default
- npm run typecheck:core / npm run lint / npm run check:file-size — clean
- npm run test:unit — 27132 tests, same 4 pre-existing/unrelated failures
as the last confirmed-clean run (no new regressions) — including the two
env/doc-sync contract tests that initially caught the missing
CHAT_LOG_MAX_BODY_KB documentation, now fixed
- npm run test:vitest — 291/291 passed
- Rebuilt and redeployed to omniroute-dev
Follow-up to the earlier truncated-request-body transcript fix, found by
re-checking the live dashboard: a specific /v1/responses request still
showed nothing for its own turn even though its body genuinely was
truncated by logTruncation.ts's truncateForLog().
Root cause (two parts):
1. truncateForLog() only counted messages[] (Chat Completions) and
contents[] (Gemini) — never input[] (Responses API) — so a truncated
Responses API request's summary carried NO count field at all.
2. buildMultiRowConversation()'s earlier fix defaulted an unknown count to
0, which silently produced "0 new turns" instead of surfacing that the
count was simply unavailable — same end symptom as the original bug
(nothing shown) despite the row being genuinely truncated.
Fixes:
- logTruncation.ts now also sets messageCount for input[] bodies (root
fix, only helps requests logged from here forward).
- multiRowConversation.ts now distinguishes "known count" (existing
specific "N messages not shown" placeholder + correct bookkeeping) from
"unknown count" (a generic placeholder, since we can't safely diff
against previousTotal without a real number) — needed for the
already-persisted historical data on omniroute-dev that will never
retroactively get a messageCount.
Test plan:
- New TDD tests for both gaps (Responses API count capture in
logTruncation, unknown-count placeholder in multiRowConversation),
confirmed failing before each fix and passing after
- npm run typecheck:core / npm run lint / npm run check:file-size — clean
- npm run test:unit — 27223 tests, same 4 pre-existing/unrelated failures
as the last confirmed-clean run (no new regressions)
- npm run test:vitest — 291/291 passed
- Rebuilt and redeployed to omniroute-dev
vulnCount 10->22 (osv-scanner, measured in PR #9439's own CI run). Not a
dependency change from this PR — `git diff upstream/release/v3.8.50 HEAD --
package.json package-lock.json` is empty, neither file was touched anywhere
in this branch. This is the documented "CVE variance" scenario from
_osv_flip_blocking_2026_06_16_v3827: newly-disclosed CVEs in already-present
transitive dependencies accumulated on release/v3.8.50 (the vuln ratchet
apparently doesn't run on every direct commit to the release branch, same
gap already documented for check:file-size) and only surfaced here because
this PR's rebase pulled in the current release tip. Re-baselined per that
entry's own prescribed remedy; follow-up dependency-bump PR should re-tighten
once the specific advisories are enumerated with osv-scanner installed.
CI failures on PR #9439:
1. Migration version collision: upstream/release/v3.8.50 landed
134_proxy_logs_egress_ip.sql (#9291) after this branch's original rebase,
colliding with this branch's own 134_agentic_conversations.sql. Renamed
to 135_agentic_conversations.sql (re-rebased onto the current tip first).
2. check:file-size: rebaselined the files this PR's own feature growth pushed
over their frozen/cap thresholds (RequestLoggerDetail.tsx, RequestTimeline.tsx,
RequestLoggerV2.tsx, chat.ts, chatCore.ts — see the new
_rebaseline_2026_08_04_9439 entry for the itemized justification) plus
open-sse/executors/base.ts, which was already over its own frozen baseline
on release/v3.8.50 independent of this branch (confirmed via `git diff
upstream/release/v3.8.50 HEAD -- open-sse/executors/base.ts` — empty).
3. A third real bug, found by re-checking the live dashboard after the
previous round's fixes: a request with a long real conversation chain
showed only its own response in the "Full Conversation" panel. Root
cause: open-sse/handlers/chatCore/logTruncation.ts's truncateForLog()
replaces any request body over ~8KB with a bare {_truncated,
_originalBytes, messageCount, ...} summary, dropping messages/input
entirely — the norm, not the exception, for any conversation with real
substance. buildRequestTurns() legitimately found nothing to parse, so
the transcript silently rendered only that row's response, and (more
subtly) every subsequent row's delta-slicing bookkeeping was computed
against the wrong running total (0 instead of the row's real turn
count), which would have corrupted the rest of the reconstruction too
for any longer chain built on top of a truncated row.
Fix: buildMultiRowConversation now detects a truncated request body via
its messageCount field, uses that count for delta bookkeeping instead of
silently treating it as zero, and renders one explicit placeholder turn
("N messages not shown — the request body was too large to log")
instead of just disappearing.
Test plan:
- New regression tests for the truncation case (single truncated row, and
a truncated row followed by a real row to verify bookkeeping stays
correct)
- npm run check:migration-numbering / check:file-size — clean
- npm run typecheck:core / npm run lint — clean
- npm run test:unit — 27086 tests, only 4 failures remain (down from 18 —
2 were fixed by the newer upstream commits pulled in by this re-rebase),
all independently pre-existing/unrelated (ServiceSupervisor timing,
monaco-editor path, npm-pack)
- npm run test:vitest — 291/291 passed
Two independent bugs found during further live verification of the
conversation-tracking feature:
1. (#9315) The dashboard's "Provider Response" panel showed a stale,
incomplete snapshot for long streamed responses. Root cause:
open-sse/utils/stream.ts reconstructed the summary from
buildStreamSummaryFromEvents(providerPayloadCollector.getEvents(), ...)
— but getEvents() only returns whatever survived the collector's
maxEvents/maxBytes cap, so once a stream exceeded it (easy with a
reasoning + tool-calling model), everything after the cutoff (final
finish_reason, tool_calls, rest of reasoning_content, usage) was
silently dropped from the reconstruction, even though the client
actually received the correct, complete response.
Fix: streamPayloadCollector.ts's per-format summary builders
(buildOpenAISummary/buildResponsesSummary/buildClaudeSummary/
buildGeminiSummary) are now also available as incremental reducers
(createXReducer: ingest one chunk at a time, finalize at the end).
createStructuredSSECollector accepts a format + fallbackModel and feeds
the reducer on every push() — including chunks that get dropped from
the retained event array once the cap is hit — via a new getSummary()
method. stream.ts's 3 call sites now use collector.getSummary() instead
of reconstructing from the (possibly truncated) getEvents().
2. Conversation continuation never actually worked for real agentic CLI
traffic. Root cause: computeFingerprintHash/hashTurnsBounded anchored
conversation identity partly on the system message's text — but real
coding-agent CLIs (Claude Code, opencode, etc.) commonly regenerate the
system prompt on every single request with live context (timestamp,
cwd, git status...). That volatility alone broke both the fingerprint
bucket lookup and the prefix-hash continuation check, so every request
minted a brand new conversation id even though apiKeyId/model/toolNames
and the actual user/assistant history were an unbroken, growing
continuation. Confirmed live: 28 consecutive requests from one real,
growing session, each recorded as its own turn_count=1 conversation —
which is also why /dashboard/conversations appeared empty (nothing ever
reached turn_count >= 2) and why an individual timeline/log entry only
ever showed a single turn.
Fix: both computeFingerprintHash's identity anchor and
hashTurnsBounded's head/tail projection now exclude the system message
entirely, so a regenerated-every-turn system prompt can no longer break
continuation detection. New regression test reproduces the exact
scenario (system prompt differs each turn, everything else constant)
and confirms the second request is now recognized as a continuation.
Also fixed while touching hashTurnsBounded: an accidental stray control
character (SOH, 0x01) in the internal join() separator — cosmetic (any
consistent separator produces a valid hash) but worth cleaning up since it
was already being edited; no stored data depended on the old format since
the continuation bug meant turn_count never reached 2 in production.
Test plan:
- New TDD regression tests for both bugs (stream-payload-collector.test.ts,
conversationTracker.test.ts), confirmed failing before the fix and
passing after
- npm run typecheck:core / npm run lint — clean
- npm run test:unit — 26983 tests, 18 failures, all independently confirmed
pre-existing on release/v3.8.50 (reproduced identically against the
clean base commit)
- npm run test:vitest — 291/291 passed
- Rebuilt and redeployed to omniroute-dev; health check + DB migration
verified
Missing title/version/lastUpdated frontmatter broke the Next.js/fumadocs
build entirely (Turbopack: "invalid frontmatter... expected string, received
undefined"), pre-existing on release/v3.8.50 as of #9323 and unrelated to
this branch's feature work — discovered because it blocked building this
branch's image for deploy.
The new "Conversations" sidebar entry and "Conversation" logs column needed
their Vietnamese strings (Vietnamese is the reference locale requiring full
translation, no __MISSING__ placeholders) and the sidebar structure
assertions needed the new item added to their expected lists.
Every agentic chat request now gets a conversation id (X-ConversationId
response header), and OmniRoute detects when a follow-up request continues
the same conversation via fingerprint + bounded prefix-hash matching, with a
strict-growth invariant to prevent false merges between independent
single-shot requests.
Dashboard changes:
- /dashboard/logs: toggleable Conversation column
- /dashboard/logs/timeline: same-conversation requests share a timeline lane,
connected by an arrow, with a configurable lane-reuse window
- Request detail panel: new "Full Conversation" transcript above the raw SSE
event stream, with Markdown rendering, per-turn timestamps, turn-relative
view (only turns up to the one you opened, with a jump-to-next link),
click-any-turn-to-open-its-log navigation, and live auto-refresh (with an
auto-follow toggle, matching the event stream's autoscroll pattern) that
rebuilds the transcript in real time from the in-flight SSE chunk buffer
while a request is still streaming
- New /dashboard/conversations page listing only conversations with 2+ turns
- Configurable auto-refresh intervals on both the timeline and conversations
list pages
Also fixes a pre-existing bug where the timeline view never showed SSE/
stream-chunk events or respected email-masking, because RequestTimeline.tsx
hardcoded debugEnabled/emailsVisible instead of reading the same
server-side/store state RequestLoggerV2.tsx already used, and makes the
request detail panel and conversations list responsive on mobile.
Bumps ip-address, hono, fast-uri, socket.io-parser, undici (v6+v7),
protobufjs, and tar via targeted package.json overrides.
All patches are lockfile-only (no code change, range already covers).
Verified: npm audit → 0 vulnerabilities.
Note: brace-expansion NOT in overrides (separate major lines need
different patches; each resolved within its parent range).
Co-authored-by: wgordon17 <22222756+wgordon17@users.noreply.github.com>
* chore(ci): stop dependabot from grouping ioredis majors with routine bumps
ioredis is loaded through a dynamic import in the distributed quota store, so a
breaking major passes build, typecheck and both test suites and only surfaces at
runtime for operators running Redis-backed quota. #9310 grouped ioredis 5.10.1 to
6.0.0 with 9 unrelated production bumps; majors get their own PR from now on.
* docs(changelog): add fragment for #9425
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(resilience): count STREAM_EARLY_EOF as a provider failure in combo routing
A STREAM_EARLY_EOF is an upstream that accepted the request (HTTP 200), opened
the SSE stream, then closed it without emitting a single non-ping event. The
combo path classified it together with STREAM_READINESS_TIMEOUT through
isStreamReadinessFailureErrorBody(), and the readiness exemption in
shouldRecordProviderBreakerFailure meant the whole-provider circuit breaker
never saw it.
During a provider-wide outage that makes the breaker blind. Over a 7-day window
on our router we recorded 311 of these events, 302 of them on one model, 265
inside the upstream's published incident window — and the provider breaker sat
at CLOSED / failure_count=0 the entire time. Every request kept being dispatched
to the failing provider instead of shedding to the next combo target.
The two codes are different signals. The readiness probe is a pre-flight
liveness check on a connection we have not committed to, so failing it means
"this connection looks stale". An early EOF means the provider took the request
and then failed to serve it. The single-model path already treats it that way:
shouldTripProviderBreakerForResult has no readiness exemption, so a 502 early
EOF trips the breaker there. This makes the combo path consistent.
isStreamReadinessFailureErrorBody keeps matching both codes, because the
transient-retry and round-robin semaphore-cooldown paths in combo.ts do want
identical treatment for both. Only the breaker needs to tell them apart, so the
distinction is added as a narrow predicate and an optional argument rather than
by changing the shared classifier. Omitting the new argument reproduces the
previous behaviour exactly.
Follows the additive-override pattern established by the isProxyUnreachable
work, and leaves the existing exclusions for client aborts and plain 429s
untouched.
* test: register stream-early-eof-breaker in stryker tap.testFiles
The mutation test-coverage gate (check:mutation-test-coverage --strict)
detects unit tests that cover a mutated module but are missing from
stryker.conf.json tap.testFiles, so their mutant kills would not count.
comboPredicates.ts is one of the mutated modules, and the new
stream-early-eof-breaker.test.ts covers it, so the gate correctly flagged
the omission. 8376-econnrefused-breaker.test.ts -- the test this one is
modeled on -- is already registered; this just brings the new file in line.
No production code change.
---------
Co-authored-by: Nick Sullivan <nick@technick.ai>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>