Skip to content

perf: optimize full-path concurrency and resource ownership#1341

Open
ding113 wants to merge 17 commits into
devfrom
perf/full-path-benchmark-optimization
Open

perf: optimize full-path concurrency and resource ownership#1341
ding113 wants to merge 17 commits into
devfrom
perf/full-path-benchmark-optimization

Conversation

@ding113

@ding113 ding113 commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

This PR is the accumulated full-path concurrency, persistence, and resource-ownership candidate based on dev@6fcb827b. It contains 17 commits across 112 files (+18,684/-2,757), including one GitHub Actions formatting-only commit. Local benchmark infrastructure and raw artifacts remain outside the repository.

The branch is ready for code review and CI: build and formatter checks passed on the semantic head; the GitHub Actions formatting-only commit has no whitespace-insensitive semantic diff; and typecheck, the full Vitest suite, focused tests, and whitespace checks pass on the final PR head. Repository-wide lint currently reports pre-existing optional-chain errors in untouched src/actions/webhook-targets.ts. This PR does not claim that the final post-change gateway benchmark or two-hour production soak is complete; the benchmark qualification limits and failed runner attempts are documented below.

Changes by objective

Performance and concurrency

  • Split the PostgreSQL budget into bounded data/control/writer lanes with admission before query creation. Production DB_POOL_MAX=20 resolves to 15/4/1; Kubernetes uses a total budget of 24 rather than the previous per-pod 30.
  • Replace eager Node-to-Web and Response.clone()/tee() draining with a demand-driven single-reader pump, bounded lookahead, backpressure propagation, explicit drain/cancel ownership, and connected non-reader deadlines.
  • Add bounded WebSocket outbound buffering (1 MiB per client), one in-flight send, upstream pause/resume, send callback deadlines, and bounded request-body drain.
  • Replace full-window Redis ZSET scans on every cost write with cutoff + ZADD + EXPIRE; batch Key/Provider/User counters into one explicit pipeline.
  • Replace twelve independent lease settlement calls with one request-idempotent Lua settlement over all twelve dimensions.
  • Replace message-buffer overflow Map scans with an indexed eviction heap and aggregate overflow warnings.
  • Add PostgreSQL statement/lock timeouts and Redis command/socket timeout containment.

Stability, correctness, and resource ownership

  • Make message terminal persistence durable and commit-observing; add conditional status_code IS NULL fallback CAS and publish public rollups from the committed merged patch.
  • Preserve terminal ownership across client abort, Provider timeout, response timeout, source close/error, drain, and delayed persistence races.
  • Move circuit, Session, and Provider side effects after durable terminal ownership; preserve 404 and endpoint/provider circuit isolation.
  • Track the exact Session concurrency slot acquired so early failures cannot leak or decrement the wrong slot.
  • Join async task generations, writer settlement, and DB pool closure during shutdown; cleanup failure exits non-zero and the hard watchdog remains referenced.
  • Make writer stop singleflight/sticky, retire timed-out generations, prevent stale durable reinsertion, and preserve terminal-priority entries under overflow.
  • Preserve hedge winner/loser accounting with stable per-loser Redis event IDs and exactly-once billing/release behavior.
  • Add real PostgreSQL/Redis integration coverage for lane isolation, slow pool close, write-buffer recovery, rolling cost, lease settlement, and hedge lifecycle.

Security, observability, and test hardening

  • Redact authentication and cookie headers at the Langfuse boundary while preserving benign headers.
  • Prevent late transport cancellation listeners from retaining or aborting completed upstream ownership.
  • Add public-seam coverage for error sanitization, overrides, terminal status, durable persistence ordering, Session/readback aggregation, usage-log queries, and terminal cost accounting.
  • Stabilize the user-insights date test by asserting the same explicit UTC contract used by the component.
  • Document the new DB lane budgets and Redis/PostgreSQL timeout settings in .env.example and deployment configuration.

Benchmark and experiment record

Run census

  • 39 gateway case directories were created in the local lab, including qualification, saturation, protocol/session semantics, 20 selected mode-off/on baseline cases, one short mixed smoke, and one 30-minute mixed qualification.
  • 4 direct calibration result files were produced: Claude and Codex closed-loop capacity, plus the original and corrected Claude 2,000 req/s CAR scheduler runs.
  • 4 comparison aggregate files exist; only claude-rep1-5-off-vs-on.json and codex-rep1-5-off-vs-on.json are the final selected five-pair historical comparisons. Earlier one/two-pair files are superseded.
  • Module and fault probes include 20-repetition Node stream backpressure, 5-repetition response-pump slow-consumer scenarios, rolling Redis cardinality samples, Redis blackhole queue containment, lease-marker memory, real PostgreSQL lane/lock behavior, and integration matrices.

Direct fixture capacity and scheduler calibration

Case Result Interpretation
Claude closed-loop, c256, 5,000 5,165.3 successful req/s; E2E p50/p95/p99 38.426/151.451/251.382 ms Mock/client headroom only
Codex closed-loop, c256, 3,000 5,076.1 successful req/s; E2E p50/p95/p99 44.886/111.986/159.539 ms Mock/client headroom only
Claude CAR 2,000 req/s, corrected offered 20,000; completed 19,751; dropped 249; 0 transport/protocol/cancel/drain errors Scheduler accounting evidence, not gateway capacity

The original CAR run scheduled only 17,403 requests in 10 seconds and had five transport errors. The cumulative-offer/deadline scheduler was fixed before the corrected run.

Historical high-concurrency mode diagnostics

These 20 cases use the unmodified baseline SHA, five ABBA-balanced repetitions per protocol and mode, 30 s warmup, 120 s measurement, 30 s cooldown, and seeds 20260713-20260717. They show where work amplification existed, but they are not a post-change candidate comparison and the old analyzer used unpaired-mean rather than valid paired-median confidence intervals.

Metric Claude off -> on Codex off -> on
Completed / offered 20,489/21,600 -> 21,600/21,600 10,516/12,600 -> 12,600/12,600
Successful goodput 34.15 -> 36.00 req/s (+5.42%) 17.53 -> 21.00 req/s (+19.82%)
E2E p50 1,270 -> 214 ms (-83.13%) 3,729 -> 323 ms (-91.34%)
E2E p95 2,579 -> 1,019 ms (-60.50%) 5,257 -> 1,515 ms (-71.18%)
E2E p99 3,639 -> 1,435 ms (-60.57%) 7,278 -> 1,856 ms (-74.50%)
Mean ELU 0.760 -> 0.584 (-23.16%) 0.809 -> 0.598 (-26.10%)
RSS peak 870.96 -> 752.84 MB (-13.56%) 1,358.94 -> 896.00 MB (-34.07%)
Heap peak 417.76 -> 335.36 MB (-19.72%) 778.61 -> 431.33 MB (-44.60%)
Redis commands 724,616 -> 518,429 (-28.45%) 390,023 -> 316,346 (-18.89%)
Redis input bytes 4.573 GB -> 114.2 MB (-97.50%) 8.430 GB -> 68.74 MB (-99.18%)

Mode-off dropped 1,111 Claude offers in one repetition and 2,084 Codex offers across five repetitions; mode-on dropped zero. The two-minute windows are insufficient for leak/no-leak claims.

Module-level before/after evidence

  • Node stream adapter, unread 96 MiB: source production reduced from 96 MiB to 192 KiB; external-memory delta from about 96.15 MiB to 0.33 MiB; ArrayBuffer from 96 MiB to 0.1875 MiB. The probe ran 20 repetitions.
  • Generic response pump, paused 64 MiB: produced bytes reduced from 64 MiB to 128 KiB; paused ArrayBuffer from about 63.94 MiB to 64 KiB. Completely unread 96 MiB reduced to 64 KiB. Each baseline/candidate scenario ran five times; all 20 processes closed cleanly.
  • Rolling Redis write at 20k/80k/200k/378k members: p50 changed from 15.955/64.499/181.879/382.867 ms to 0.161/0.135/0.192/0.172 ms, removing O(window-cardinality) work from writes. This is a module result, not gateway throughput.
  • Message-buffer overflow: 2,000 enqueues into a full 5,000-entry queue previously took 1,289.35 ms versus 1.30 ms without overflow; the candidate uses O(log n) eviction with durable entries excluded from the eviction index.
  • Redis blackhole: 4,950 commands offered over two seconds previously left 4,702 queued while the client stayed ready. The candidate applies command timeout, a later socket no-progress timeout, and disables resend of unfulfilled commands.
  • Lease marker memory: 10,000 markers measured 215.2 bytes/marker. TTL was reduced from one hour to five minutes, cutting projected steady cardinality by 12x while retaining the configured retry horizon.
  • Real PostgreSQL lane test with total pool 6: four data sleepers occupied data capacity while control and writer probes completed within 300 ms; the 33rd outstanding data query failed immediately with DB_POOL_ADMISSION_EXCEEDED.

Mixed 30-minute qualification: failed, not promoted

  • Offered 52,560; scheduled/completed/successful 50,871; dropped 1,689 (3.21%). Scheduled requests had zero transport/protocol/cancel/drain errors.
  • Overall TTFB p50/p95/p99/p99.9: 420/3,570/7,652/17,339 ms. E2E: 554/3,617/7,687/17,341 ms.
  • Control probes: 3,787/3,820 successes plus 33 three-second timeouts across every endpoint family. This fails the requirement that the management plane remain responsive under load.
  • Redis: 7,036,568 commands (136.07/request), 1.263 GB input, 879,434 EVALs. PostgreSQL: 397,889 commits (7.69/request), 803.4 MB temp bytes.
  • RSS peaked at 1,160.82 MiB and recovered 44.31% from peak; heap/external/ArrayBuffer/handles/sockets recovered approximately fully. Thirty minutes is still insufficient for the required two-hour leak conclusion.

Runtime/integration attempts

  • T10 I1 attempt-0001: four tests passed; the fifth exposed a test-only Drizzle shape mismatch (cause.code=55P03 versus top-level code). Cleanup also failed on container-owned files. The failed attempt remains immutable.
  • T10 I1 attempt-0002: the exact PostgreSQL integration file passed 5/5, but the overall attempt is still FAIL. docker compose stop remained active for roughly 579.77 s after both containers exited; the 600 s outer deadline sent SIGTERM. Cleanup then recorded 21 failures, five missing manifest pairs, five unreadable paths, a partial freeze, and one retained quarantine. No T10 task container/network/volume/listener/process/lock remains; ambient benchmark PostgreSQL/Redis remained healthy.
  • Because attempt-0002 did not achieve cleanup/evidence PASS, this PR does not claim I1 runtime GREEN, does not claim T12/I2 completion, and does not claim the final formal candidate benchmark.

Validation

  • bun run build: PASS; 187 static pages generated; 11 existing Turbopack/Edge Runtime warnings.
  • bun run lint: the semantic head passed; on the final formatted PR head it reports pre-existing optional-chain errors in untouched src/actions/webhook-targets.ts.
  • bun run lint:fix: PASS; no fixes applied.
  • bun run typecheck: PASS.
  • bun run test: PASS on the final committed tree.
  • 32 directly affected/uncommitted test files passed before commit.
  • User-insights UTC regression passed 3 consecutive runs (21/21 assertions across the file runs).
  • Hedge-loser billing test passed 3/3 after replacing test as any with a real ProxySession fixture.
  • git diff --check origin/dev...HEAD: PASS.
  • Focused historical gates include 328 changed-unit tests, 27 configured integration tests, 108 T07 tests, real PostgreSQL and Redis semantic tests, and repeated process-tree/readiness matrices.

Readiness and residual risk

Ready now

  • The Git worktree is clean.
  • The branch contains no benchmark raw data, runner evidence, credentials, runtime directories, or generated intermediate files.
  • Application build, typecheck, formatter, full unit/API test suite, focused regressions, and diff checks pass; the repository-wide lint exception is isolated above.
  • The changes are split into 17 commits, including one formatting-only automation commit, and are ready for CI and maintainer review.

Not claimed

  • No accepted same-seed post-change gateway A/B result exists for the final SHA.
  • No two-hour post-change soak exists; no final leak/no-leak conclusion is made.
  • The 30-minute mixed qualification failed control-plane/drop criteria.
  • T10 integration behavior is 5/5 GREEN, but its overall isolated runtime attempt is FAIL because cleanup/evidence did not pass.
  • HTTP/2, authenticated proxy identity, Responses WebSocket slow-consumer, silent billed hedge loser, and several large/error-body cases still require targeted qualification.

Review guidance

Suggested order:

  1. DB lane/admission ownership and shutdown ordering.
  2. Demand-driven stream/WS backpressure and cancellation state machines.
  3. Durable terminal CAS, committed-payload callback, and public rollup semantics.
  4. Redis batching/Lua settlement and hedge event identity.
  5. Security boundary redaction and deployment defaults.
  6. Integration and fault-interleaving tests.

Greptile Summary

This PR (perf: optimize full-path concurrency and resource ownership) is a large-scale performance and reliability overhaul across 112 files (+18,038/−2,100 lines). It replaces eager Response.clone()/tee() draining with a demand-driven response pump, introduces a three-lane PostgreSQL pool with admission control, adds an O(log n) eviction heap for the write buffer, implements atomic Lua lease settlement, batches Redis cost pipelines, adds WebSocket outbound buffering, and hardens shutdown ordering.

  • Demand-driven response pump (demand-driven-response-pump.ts): new ReadableStream (HWM=0) with state machine (client-active → draining → finalizing → closed), 60s pending-chunk deadline, and clean wasClientAborted signal for circuit-breaker logic.
  • DB admission control (admitted-client.ts, db.ts): createAdmittedSqlClient Proxy wraps unsafe/begin with an outstanding-query counter; three named pools (data/control/writer) with budget split splitPoolBudget(total).
  • Atomic lease settlement (lease-service.ts): single Lua script covers all 12 lease dimensions with idempotency marker TTL=5 min; shutdownAll and DB close are now critical barriers with no timeout wrapping.

Confidence Score: 4/5

Safe to merge with minor follow-up items; no data-loss or security-critical bugs found in the diff.

The PR is a well-structured performance overhaul. All five findings are P2 (non-blocking). The most significant concern is the tag-template-literal bypass of admission control, but it affects back-pressure rather than correctness. Shutdown-delay risk from ignored AbortSignal and silent cost-tracking drift are also P2. No P0/P1 issues were identified.

src/drizzle/admitted-client.ts (apply-trap gap), src/app/v1/_lib/proxy/response-handler.ts (AbortSignal threading), src/lib/rate-limit/service.ts (pipeline error propagation)

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/demand-driven-response-pump.ts New demand-driven pump with clean state machine; minor semantic ambiguity in clientAborted for deadline vs. cancel path.
src/app/v1/_lib/proxy/response-handler.ts Durable persistence and post-terminal side-effect scheduling look correct; AbortSignal ignored in task factory.
src/drizzle/admitted-client.ts Admission control Proxy is well-structured but missing apply trap — tag template literal calls bypass outstanding counter.
src/drizzle/db.ts Three-lane pool split with graceful budget degradation; closeDbPools uses allSettled correctly.
src/lib/async-task-manager.ts Factory-based registration and shutdownAll loop are correct; post-allSettled cleanup is dead code.
src/repository/message-write-buffer.ts EvictablePendingIndex min-heap implementation is correct; durable acknowledgement callbacks are well guarded.
src/lib/rate-limit/lease-service.ts Atomic Lua settlement script covers all 12 dimensions with idempotency; TTL reduced from 1h to 5min.
src/lib/redis/lua-scripts.ts TRACK_COST_ROLLING_WINDOW write-only refactor removes per-write ZRANGE scan; unified daily+5h into single script.
src/lib/redis/client.ts commandTimeout and socketTimeout (commandTimeout+5000) added; autoResendUnfulfilledCommands disabled for blackhole safety.
src/lib/lifecycle/shutdown.ts Critical barriers (async tasks, writer quiescence, DB pool) are now properly awaited without timeout wrapping.
src/lib/langfuse/trace-proxy-request.ts redactHeaders applied to both request and response headers before Langfuse submission.
src/lib/rate-limit/service.ts Batched pipeline reduces Redis round trips; pipeline errors silently swallowed — cost counters can drift.
src/repository/message.ts updateMessageRequestDetailsDurably and CAS fallback path are correctly implemented with commitPublished guard.
server.js WebSocket outbound buffering with 1 MiB cap, one-in-flight constraint, and pressured-response resume logic look correct.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Client
    participant RH as ResponseHandler
    participant Pump as DemandDrivenPump
    participant UP as UpstreamProvider
    participant WB as WriteBuffer
    participant DB as DB (writer lane)

    C->>RH: POST /v1/...
    RH->>Pump: createDemandDrivenResponsePump()
    RH->>UP: fetch(upstream)
    UP-->>Pump: ReadableStream chunks
    Pump-->>C: "demand-driven stream (HWM=0)"
    C->>Pump: pull() [demand signal]
    Pump->>UP: read chunk
    UP-->>Pump: chunk
    Pump-->>C: enqueue chunk
    Note over Pump: armPendingChunkDeadline (60s)
    alt Client cancels
        C->>Pump: cancel()
        Pump->>Pump: "startDrain() clientAborted=true"
    else 60s deadline
        Pump->>Pump: "startDrain() clientAborted=true"
        Pump->>UP: cancelSource()
    end
    Pump->>RH: completion (wasClientAborted)
    RH->>WB: updateMessageRequestDetailsDurably()
    WB->>DB: INSERT/UPDATE (writer pool)
    DB-->>WB: commit ack
    WB-->>RH: onCommittedCallback
    RH->>RH: schedulePostTerminalSideEffects()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as Client
    participant RH as ResponseHandler
    participant Pump as DemandDrivenPump
    participant UP as UpstreamProvider
    participant WB as WriteBuffer
    participant DB as DB (writer lane)

    C->>RH: POST /v1/...
    RH->>Pump: createDemandDrivenResponsePump()
    RH->>UP: fetch(upstream)
    UP-->>Pump: ReadableStream chunks
    Pump-->>C: "demand-driven stream (HWM=0)"
    C->>Pump: pull() [demand signal]
    Pump->>UP: read chunk
    UP-->>Pump: chunk
    Pump-->>C: enqueue chunk
    Note over Pump: armPendingChunkDeadline (60s)
    alt Client cancels
        C->>Pump: cancel()
        Pump->>Pump: "startDrain() clientAborted=true"
    else 60s deadline
        Pump->>Pump: "startDrain() clientAborted=true"
        Pump->>UP: cancelSource()
    end
    Pump->>RH: completion (wasClientAborted)
    RH->>WB: updateMessageRequestDetailsDurably()
    WB->>DB: INSERT/UPDATE (writer pool)
    DB-->>WB: commit ack
    WB-->>RH: onCommittedCallback
    RH->>RH: schedulePostTerminalSideEffects()
Loading
Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
src/app/v1/_lib/proxy/response-handler.ts:144-178
**AbortSignal ignored in post-terminal task factory**

The factory passed to `AsyncTaskManager.register` accepts an `AbortSignal` parameter but the implementation never threads it into the work it schedules. When the pod receives SIGTERM, `shutdownAllAsyncTasks()` fires the signal but the in-flight finalization work continues uninterrupted — delaying pod shutdown by up to `STREAM_FINALIZATION_MAX_MS = 120_000` ms regardless of the shutdown deadline.

### Issue 2 of 5
src/drizzle/admitted-client.ts:195-260
**Tag template literal calls bypass admission control**

The `createAdmittedSqlClient` Proxy intercepts named properties (`unsafe`, `begin`) but has no `apply` trap. When Drizzle calls the client as a tag template literal (e.g. `` client`SELECT 1` ``), the call goes directly to the underlying `postgres.js` client, completely bypassing the outstanding-query counter. Under pool pressure this silently removes the back-pressure guarantee for the most common query path.

### Issue 3 of 5
src/app/v1/_lib/proxy/demand-driven-response-pump.ts:124-158
**`clientAborted` conflates client cancel and server-side drain**

`startDrain` sets `clientAborted = true` unconditionally. It is called both when the client cancels and when `armPendingChunkDeadline` fires after 60s of unconsumed chunk. In the deadline case the provider delivered data that the client simply has not consumed yet — setting `clientAborted = true` here causes `wasClientAborted()` to report a client fault, which may suppress an accurate circuit-breaker update blaming the provider.

### Issue 4 of 5
src/lib/rate-limit/service.ts:220-250
**Pipeline errors are logged but not propagated**

`logCostPipelineErrors` logs individual command failures at `error` level but returns `void`. The `trackCost` caller discards the result, so any Redis pipeline failure causes silent cost-tracking drift — counters are neither decremented nor retried, leading to incorrect billing or rate-limit state with no observable error in the call stack.

### Issue 5 of 5
src/lib/async-task-manager.ts:321-390
**Post-`allSettled` cleanup calls in `shutdownAll` loop are always no-ops**

After `Promise.allSettled`, the loop iterates `pendingTasks` and calls `pendingTasks.delete(task)` for each settled task. But tasks self-delete from `pendingTasks` inside their own `.finally()` chain before the `allSettled` resolves, so by the time the loop runs, `pendingTasks` is already empty. The explicit cleanup is dead code and may create confusion about the actual cleanup path.

Reviews (1): Last reviewed commit: "chore: format code (perf-full-path-bench..." | Re-trigger Greptile

Greptile also left 5 inline comments on this PR.

ding113 and others added 17 commits July 14, 2026 19:24
…ssion control

Replace the single postgres.js pool with three isolated lanes (data,
control, writer) that share a configurable total connection budget.
Each lane gets a per-pool outstanding-operation admission wrapper
that fast-fails with DB_POOL_ADMISSION_EXCEEDED before saturating the
underlying driver queue, preventing unbounded memory growth under
load.

Route handlers now wrap with withDataDbScope so request-path queries
use the data lane via AsyncLocalStorage, while control-plane and
writer traffic remain isolated. Add statement_timeout and
lock_timeout at the connection level so slow SQL cannot block the
streaming finalization deadline.

Introduce LOCAL_OVERLOAD error category so admission rejections are
never retried, failovered, or counted against Provider/endpoint
circuits.
Introduce createDemandDrivenResponsePump to replace the TransformStream
+ tee() pipeline with a single high-water-mark-0 ReadableStream that
primes exactly one upstream chunk and never reads ahead past
unconsumed downstream demand. This eliminates unbounded buffering for
slow clients and removes the generic streaming stale watchdog.

Rewrite nodeStreamToWebStreamSafe to pause/resume the Node source on
controller.desiredSize backpressure, handle pre-existing
errored/destroyed/closed states before listener registration, and guard
delayed asynchronous destroy(error) events so they cannot surface as
uncaught exceptions after the Web stream has settled.

Defer all circuit-breaker, session-binding, and Codex cache side
effects into post-terminal tasks so a slow Redis call cannot turn an
already-completed billable request into a fallback 500 or block lease
settlement.
…ssage requests

Introduce enqueueMessageRequestUpdateDurably which returns a Promise
that resolves only after the batch SQL commits, using RETURNING id and
a status_code IS NULL fence to guarantee exactly-once terminal
persistence. If the durable write fails or the row is already
finalized, a conditional fallback (updateMessageRequestDetailsIfUnfinalized)
prevents overwriting an existing terminal status.

The write buffer now uses a dedicated writer-lane DB handle, an
evictable min-heap for priority-aware overflow dropping, and aggregated
overflow logging. Public-status rollup callbacks fire only after the
batch commit acknowledgement, preventing premature or duplicate
rollup emission when a timeout fallback races with a late primary
write.
… leases atomically

Consolidate all rolling-window and fixed-window cost writes into one
Redis pipeline per trackCost / trackUserDailyCost call, eliminating
sequential round-trips. The unified TRACK_COST_ROLLING_WINDOW script
is now write-only (cleanup + append + TTL repair) and no longer scans
the full ZSET on every successful request.

Add settleLeaseBudgets: a single Lua invocation that validates all
twelve lease keys, applies decrements, and writes an idempotency
marker in one atomic step. Replace twelve fire-and-forget
decrementLeaseBudget calls with one settlement, preventing duplicate
deductions on ioredis reconnect.

Configure commandTimeout, socketTimeout, and autoResendUnfulfilledCommands
on the shared Redis client so a TCP blackhole cannot grow the command
queue or replay timed-out writes after reconnection.
…uiescence

AsyncTaskManager.register now accepts a factory invoked after
admission rather than a pre-started Promise, enabling shutdown to
abort and join all pending generations—including tail tasks
registered by abort listeners. shutdownAll returns a shared Promise
that loops until the pending set is empty.

runApplicationCleanup treats async-task settlement, writer flush, and
DB pool close as non-detachable critical barriers: per-step timeouts
become soft warnings, and failures propagate to server.js which exits
non-zero. The hard-exit watchdog timer is now referenced so the
process stays alive long enough to report a truthful exit status.
Replace the raw headersToRecord helper with redactHeaders so
authorization, cookie, x-api-key, and set-cookie values are masked
before being written to Langfuse generation metadata, preventing
secret leakage in observability exports.
Verify that buildRedisOptionsForUrl sets commandTimeout, socketTimeout,
and autoResendUnfulfilledCommands on both redis:// and rediss:// URLs,
and that REDIS_COMMAND_TIMEOUT_MS overrides the defaults. Guards the
rate-limit performance commit against silent regressions in Redis
client hardening.
…callback

The public-status rollup test for timed-out durable waiters previously
typed onCommitted as () => void and invoked it with no arguments,
mismatching the real DurableMessageRequestUpdateOptions contract which
receives the committed MessageRequestUpdatePatch. Align the test mock
and invocation so the delayed-commit path exercises the actual callback
signature. Also fix import ordering in message.ts to satisfy the
formatter.
Replace fire-and-forget safeSend with a per-WebSocket outbound queue
that caps pending bytes at 1 MiB, serializes sends behind a single
in-flight callback, and pauses the upstream SSE response until the
client drains. Late callbacks from a closed socket are invalidated by
generation counter so they cannot deliver stale frames.

Guard the internal HTTP request body write with a 30 s drain timeout
and destroy both the request and response when the client vanishes
mid-payload. Error and close paths now send a structured error frame
before initiating the WebSocket close handshake so clients always
receive a terminal event.
Capture the sessionId at increment time into a dedicated variable so
the finally block decrements exactly the session it acquired, even if
ProxySession.fromContext or the guard pipeline mutates session state
before forwarding begins. Previously the finally block re-read
session.sessionId, which could be null or different by the time the
handler reached its cleanup path, leaking a concurrency slot.

Add unit tests covering early guard rejection, successful forwarding,
post-session error translation, and pre-session decode failures.
Replace the cloned-response reader pattern with a single
demand-driven response pump that feeds the client stream and observes
chunks for stats collection. The client now receives a new Response
wrapping the pump stream instead of the original upstream Response,
eliminating the unbounded clone that held a second copy of every
streaming byte in memory.

The pump arms a 60 s deadline on each unconsumed lookahead chunk so a
connected-but-non-reading client cannot pin the upstream connection
indefinitely. Client cancellation propagates to the source even when
the cancel observer throws, and reentrant cancel calls preserve the
first hard-cancel owner.

Response handler terminal paths now use durable persistence with
conditional fallback and bounded failure deadlines so a hanging
database cannot stall the streaming task indefinitely.
…king

Switch error-handler from separate duration and details writes to a
single updateMessageRequestDetailsDurably call that includes
durationMs, ensuring the Langfuse trace, persistence commit, and
status-tracker endRequest fire in deterministic order. The trace is
emitted first, the durable write is awaited, and only then is the
status tracker released — so a database failure surfaces as a
rejected handler rather than a silently orphaned trace.

Add comprehensive unit tests for client-safe message sanitisation,
override application, terminal status mapping, durable persistence
ordering, and the end-to-end terminal outcome contract.
…ansport controller

Replace combineAbortSignals polyfill with a dedicated
AbortController whose abort is driven by lightweight
bindClientAbortListener registrations on the response and client
signals. The client-signal listener is cleaned up as soon as the
forwarder obtains the upstream response, so a client disconnect after
headers no longer aborts the in-flight transport — only the response
controller retains that authority through stream consumption.

Add an integration test exercising real loopback transports through
the hedge lifecycle: winner settlement fences loser timers, each
launched transport releases its agent exactly once, database overload
is classified before fanout, and hedge winner/loser billing fires
exactly once per request.
Add a PostgreSQL integration test that exercises the async message
write buffer under row-level locks: mixed durable/ordinary batches
settle only after commit, timed-out primaries yield to fallback
ownership, retired pending patches are excluded from reinserted
generations, a saturated 5000-entry queue evicts non-terminal patches
while retaining terminal priority, and winner-cost retries respect
the bounded cadence with authoritative loser-cost accumulation.
Align test assertions with the production call site, which now
requires a timezone argument to resolve date presets deterministically.
…, and readback

Add a shared drizzle query tracing harness and comprehensive unit
tests for the message repository: terminal detail writes with
durable acknowledgement and CAS-guarded unfinalized claims, winner
and hedge-loser cost accounting with idempotent retries, session
stats aggregation with provider/model/cache-TTL grouping, paged
session request queries with adjacent-sequence lookup, usage-log
filtering with ledger fallback, and public readback projections for
single-request, session, and audit lookups.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

本次变更重构了数据库连接池、代理流生命周期、消息 durable 写入、Redis 限流结算、异步任务关停和 WebSocket 背压处理,并补充大量单元及集成测试覆盖相关时序、错误和资源释放行为。

Changes

Runtime and persistence flows

Layer / File(s) Summary
数据库连接池与作用域
src/drizzle/*, src/app/v1*/[...route]/route.ts
数据库按 data/control/writer lane 拆分连接池,并通过 AsyncLocalStorage 将数据路由绑定到请求作用域。
代理流与 WebSocket 控制
server.js, src/app/v1/_lib/proxy/*
新增出站背压、需求驱动响应泵、Node 流取消保护,以及流式终态和客户端断开处理。
Durable 消息写入
src/repository/message*.ts
新增 durable acknowledgement、终态 fence、优先级淘汰、提交后回调和 durable 详情更新 API。
Redis 计费与租约结算
src/lib/rate-limit/*, src/lib/redis/*
成本写入改用 pipeline,滚动窗口脚本改为通用写路径,并增加原子租约预算结算与幂等 marker。
异步任务与关停
src/lib/async-task-manager.ts, src/lib/lifecycle/shutdown.ts
任务注册改为 factory,关停等待所有 pending generation,并让 writer 与数据库池关闭保持关键屏障。
验证覆盖
tests/unit/*, tests/integration/*
新增数据库池、Redis、消息写入、代理 hedge、流生命周期、背压和关停测试。

Estimated code review effort: 5 (Critical) | ~180 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed 标题准确概括了本次 PR 的核心:全路径并发与资源所有权优化。
Description check ✅ Passed 描述与实际变更高度一致,覆盖了数据库分层、流式传输、持久化和资源管理等主要改动。
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/full-path-benchmark-optimization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added enhancement New feature or request area:core area:Rate Limit size/XL Extra Large PR (> 1000 lines) labels Jul 16, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces significant improvements to the message request persistence layer, including a durable write API, refined database connection pool management with lane isolation, and a robust shutdown orchestration process. My review identified two issues: a premature publication of status rollups for non-durable writes in updateMessageRequestDetails, and a redundant call to publishCommit in updateMessageRequestDetailsDurably after the await on the durable write promise.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/repository/message.ts
if (shouldQueuePublicStatusRollup) {
queuePublicStatusRollupForFinalDetails(id, details);
}
publishCommittedMessageRequestDetails(id, details);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This call to publishCommittedMessageRequestDetails is premature. enqueueMessageRequestUpdate is a fire-and-forget operation for the async writer, meaning the update is only buffered and not guaranteed to be committed to the database (it could be dropped on overflow or fail during the batch write). Publishing the commit to the public status rollup at this stage can lead to inconsistent data if the write never completes. This publication should only occur for writes that are confirmed to be durable.

Comment thread src/repository/message.ts
...options,
onCommitted: publishCommit,
});
publishCommit(details);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This call to publishCommit(details) appears to be redundant. The onCommitted callback passed to enqueueMessageRequestUpdateDurably is designed to be executed once the write is confirmed as part of a successful batch. The await on the preceding line ensures this function doesn't proceed until the durable write promise settles. Calling publishCommit again here is unnecessary. While the idempotency guard within publishCommit prevents duplicate rollups, this line should be removed for clarity and correctness.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (11)
tests/unit/proxy/proxy-forwarder-retry-limit.test.ts-328-340 (1)

328-340: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

不要 mock 掉本测试要验证的 admission 分类。

categorizeErrorAsync 被直接固定为数值 5 后,构造的 DbPoolAdmissionError cause 完全不影响结果;cause 链识别失效时该测试仍会通过。请调用真实分类器,并使用命名枚举而非序号。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/proxy-forwarder-retry-limit.test.ts` around lines 328 - 340,
Update the test around categorizeErrorAsync to use the real error classifier
instead of mocking it to numeric value 5, so the DbPoolAdmissionError cause
chain is actually exercised. Assert or configure the expected classification
with the named ErrorCategory enum member rather than a positional number, while
preserving the existing wrappedError and doForward rejection setup.
tests/unit/proxy/terminal-outcome-contract.test.ts-225-249 (1)

225-249: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

确保失败路径也停止全局 message writer。

stopMessageRequestWriteBuffer() 只在成功路径执行;前置断言失败时,模块级 writer 会残留并污染或挂起后续测试。请用 finally 先释放 releaseCommit,再等待并停止 writer。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/terminal-outcome-contract.test.ts` around lines 225 - 249,
更新该测试的清理流程,将 releaseCommit.resolve()、等待 flushPromise 以及
stopMessageRequestWriteBuffer() 放入 finally 中,确保前置断言失败时也会释放提交并停止全局 message
writer;保留现有成功路径断言和 handlePromise 响应验证。
tests/unit/proxy/response-handler-gemini-terminal.test.ts-119-123 (1)

119-123: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

不要吞掉托管终态任务的拒绝。

当前 helper 会忽略所有 rejected settlement,因此取消、idle reset 和 durable fallback 测试可能在后台任务失败时仍通过。请像同批其他测试一样汇总并抛出拒绝原因。

建议修改
 async function settleTasks(): Promise<void> {
   while (mocks.tasks.length > 0) {
-    await Promise.allSettled(mocks.tasks.splice(0, mocks.tasks.length));
+    const settlements = await Promise.allSettled(
+      mocks.tasks.splice(0, mocks.tasks.length)
+    );
+    const errors = settlements
+      .filter(
+        (result): result is PromiseRejectedResult =>
+          result.status === "rejected"
+      )
+      .map((result) => result.reason);
+    if (errors.length > 0) {
+      throw new AggregateError(errors, "Unexpected terminal task rejection");
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/response-handler-gemini-terminal.test.ts` around lines 119 -
123, 更新 settleTasks,使每批 mocks.tasks 的托管任务拒绝不会被忽略:汇总 Promise.allSettled 的
rejected 结果,并在存在拒绝时抛出其原因;继续等待后续批次任务,保留当前清空并重复处理动态新增任务的行为。
tests/integration/billing-model-source.test.ts-26-33 (1)

26-33: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

清理前应观察被中止的异步任务。

当前 beforeEach 在 abort 后立即清空 Promise 引用;遗留任务若随后拒绝或继续修改 mock,会产生未处理拒绝或跨用例污染。请在注册时附加拒绝观察,并在重置状态前等待已中止任务 settled。

建议修改
       asyncTasks.push(promise);
       asyncTaskControllers.set(promise, controller);
+      void promise.catch(() => undefined);
       return controller;
@@
-beforeEach(() => {
+beforeEach(async () => {
   vi.clearAllMocks();
   asyncTaskAdmissionOpen = false;
+  const pendingTasks = asyncTasks.splice(0, asyncTasks.length);
   for (const controller of asyncTaskControllers.values()) {
     controller.abort();
   }
-  asyncTasks.splice(0, asyncTasks.length);
+  await Promise.allSettled(pendingTasks);
   asyncTaskControllers.clear();
   asyncTaskAdmissionOpen = true;

Also applies to: 121-129

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/billing-model-source.test.ts` around lines 26 - 33,
在异步任务注册逻辑中为每个 promise 附加拒绝观察,避免任务后续拒绝形成未处理拒绝;更新 beforeEach 的清理流程,在清空 asyncTasks
和 asyncTaskControllers 前等待所有已中止任务 settled,确保遗留任务不会跨用例继续运行或修改 mock。
tests/unit/lib/redis/client.test.ts-37-39 (1)

37-39: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

恢复原始环境变量值,而不是无条件删除。

如果测试进程启动时已配置 REDIS_COMMAND_TIMEOUT_MS,当前清理会永久移除它并影响后续测试。请在修改前保存原值,并在 afterEach 中按原状态恢复。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/lib/redis/client.test.ts` around lines 37 - 39, 在测试环境变量清理逻辑中保存
REDIS_COMMAND_TIMEOUT_MS 的初始值,并更新 afterEach,使其根据初始状态恢复原值;若初始未设置则删除该变量,避免影响后续测试。
tests/unit/proxy/response-handler-lease-decrement.test.ts-584-593 (1)

584-593: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

不要将缺失的 5h 重置模式固化为正确结果。

该用例声称验证实体与窗口参数,但三个 "5h" 值均断言为 undefined。请为测试 provider、user、key 设置明确的 "fixed""rolling" 模式并验证传播,否则 5h 结算参数回归仍会通过。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/response-handler-lease-decrement.test.ts` around lines 584 -
593, Update the test around RateLimitService.settleLeaseBudgets to assign
explicit “5h” reset modes for the provider, user, and key fixtures, using fixed
or rolling values as appropriate, and assert those same modes are propagated in
the settlement entities instead of undefined. Keep the existing daily mode and
other settlement assertions unchanged.
tests/unit/proxy/response-handler-lease-decrement.test.ts-19-40 (1)

19-40: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

统一三处 AsyncTaskManager.register mock 的预中止行为。

生产实现不会为已中止的 controller 调用 factory;三处 mock 均会继续启动任务,可能掩盖取消后的副作用。

  • tests/unit/proxy/response-handler-lease-decrement.test.ts#L19-L40:在任务入队前检查 controller.signal.aborted
  • tests/unit/proxy/response-handler-hedge-loser-priority.test.ts#L39-L50:在执行 factory 前直接返回已中止的 controller。
  • tests/unit/proxy/response-handler-non200.test.ts#L22-L38:在创建 Promise 前补充相同检查。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/response-handler-lease-decrement.test.ts` around lines 19 -
40, Update all three AsyncTaskManager.register mocks to match production
cancellation behavior: in
tests/unit/proxy/response-handler-lease-decrement.test.ts (lines 19-40), check
controller.signal.aborted before enqueueing the task and return without invoking
factory; in tests/unit/proxy/response-handler-hedge-loser-priority.test.ts
(lines 39-50), return the already-aborted controller before executing factory;
and in tests/unit/proxy/response-handler-non200.test.ts (lines 22-38), add the
same aborted-signal check before creating the promise.
tests/unit/lib/rate-limit/lease-service.test.ts-1141-1153 (1)

1141-1153: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

请实际验证全部租约读取完成后才开始写入。

当前比较只证明 SETEX 位于 pendingWrites 循环内;即使整个写入循环被移到租约验证循环之前,测试仍会通过。请同时断言读取循环、结果编码和写入循环的先后顺序。

建议修改
+      const readLoop = script.indexOf("for keyIndex = 2, `#KEYS` do");
+      const encodeResults = script.indexOf("local encodedOk, encoded = pcall");
+      const writeLoop = script.indexOf("for writeIndex = 1, `#pendingWrites` do");
+
+      expect(readLoop).toBeLessThan(encodeResults);
+      expect(encodeResults).toBeLessThan(writeLoop);
-      expect(script.indexOf("for writeIndex = 1, `#pendingWrites` do")).toBeLessThan(
+      expect(writeLoop).toBeLessThan(
         script.indexOf('redis.call("SETEX", pendingWrite[1]')
       );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/lib/rate-limit/lease-service.test.ts` around lines 1141 - 1153,
Update the test for LeaseService.settleLeaseBudgets to assert the Lua script
ordering: the lease-reading/validation loop must complete before settlement
result encoding, and encoding must complete before the pendingWrites write loop.
Extend the existing script.indexOf checks around the visible pendingWrites and
SETEX markers without changing the production implementation.
src/repository/message.ts-494-496 (1)

494-496: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

移除代码注释中的 emoji。

建议修改
-  model?: string; // ⭐ 新增:支持更新重定向后的模型名称
+  model?: string; // 支持更新重定向后的模型名称
   actualResponseModel?: string | null; // 上游响应实际返回的模型名(audit 用途,不影响计费)
-  providerId?: number; // ⭐ 新增:支持更新最终供应商ID(重试切换后)
+  providerId?: number; // 支持更新最终供应商 ID(重试切换后)

As per coding guidelines, “Never use emoji characters in any code, comments, or string literals”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/repository/message.ts` around lines 494 - 496, Remove the emoji
characters from the comments on the model and providerId properties in the
relevant message type, while preserving the existing comment text and
declarations.

Source: Coding guidelines

src/app/v1/_lib/proxy/node-stream-to-web.test.ts-133-157 (1)

133-157: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

确保 uncaughtException 监听器在失败路径也被移除。

这些测试若在 removeListener() 前失败,会遗留全局异常处理器,进而污染或掩盖后续测试。请将每段注册后的测试逻辑包在 try/finally 中,并在 finally 删除监听器。

Also applies to: 300-333, 362-384

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/v1/_lib/proxy/node-stream-to-web.test.ts` around lines 133 - 157,
Update the tests registering process uncaughtException listeners, including the
cases around the delayed destroy, other failure-path test block, and the third
referenced test block, to wrap their assertions and awaits in try/finally. Move
each corresponding process.removeListener call into finally so listeners are
always removed even when the test fails, while preserving the existing
assertions.
tests/unit/server-response-write-backpressure.test.ts-114-114 (1)

114-114: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

afterEach 中恢复真实计时器。 vi.restoreAllMocks() 不会退出 fake-timer 模式;这个文件里有 vi.useFakeTimers(),但没有对应的 vi.useRealTimers(),后续用例可能继承假时钟。建议在清理里补上 vi.useRealTimers()

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/server-response-write-backpressure.test.ts` at line 114, 在该测试文件的
afterEach 清理中补充 vi.useRealTimers(),与现有的 vi.restoreAllMocks() 一起执行,确保
vi.useFakeTimers() 创建的假计时器在每个用例后被恢复为真实计时器。
🧹 Nitpick comments (1)
src/app/v1/_lib/proxy/demand-driven-response-pump.test.ts (1)

2-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

改用 @/ 路径别名。

建议修改
 } from "./demand-driven-response-pump";
+} from "`@/app/v1/_lib/proxy/demand-driven-response-pump`";

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Use path alias @/ to map to ./src/ for imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/v1/_lib/proxy/demand-driven-response-pump.test.ts` around lines 2 -
5, Update the imports in the demand-driven response pump test to use the
configured `@/` path alias mapped to src instead of the relative
"./demand-driven-response-pump" path, while preserving the imported symbols.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server.js`:
- Around line 414-417: 更新内部 HTTP response 回调,在写入 currentInternalReq 或
currentInternalRes 前检查 closed;客户端关闭后直接忽略已排队的迟到回调,避免重新注册响应并绕过
abortCurrentInternalReq 的清理流程。保留未关闭时现有的赋值行为。

In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 1693-1708: Update the non-stream terminal-details persistence flow
around messageContext to reuse persistNonStreamTerminalDetails() as the CAS
fallback when updateMessageRequestDetailsDurably() fails. Ensure
tracker.endRequest(messageContext.user.id, messageContext.id) runs from a
finally block so it executes on both successful and failed persistence paths.
- Around line 5246-5255: 修改 Error cause 处理逻辑,避免在 errorCause 中原样序列化任意 cause。围绕
ErrnoException.cause 仅提取允许的 name、code 及脱敏后的 message,过滤请求头、令牌、URL
和用户数据等敏感内容,再执行现有长度截断;保留不可安全提取时的安全降级行为。
- Around line 2444-2455: Remove the immediate
passthroughPump.cancelSource(reason) call from the bindClientAbortListener
callback after startDrain; client abort should only enter background draining.
Preserve the existing cancellation in onClientCancel if required, and let
bounded drain, idle, or response-timeout handling perform the final source
cancellation so terminal usage can be collected.

In `@src/drizzle/db.ts`:
- Around line 7-8: Update the imports in the db module to use the configured `@/`
path alias instead of relative "./" paths, targeting the existing
admitted-client and schema modules while preserving their imported symbols.

In `@src/lib/redis/lua-scripts.ts`:
- Around line 273-290: Require a non-empty, stable request ID for rolling cost
tracking and construct the Redis sorted-set member from that ID so concurrent
requests cannot overwrite one another. Update TRACK_COST_ROLLING_WINDOW in
src/lib/redis/lua-scripts.ts (lines 273-290) to reject empty request_id and use
it for an idempotent member; require and validate options.requestId in the
rolling paths at src/lib/rate-limit/service.ts (lines 977-995 and 1577-1593).

In `@src/repository/message.ts`:
- Around line 20-25: Replace the relative import in src/repository/message.ts
lines 20-25 with the `@/repository/message-write-buffer` alias, and replace the
relative import in src/lib/rate-limit/service.ts lines 94-99 with the
`@/lib/rate-limit/lease-service` alias. Preserve all imported symbols and
behavior.
- Around line 505-513: 更新 updateMessageRequestDetails 的普通 async 分支,移除
enqueueMessageRequestUpdate 后立即调用 publishCommittedMessageRequestDetails 的逻辑;仅在队列
flush 成功并确认 PostgreSQL 已提交后发布终态,或将携带终态字段的调用改为使用现有 durable API,确保未持久化的更新不会被
rollup 消费。

In `@tests/integration/db-pool-isolation-postgres.test.ts`:
- Around line 8-14: Ensure tests restore every modified process environment
variable: in tests/integration/db-pool-isolation-postgres.test.ts, move DSN and
DB_POOL_MAX setup into the non-skipped lifecycle, preserve their prior values,
and restore or delete them appropriately after the tests; in
tests/unit/lib/redis/client.test.ts, preserve REDIS_COMMAND_TIMEOUT_MS before
modification and restore it afterward, deleting it only when it was originally
unset.

In `@tests/unit/proxy/error-handler-client-message.test.ts`:
- Around line 101-119: 更新 error-handler.ts 中的错误解析流程,为
resolveFinalClientErrorMessage 传入 locale,并使用 next-intl
获取各状态码的通用上游错误翻译,避免写死简体中文。同步修改 error-handler-client-message.test.ts,将状态码断言按 5
个支持的语言参数化,并验证每种 locale 返回对应翻译。

In `@tests/unit/proxy/response-handler-client-abort-drain.test.ts`:
- Around line 23-24: Remove the duplicated inserted lines in
response-handler-client-abort-drain.test.ts, including the repeated
asyncTasks/registeredTasks declarations and the duplicate content at the
referenced later locations. Preserve the original declarations, parameters, and
valid expressions so the test compiles without duplicate declarations or invalid
syntax.

In `@tests/unit/repository/message-public-status-rollup.test.ts`:
- Around line 369-390: Update the test around updateMessageRequestDetailsDurably
to defer mockDbUpdateWhere with a pending Promise, then assert
mockQueuePublicStatusRollupWrite has not been called before resolving that
Promise. Release the deferred database write, flush microtasks, and assert the
rollup is queued afterward while preserving the existing sync-mode assertions.
- Around line 180-196: Strengthen the CAS WHERE-condition assertions in
updateMessageRequestDetailsIfUnfinalized tests: at
tests/unit/repository/message-public-status-rollup.test.ts:180-196, render the
SQL and parameters and assert the condition includes target ID 606; at :198-217,
assert ID 608 and the terminal-null condition on the async path; at :219-232,
assert ID 607 and status_code IS NULL before simulating CAS failure.

---

Minor comments:
In `@src/app/v1/_lib/proxy/node-stream-to-web.test.ts`:
- Around line 133-157: Update the tests registering process uncaughtException
listeners, including the cases around the delayed destroy, other failure-path
test block, and the third referenced test block, to wrap their assertions and
awaits in try/finally. Move each corresponding process.removeListener call into
finally so listeners are always removed even when the test fails, while
preserving the existing assertions.

In `@src/repository/message.ts`:
- Around line 494-496: Remove the emoji characters from the comments on the
model and providerId properties in the relevant message type, while preserving
the existing comment text and declarations.

In `@tests/integration/billing-model-source.test.ts`:
- Around line 26-33: 在异步任务注册逻辑中为每个 promise 附加拒绝观察,避免任务后续拒绝形成未处理拒绝;更新 beforeEach
的清理流程,在清空 asyncTasks 和 asyncTaskControllers 前等待所有已中止任务
settled,确保遗留任务不会跨用例继续运行或修改 mock。

In `@tests/unit/lib/rate-limit/lease-service.test.ts`:
- Around line 1141-1153: Update the test for LeaseService.settleLeaseBudgets to
assert the Lua script ordering: the lease-reading/validation loop must complete
before settlement result encoding, and encoding must complete before the
pendingWrites write loop. Extend the existing script.indexOf checks around the
visible pendingWrites and SETEX markers without changing the production
implementation.

In `@tests/unit/lib/redis/client.test.ts`:
- Around line 37-39: 在测试环境变量清理逻辑中保存 REDIS_COMMAND_TIMEOUT_MS 的初始值,并更新
afterEach,使其根据初始状态恢复原值;若初始未设置则删除该变量,避免影响后续测试。

In `@tests/unit/proxy/proxy-forwarder-retry-limit.test.ts`:
- Around line 328-340: Update the test around categorizeErrorAsync to use the
real error classifier instead of mocking it to numeric value 5, so the
DbPoolAdmissionError cause chain is actually exercised. Assert or configure the
expected classification with the named ErrorCategory enum member rather than a
positional number, while preserving the existing wrappedError and doForward
rejection setup.

In `@tests/unit/proxy/response-handler-gemini-terminal.test.ts`:
- Around line 119-123: 更新 settleTasks,使每批 mocks.tasks 的托管任务拒绝不会被忽略:汇总
Promise.allSettled 的 rejected 结果,并在存在拒绝时抛出其原因;继续等待后续批次任务,保留当前清空并重复处理动态新增任务的行为。

In `@tests/unit/proxy/response-handler-lease-decrement.test.ts`:
- Around line 584-593: Update the test around
RateLimitService.settleLeaseBudgets to assign explicit “5h” reset modes for the
provider, user, and key fixtures, using fixed or rolling values as appropriate,
and assert those same modes are propagated in the settlement entities instead of
undefined. Keep the existing daily mode and other settlement assertions
unchanged.
- Around line 19-40: Update all three AsyncTaskManager.register mocks to match
production cancellation behavior: in
tests/unit/proxy/response-handler-lease-decrement.test.ts (lines 19-40), check
controller.signal.aborted before enqueueing the task and return without invoking
factory; in tests/unit/proxy/response-handler-hedge-loser-priority.test.ts
(lines 39-50), return the already-aborted controller before executing factory;
and in tests/unit/proxy/response-handler-non200.test.ts (lines 22-38), add the
same aborted-signal check before creating the promise.

In `@tests/unit/proxy/terminal-outcome-contract.test.ts`:
- Around line 225-249: 更新该测试的清理流程,将 releaseCommit.resolve()、等待 flushPromise 以及
stopMessageRequestWriteBuffer() 放入 finally 中,确保前置断言失败时也会释放提交并停止全局 message
writer;保留现有成功路径断言和 handlePromise 响应验证。

In `@tests/unit/server-response-write-backpressure.test.ts`:
- Line 114: 在该测试文件的 afterEach 清理中补充 vi.useRealTimers(),与现有的 vi.restoreAllMocks()
一起执行,确保 vi.useFakeTimers() 创建的假计时器在每个用例后被恢复为真实计时器。

---

Nitpick comments:
In `@src/app/v1/_lib/proxy/demand-driven-response-pump.test.ts`:
- Around line 2-5: Update the imports in the demand-driven response pump test to
use the configured `@/` path alias mapped to src instead of the relative
"./demand-driven-response-pump" path, while preserving the imported symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef7158f8-7620-4220-ae59-aefafb11d89d

📥 Commits

Reviewing files that changed from the base of the PR and between 6fcb827 and 2e2b059.

📒 Files selected for processing (112)
  • .env.example
  • deploy/k8s/app/deployment.yaml
  • server.js
  • src/app/v1/[...route]/route.ts
  • src/app/v1/_lib/proxy-handler.ts
  • src/app/v1/_lib/proxy/demand-driven-response-pump.test.ts
  • src/app/v1/_lib/proxy/demand-driven-response-pump.ts
  • src/app/v1/_lib/proxy/error-handler.ts
  • src/app/v1/_lib/proxy/errors.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/node-stream-to-web.test.ts
  • src/app/v1/_lib/proxy/node-stream-to-web.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1beta/[...route]/route.ts
  • src/drizzle/admitted-client.ts
  • src/drizzle/db.ts
  • src/lib/async-task-manager.ts
  • src/lib/config/env.schema.ts
  • src/lib/langfuse/trace-proxy-request.ts
  • src/lib/lifecycle/shutdown.ts
  • src/lib/price-sync/cloud-price-updater.ts
  • src/lib/provider-testing/test-service.test.ts
  • src/lib/rate-limit/lease-service.ts
  • src/lib/rate-limit/service.ts
  • src/lib/redis/client.ts
  • src/lib/redis/lua-scripts.ts
  • src/lib/utils/upstream-error-detection.test.ts
  • src/repository/message-write-buffer.ts
  • src/repository/message.ts
  • tests/configs/integration.config.ts
  • tests/integration/billing-model-source.test.ts
  • tests/integration/db-pool-isolation-postgres.test.ts
  • tests/integration/db-pool-slow-close-postgres.test.ts
  • tests/integration/lease-settlement-redis.test.ts
  • tests/integration/message-write-buffer-recovery-postgres.test.ts
  • tests/integration/proxy-hedge-lifecycle.test.ts
  • tests/integration/rolling-cost-redis.test.ts
  • tests/unit/actions/providers-patch-contract.test.ts
  • tests/unit/api/actions/legacy-deprecation.test.ts
  • tests/unit/api/v1/status-code-map.test.ts
  • tests/unit/dashboard/user-insights-page.test.tsx
  • tests/unit/drizzle/db-admission.test.ts
  • tests/unit/drizzle/db-pool-config.test.ts
  • tests/unit/drizzle/db-scope.test.ts
  • tests/unit/drizzle/db-shutdown.test.ts
  • tests/unit/i18n/key-created-copy.test.ts
  • tests/unit/instrumentation-crash-handler.test.ts
  • tests/unit/langfuse/langfuse-trace.test.ts
  • tests/unit/lib/async-task-manager-edge-runtime.test.ts
  • tests/unit/lib/provider-allowed-model-schema.test.ts
  • tests/unit/lib/provider-model-redirect-schema.test.ts
  • tests/unit/lib/rate-limit/lease-service.test.ts
  • tests/unit/lib/rate-limit/rolling-window-5h.test.ts
  • tests/unit/lib/rate-limit/rolling-window-cache-warm.test.ts
  • tests/unit/lib/rate-limit/service-extra.test.ts
  • tests/unit/lib/redis/client.test.ts
  • tests/unit/lib/shutdown.test.ts
  • tests/unit/lib/upstream-error-detection-status.test.ts
  • tests/unit/price-sync/cloud-price-updater.test.ts
  • tests/unit/proxy/client-abort-vs-upstream-499.test.ts
  • tests/unit/proxy/client-detector.test.ts
  • tests/unit/proxy/codex-provider-overrides.test.ts
  • tests/unit/proxy/connected-non-reader-lifetime.test.ts
  • tests/unit/proxy/endpoint-family-catalog.test.ts
  • tests/unit/proxy/endpoint-family-provider-routing.test.ts
  • tests/unit/proxy/endpoint-path-normalization.test.ts
  • tests/unit/proxy/error-handler-client-message.test.ts
  • tests/unit/proxy/error-handler-durable-persistence.test.ts
  • tests/unit/proxy/error-handler-langfuse-trace.test.ts
  • tests/unit/proxy/error-handler-overrides.test.ts
  • tests/unit/proxy/error-handler-terminal-status.test.ts
  • tests/unit/proxy/fake-streaming-response-validator.test.ts
  • tests/unit/proxy/fake-streaming-response.test.ts
  • tests/unit/proxy/fake-streaming-stream-intent.test.ts
  • tests/unit/proxy/pricing-no-price.test.ts
  • tests/unit/proxy/provider-selector-cross-type-model.test.ts
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
  • tests/unit/proxy/proxy-forwarder-retry-limit.test.ts
  • tests/unit/proxy/proxy-handler-concurrency-ownership.test.ts
  • tests/unit/proxy/proxy-handler-public-errors.test.ts
  • tests/unit/proxy/proxy-handler-public-success.test.ts
  • tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts
  • tests/unit/proxy/response-handler-bill-non-success.test.ts
  • tests/unit/proxy/response-handler-client-abort-drain.test.ts
  • tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts
  • tests/unit/proxy/response-handler-exported-finalizers.test.ts
  • tests/unit/proxy/response-handler-gemini-stream-passthrough-timeouts.test.ts
  • tests/unit/proxy/response-handler-gemini-terminal.test.ts
  • tests/unit/proxy/response-handler-hedge-loser-priority.test.ts
  • tests/unit/proxy/response-handler-lease-decrement.test.ts
  • tests/unit/proxy/response-handler-non200.test.ts
  • tests/unit/proxy/response-handler-nonstream-terminal.test.ts
  • tests/unit/proxy/response-handler-public-dispatch.test.ts
  • tests/unit/proxy/response-handler-stream-terminal.test.ts
  • tests/unit/proxy/session.test.ts
  • tests/unit/proxy/terminal-outcome-contract.test.ts
  • tests/unit/repository/message-aggregate-multiple-session-stats.test.ts
  • tests/unit/repository/message-aggregate-session-stats.test.ts
  • tests/unit/repository/message-public-readback.test.ts
  • tests/unit/repository/message-public-status-rollup.test.ts
  • tests/unit/repository/message-query-test-support.ts
  • tests/unit/repository/message-session-readback.test.ts
  • tests/unit/repository/message-session-request-query.test.ts
  • tests/unit/repository/message-terminal-cas-durable.test.ts
  • tests/unit/repository/message-terminal-cost-accounting.test.ts
  • tests/unit/repository/message-terminal-public-status-seam.test.ts
  • tests/unit/repository/message-terminal-write-apis.test.ts
  • tests/unit/repository/message-usage-logs-query.test.ts
  • tests/unit/repository/message-write-buffer.test.ts
  • tests/unit/server-response-write-backpressure.test.ts
  • tests/unit/server-shutdown.test.ts

Comment thread server.js
Comment on lines +414 to 417
(clientReq, clientRes) => {
currentInternalReq = clientReq;
if (clientRes) currentInternalRes = clientRes;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

关闭后不要重新注册迟到的内部响应。

客户端关闭可能先执行 abortCurrentInternalReq(),随后已排队的 HTTP response 回调再次写入 currentInternalRes。由于 closed 已为 true,后续不会再清理该响应,上游工作可能继续运行和计费。

建议修复
       (clientReq, clientRes) => {
+        if (closed) {
+          try {
+            if (!clientReq.destroyed) clientReq.destroy();
+          } catch {}
+          try {
+            if (clientRes && !clientRes.destroyed) clientRes.destroy();
+          } catch {}
+          return;
+        }
         currentInternalReq = clientReq;
         if (clientRes) currentInternalRes = clientRes;
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(clientReq, clientRes) => {
currentInternalReq = clientReq;
if (clientRes) currentInternalRes = clientRes;
},
(clientReq, clientRes) => {
if (closed) {
try {
if (!clientReq.destroyed) clientReq.destroy();
} catch {}
try {
if (clientRes && !clientRes.destroyed) clientRes.destroy();
} catch {}
return;
}
currentInternalReq = clientReq;
if (clientRes) currentInternalRes = clientRes;
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.js` around lines 414 - 417, 更新内部 HTTP response 回调,在写入
currentInternalReq 或 currentInternalRes 前检查
closed;客户端关闭后直接忽略已排队的迟到回调,避免重新注册响应并绕过 abortCurrentInternalReq
的清理流程。保留未关闭时现有的赋值行为。

Comment on lines +1693 to +1708
if (messageContext) {
const duration = Date.now() - session.startTime;
await updateMessageRequestDuration(messageContext.id, duration);
await updateMessageRequestDetailsDurably(messageContext.id, {
statusCode: finalizedStatusCode,
...errorDetails,
ttfbMs: session.ttfbMs ?? duration,
providerChain: session.getProviderChain(),
model: session.getCurrentModel() ?? undefined,
providerId: session.provider?.id,
context1mApplied: session.getContext1mApplied(),
swapCacheTtlApplied: session.provider?.swapCacheTtlBilling ?? false,
specialSettings: session.getSpecialSettings() ?? undefined,
});
const tracker = ProxyStatusTracker.getInstance();
tracker.endRequest(messageContext.user.id, messageContext.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

为 Gemini 非流式错误终态补上 CAS fallback。

这里的 durable 写入一旦失败,控制流只会进入外层日志处理;不会调用 updateMessageRequestDetailsIfUnfinalizedtracker.endRequest 也不会执行。请复用 persistNonStreamTerminalDetails(),并在 finally 中结束 tracker。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/v1/_lib/proxy/response-handler.ts` around lines 1693 - 1708, Update
the non-stream terminal-details persistence flow around messageContext to reuse
persistNonStreamTerminalDetails() as the CAS fallback when
updateMessageRequestDetailsDurably() fails. Ensure
tracker.endRequest(messageContext.user.id, messageContext.id) runs from a
finally block so it executes on both successful and failed persistence paths.

Comment on lines +2444 to +2455
onClientCancel: (reason) => {
passthroughPump.startDrain(reason);
passthroughPump.cancelSource(reason);
},
});
const cleanupPassthroughClientAbortListener = bindClientAbortListener(
session.clientAbortSignal,
() => {
const reason = session.clientAbortSignal?.reason;
passthroughPump.startDrain(reason);
passthroughPump.cancelSource(reason);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

进入后台 drain 后不要立即取消源流。

startDrain() 后立刻调用 cancelSource() 会马上 settle 并取消 reader,终态 usage 无法继续收集,客户端断开将被错误结算为未计费的 499。应仅由有界 drain、idle 或 response timeout 负责最终取消。

建议修复
           onClientCancel: (reason) => {
             passthroughPump.startDrain(reason);
-            passthroughPump.cancelSource(reason);
           },
...
             const reason = session.clientAbortSignal?.reason;
             passthroughPump.startDrain(reason);
-            passthroughPump.cancelSource(reason);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onClientCancel: (reason) => {
passthroughPump.startDrain(reason);
passthroughPump.cancelSource(reason);
},
});
const cleanupPassthroughClientAbortListener = bindClientAbortListener(
session.clientAbortSignal,
() => {
const reason = session.clientAbortSignal?.reason;
passthroughPump.startDrain(reason);
passthroughPump.cancelSource(reason);
}
onClientCancel: (reason) => {
passthroughPump.startDrain(reason);
},
});
const cleanupPassthroughClientAbortListener = bindClientAbortListener(
session.clientAbortSignal,
() => {
const reason = session.clientAbortSignal?.reason;
passthroughPump.startDrain(reason);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/v1/_lib/proxy/response-handler.ts` around lines 2444 - 2455, Remove
the immediate passthroughPump.cancelSource(reason) call from the
bindClientAbortListener callback after startDrain; client abort should only
enter background draining. Preserve the existing cancellation in onClientCancel
if required, and let bounded drain, idle, or response-timeout handling perform
the final source cancellation so terminal usage can be collected.

Comment on lines +5246 to +5255
let errorCause: string | undefined;
if (error instanceof Error && (error as NodeJS.ErrnoException).cause) {
try {
const cause = (error as NodeJS.ErrnoException).cause;
errorCause = JSON.stringify(cause, Object.getOwnPropertyNames(cause as object));
} catch {
errorCause = String((error as NodeJS.ErrnoException).cause);
}
if (errorCause && errorCause.length > maxErrorCauseLength) {
errorCause = `${errorCause.substring(0, maxErrorCauseLength)}...[truncated]`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

不要原样持久化任意 Error.cause

cause 可能包含请求头、认证令牌、完整 URL 或用户数据;长度截断并不能脱敏。请仅保留允许字段(如 namecode 和脱敏后的 message),避免敏感信息进入数据库。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/v1/_lib/proxy/response-handler.ts` around lines 5246 - 5255, 修改 Error
cause 处理逻辑,避免在 errorCause 中原样序列化任意 cause。围绕 ErrnoException.cause 仅提取允许的
name、code 及脱敏后的 message,过滤请求头、令牌、URL 和用户数据等敏感内容,再执行现有长度截断;保留不可安全提取时的安全降级行为。

Comment thread src/drizzle/db.ts
Comment on lines +7 to +8
import { createAdmittedSqlClient } from "./admitted-client";
import * as schema from "./schema";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

请对 src 内部模块使用 @/ 路径别名。

这两个相对导入不符合仓库的 TypeScript 导入约定。

建议修改
-import { createAdmittedSqlClient } from "./admitted-client";
-import * as schema from "./schema";
+import { createAdmittedSqlClient } from "`@/drizzle/admitted-client`";
+import * as schema from "`@/drizzle/schema`";

As per coding guidelines,TypeScript 导入必须使用 @/ 路径别名映射到 ./src/

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createAdmittedSqlClient } from "./admitted-client";
import * as schema from "./schema";
import { createAdmittedSqlClient } from "`@/drizzle/admitted-client`";
import * as schema from "`@/drizzle/schema`";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/drizzle/db.ts` around lines 7 - 8, Update the imports in the db module to
use the configured `@/` path alias instead of relative "./" paths, targeting the
existing admitted-client and schema modules while preserving their imported
symbols.

Source: Coding guidelines

Comment on lines +8 to +14
if (!process.env.DSN && process.env.DATABASE_URL) {
process.env.DSN = process.env.DATABASE_URL;
}

const previousPoolMax = process.env.DB_POOL_MAX;
process.env.DB_POOL_MAX = "6";
vi.resetModules();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

请保证测试完整恢复其修改的进程环境。

两个测试都可能覆盖调用方预先配置的环境变量,导致同一 Vitest worker 中的后续测试使用错误配置。

  • tests/integration/db-pool-isolation-postgres.test.ts#L8-L14:仅在非跳过的生命周期内设置 DSNDB_POOL_MAX,并在结束时恢复两者。
  • tests/unit/lib/redis/client.test.ts#L37-L39:保存测试前的 REDIS_COMMAND_TIMEOUT_MS,结束后恢复原值;仅当原值不存在时才删除。
📍 Affects 2 files
  • tests/integration/db-pool-isolation-postgres.test.ts#L8-L14 (this comment)
  • tests/unit/lib/redis/client.test.ts#L37-L39
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/db-pool-isolation-postgres.test.ts` around lines 8 - 14,
Ensure tests restore every modified process environment variable: in
tests/integration/db-pool-isolation-postgres.test.ts, move DSN and DB_POOL_MAX
setup into the non-skipped lifecycle, preserve their prior values, and restore
or delete them appropriately after the tests; in
tests/unit/lib/redis/client.test.ts, preserve REDIS_COMMAND_TIMEOUT_MS before
modification and restore it afterward, deleting it only when it was originally
unset.

Comment on lines +101 to +119
test.each([
[400, "上游请求参数无效,请检查后重试"],
[401, "上游鉴权失败,请稍后重试"],
[429, "上游服务当前限流,请稍后重试"],
[503, "上游服务暂时不可用,请稍后重试"],
])("maps status %i to a generic message when pass-through is disabled", (status, expected) => {
const error = new ProxyError("sensitive upstream failure", status, {
body: "sensitive upstream failure",
});

const message = resolveFinalClientErrorMessage({
error,
currentFallbackMessage: "sensitive upstream failure Upstream detail: relay-a",
settings: { passThroughUpstreamErrorMessage: false },
override: null,
});

expect(message).toBe(expected);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File outline ==\n'
ast-grep outline tests/unit/proxy/error-handler-client-message.test.ts --view expanded || true

printf '\n== Relevant snippets ==\n'
sed -n '1,220p' tests/unit/proxy/error-handler-client-message.test.ts | cat -n

printf '\n== Search for message resolver and i18n usage ==\n'
rg -n "resolveFinalClientErrorMessage|passThroughUpstreamErrorMessage|next-intl|useTranslations|getTranslations|locale" src tests -g '!**/dist/**' -g '!**/build/**' || true

printf '\n== Files that look relevant ==\n'
fd -a "error-handler-client-message" src tests || true

Repository: ding113/claude-code-hub

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== test file focused slice ==\n'
sed -n '1,220p' tests/unit/proxy/error-handler-client-message.test.ts | cat -n

printf '\n== resolver implementation locations ==\n'
rg -n "function resolveFinalClientErrorMessage|const resolveFinalClientErrorMessage|resolveFinalClientErrorMessage\\(" src tests -g '!**/dist/**' -g '!**/build/**' | head -n 50

printf '\n== proxy error message implementation slice ==\n'
FILE="$(rg -l "resolveFinalClientErrorMessage" src tests -g '!**/dist/**' -g '!**/build/**' | head -n 1)"
if [ -n "$FILE" ]; then
  sed -n '1,260p' "$FILE" | cat -n
fi

Repository: ding113/claude-code-hub

Length of output: 16844


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== error-handler implementation slice ==\n'
sed -n '1,220p' src/app/v1/_lib/proxy/error-handler.ts | cat -n

printf '\n== search for related status messages and i18n keys ==\n'
rg -n "上游请求参数无效|上游鉴权失败|上游服务当前限流|上游服务暂时不可用|getTranslations|next-intl|resolveFinalClientErrorMessage" src messages tests -g '!**/dist/**' -g '!**/build/**' | head -n 200

printf '\n== locale message files around proxy/error related keys ==\n'
fd -a "messages" . || true

Repository: ding113/claude-code-hub

Length of output: 32942


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== locale usage in proxy error handling ==\n'
rg -n "getLocale|locale" src/app/v1/_lib/proxy/error-handler.ts src/app/v1/_lib/proxy/*.ts -g '!**/dist/**' -g '!**/build/**' || true

printf '\n== related client error message helper ==\n'
sed -n '1,220p' src/app/v1/_lib/proxy/client-error-message.ts | cat -n

printf '\n== proxy error-related message keys in messages ==\n'
rg -n "upstream|error|proxy|limit|鉴权|限流|不可用|参数无效" messages src/app/v1 -g '!**/dist/**' -g '!**/build/**' | head -n 200

Repository: ding113/claude-code-hub

Length of output: 31888


不要把通用上游错误写死为简体中文。 src/app/v1/_lib/proxy/error-handler.ts:78-107 里的状态码兜底文案目前直接返回 zh-CN 字符串,tests/unit/proxy/error-handler-client-message.test.ts:101-136 也把这些文案锁死了;这里需要把 locale 传入错误解析并通过 next-intl 取翻译,再把测试改成按 5 个支持语言参数化。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/error-handler-client-message.test.ts` around lines 101 -
119, 更新 error-handler.ts 中的错误解析流程,为 resolveFinalClientErrorMessage 传入 locale,并使用
next-intl 获取各状态码的通用上游错误翻译,避免写死简体中文。同步修改
error-handler-client-message.test.ts,将状态码断言按 5 个支持的语言参数化,并验证每种 locale 返回对应翻译。

Source: Coding guidelines

Comment on lines 23 to +24
const asyncTasks: Promise<void>[] = [];
const registeredTasks: Array<{ taskType: string; promise: Promise<void> }> = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

删除重复插入的代码行。

Line 24、Line 39、Line 1382 和 Line 2133 的重复内容会分别造成重复声明、重复参数或无效表达式,导致 TypeScript 编译失败。

建议修复
 const registeredTasks: Array<{ taskType: string; promise: Promise<void> }> = [];
-const registeredTasks: Array<{ taskType: string; promise: Promise<void> }> = [];

 options?: string | { abortController?: AbortController; taskType?: string }
-options?: string | { abortController?: AbortController; taskType?: string }

 let resolveBinding!: (result: { updated: boolean; reason: string }) => void;
-let resolveBinding!: (result: { updated: boolean; reason: string }) => void;

 const calls = (
   updateMessageRequestDetailsDurably as unknown as { mock: { calls: unknown[][] } }
-  updateMessageRequestDetailsDurably as unknown as { mock: { calls: unknown[][] } }
 ).mock.calls;

Also applies to: 35-39, 1378-1383, 2131-2134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/proxy/response-handler-client-abort-drain.test.ts` around lines 23
- 24, Remove the duplicated inserted lines in
response-handler-client-abort-drain.test.ts, including the repeated
asyncTasks/registeredTasks declarations and the duplicate content at the
referenced later locations. Preserve the original declarations, parameters, and
valid expressions so the test compiles without duplicate declarations or invalid
syntax.

Comment on lines +180 to +196
it("writes timeout fallback details only while the request is unfinalized", async () => {
mockWriterDbUpdateReturning.mockResolvedValueOnce([{ id: 606 }]);
const { updateMessageRequestDetailsIfUnfinalized } = await import("@/repository/message");

await updateMessageRequestDetailsIfUnfinalized(606, {
statusCode: 500,
errorMessage: "Error: stream_finalization_timeout",
});

expect(mockGetMessageWriterDb).toHaveBeenCalledTimes(1);
expect(mockWriterDbUpdateWhere).toHaveBeenCalledTimes(1);
expect(mockWriterDbUpdateReturning).toHaveBeenCalledWith({ id: "id" });
expect(mockDbUpdateSet).not.toHaveBeenCalled();
const whereSql = sqlToString(mockWriterDbUpdateWhere.mock.calls[0]?.[0]).toLowerCase();
expect(whereSql).toContain("statuscode");
expect(whereSql).toContain("is null");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

请验证终态 CAS 的 WHERE 条件同时包含目标消息 ID。

这些用例只检查 status_code IS NULL,或直接模拟空 RETURNING。如果 ID 谓词意外丢失,测试仍会通过,而 fallback 更新可能影响全部未终态请求。

  • tests/unit/repository/message-public-status-rollup.test.ts#L180-L196: 将条件渲染为 SQL 与参数,并断言包含 ID 606
  • tests/unit/repository/message-public-status-rollup.test.ts#L198-L217: 同时断言异步路径包含 ID 608 和终态空值条件。
  • tests/unit/repository/message-public-status-rollup.test.ts#L219-L232: 在模拟 CAS 失败前断言条件包含 ID 607status_code IS NULL
📍 Affects 1 file
  • tests/unit/repository/message-public-status-rollup.test.ts#L180-L196 (this comment)
  • tests/unit/repository/message-public-status-rollup.test.ts#L198-L217
  • tests/unit/repository/message-public-status-rollup.test.ts#L219-L232
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/repository/message-public-status-rollup.test.ts` around lines 180
- 196, Strengthen the CAS WHERE-condition assertions in
updateMessageRequestDetailsIfUnfinalized tests: at
tests/unit/repository/message-public-status-rollup.test.ts:180-196, render the
SQL and parameters and assert the condition includes target ID 606; at :198-217,
assert ID 608 and the terminal-null condition on the async path; at :219-232,
assert ID 607 and status_code IS NULL before simulating CAS failure.

Comment on lines +369 to +390
it("sync durable details remain direct and queue rollup after the DB write", async () => {
mockGetEnvConfig.mockReturnValue({ MESSAGE_REQUEST_WRITE_MODE: "sync" });
mockDbSelectLimit.mockResolvedValueOnce([
{
createdAt: new Date("2026-04-21T10:04:00.000Z"),
model: "gpt-4.1",
originalModel: "gpt-4.1",
durationMs: 100,
},
]);
const { updateMessageRequestDetailsDurably } = await import("@/repository/message");

await updateMessageRequestDetailsDurably(809, {
statusCode: 200,
providerChain: [{ id: 1, name: "provider-a", groupTag: "openai" }],
});
await flushMicrotasks();

expect(mockDbUpdateSet).toHaveBeenCalledTimes(1);
expect(mockGetMessageWriterDb).not.toHaveBeenCalled();
expect(mockEnqueueMessageRequestUpdateDurably).not.toHaveBeenCalled();
expect(mockQueuePublicStatusRollupWrite).toHaveBeenCalledTimes(1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

请用延迟 DB Promise 验证 rollup 确实发生在写入完成之后。

当前用例只验证 DB 更新和 rollup 都被调用;若实现先发布 rollup、后等待数据库,测试仍会通过。请挂起 mockDbUpdateWhere,先断言未入队,再释放写入并断言入队。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/repository/message-public-status-rollup.test.ts` around lines 369
- 390, Update the test around updateMessageRequestDetailsDurably to defer
mockDbUpdateWhere with a pending Promise, then assert
mockQueuePublicStatusRollupWrite has not been called before resolving that
Promise. Release the deferred database write, flush microtasks, and assert the
rollup is queued afterward while preserving the existing sync-mode assertions.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e2b059bf4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

onChunk: (value) => observePassthroughChunk(value),
onClientCancel: (reason) => {
passthroughPump.startDrain(reason);
passthroughPump.cancelSource(reason);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve Gemini passthrough drain after client cancel

When a Gemini passthrough client stops reading the returned stream, this cancelSource() call immediately cancels the only upstream reader right after startDrain(). That defeats the new bounded drain behavior used by the normal streaming path, so a stream that would have produced terminal usage/status within the drain window is finalized from partial content as a client abort instead of being billed and recorded accurately. Let the pump drain on downstream cancel and only cancel on the explicit drain timeout/idle timeout paths.

Useful? React with 👍 / 👎.

Comment thread server.js
? parsed.error
: { code: `http_${res.statusCode}`, message: text.slice(0, 512) },
},
{ response: res, onSuccess: settleResponse, onFailure: settleAndClose }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Settle JSON responses before the normal close event

For WebSocket /v1/responses turns that get a non-SSE JSON response, this now waits for the WebSocket send callback before marking the internal response settled. http.IncomingMessage emits close after a normal end, so if the WS callback is delayed by the new outbound queue, the close handler below still sees responseSettled === false and sends internal_response_closed/closes the socket even though the JSON body was complete. Mark the HTTP response complete after parsing end (or ignore close after end) while keeping the WS send queued.

Useful? React with 👍 / 👎.

Comment on lines +606 to +610
const rawResult = (await redis.eval(
LeaseService.SETTLE_LEASE_BUDGETS_LUA_SCRIPT,
1 + leaseKeys.length,
markerKey,
...leaseKeys,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep lease settlement keys in one Redis cluster slot

In Redis Cluster deployments, this EVAL now passes the marker plus twelve lease keys such as lease:key:..., lease:user:..., and lease:provider:... without a shared hash tag, so the script is rejected as a cross-slot multi-key command. The catch path turns that into fail_open, which means successful requests stop decrementing cached lease budgets until the next DB refresh and can exceed quota slices; put all settlement keys in the same hash slot or avoid a multi-key script for cluster setups.

Useful? React with 👍 / 👎.

Comment on lines +144 to +178
logger.error("ResponseHandler: Durable non-stream terminal persistence failed", {
taskId: options.taskId,
messageId: options.messageRequestId,
statusCode: options.details.statusCode,
error: primaryError,
});
}

if (provider.firstByteTimeoutStreamingMs > 0) {
return Math.max(provider.firstByteTimeoutStreamingMs, provider.streamingIdleTimeoutMs);
try {
await updateMessageRequestDetailsIfUnfinalized(
options.messageRequestId,
completeTerminalDetails
);
} catch (fallbackError) {
logger.error("ResponseHandler: Conditional non-stream terminal fallback failed", {
taskId: options.taskId,
messageId: options.messageRequestId,
statusCode: options.details.statusCode,
fallbackError,
});
throw markNonStreamTerminalPersistenceError(fallbackError);
}
}

return Number.POSITIVE_INFINITY;
function raceWithTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
let timeoutId: NodeJS.Timeout | null = null;
const timeoutPromise = new Promise<never>((_resolve, reject) => {
timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
timeoutId.unref?.();
});

return Promise.race([promise, timeoutPromise]).finally(() => {
if (timeoutId) {
clearTimeout(timeoutId);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 AbortSignal ignored in post-terminal task factory

The factory passed to AsyncTaskManager.register accepts an AbortSignal parameter but the implementation never threads it into the work it schedules. When the pod receives SIGTERM, shutdownAllAsyncTasks() fires the signal but the in-flight finalization work continues uninterrupted — delaying pod shutdown by up to STREAM_FINALIZATION_MAX_MS = 120_000 ms regardless of the shutdown deadline.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/response-handler.ts
Line: 144-178

Comment:
**AbortSignal ignored in post-terminal task factory**

The factory passed to `AsyncTaskManager.register` accepts an `AbortSignal` parameter but the implementation never threads it into the work it schedules. When the pod receives SIGTERM, `shutdownAllAsyncTasks()` fires the signal but the in-flight finalization work continues uninterrupted — delaying pod shutdown by up to `STREAM_FINALIZATION_MAX_MS = 120_000` ms regardless of the shutdown deadline.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +195 to +260
export function createAdmittedSqlClient<TClient extends object>(
client: TClient,
options: AdmittedClientOptions
): TClient {
const rawClient = client as unknown as UnsafeAndBeginClient;
const originalUnsafe = rawClient.unsafe.bind(client);
const originalBegin = rawClient.begin.bind(client);
let outstanding = 0;

const acquire = () => {
if (outstanding >= options.maxOutstanding) {
throw new DbPoolAdmissionError(options.pool, options.maxOutstanding);
}
outstanding += 1;
let released = false;
return () => {
if (released) return;
released = true;
outstanding -= 1;
};
};

const admittedUnsafe = (...args: unknown[]) => {
const release = acquire();
try {
return wrapPendingQuery(originalUnsafe(...args), release);
} catch (error) {
release();
throw error;
}
};

const admittedBegin = (...args: unknown[]) => {
const release = acquire();
let result: unknown;
try {
result = originalBegin(...args);
} catch (error) {
release();
throw error;
}

if (!isPromiseLike(result)) {
release();
return result;
}
return Promise.resolve(result).then(
(value) => {
release();
return value;
},
(error) => {
release();
throw error;
}
);
};

return new Proxy(client, {
get(target, property, receiver) {
if (property === "unsafe") return admittedUnsafe;
if (property === "begin") return admittedBegin;
return Reflect.get(target, property, receiver);
},
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Tag template literal calls bypass admission control

The createAdmittedSqlClient Proxy intercepts named properties (unsafe, begin) but has no apply trap. When Drizzle calls the client as a tag template literal (e.g. client`SELECT 1`), the call goes directly to the underlying postgres.js client, completely bypassing the outstanding-query counter. Under pool pressure this silently removes the back-pressure guarantee for the most common query path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/drizzle/admitted-client.ts
Line: 195-260

Comment:
**Tag template literal calls bypass admission control**

The `createAdmittedSqlClient` Proxy intercepts named properties (`unsafe`, `begin`) but has no `apply` trap. When Drizzle calls the client as a tag template literal (e.g. `` client`SELECT 1` ``), the call goes directly to the underlying `postgres.js` client, completely bypassing the outstanding-query counter. Under pool pressure this silently removes the back-pressure guarantee for the most common query path.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +124 to +158
const startDrain = (_reason?: unknown) => {
if (settled || state === "finalizing" || state === "closed") return;
if (state === "draining") {
scheduleDrain();
return;
}
clientAborted = true;
state = "draining";
try {
clientController?.error(
_reason == null ? new Error("Client disconnected") : toError(_reason)
);
} catch (controllerError) {
if (!(controllerError instanceof TypeError)) throw controllerError;
// The ReadableStream cancel algorithm may have already detached the controller.
}
scheduleDrain();
};

const cancelSource = (reason?: unknown) => {
if (settled) return;
const normalized = reason == null ? new Error("Source cancelled") : toError(reason);
settle(false, normalized, normalized);
};

const armPendingChunkDeadline = () => {
clearPendingChunkDeadline();
pendingChunkDeadlineId = setTimeout(() => {
const error = new DOMException(
`Client response body was not consumed within ${PENDING_CHUNK_DEADLINE_MS}ms`,
"AbortError"
);
startDrain(error);
cancelSource(error);
}, PENDING_CHUNK_DEADLINE_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 clientAborted conflates client cancel and server-side drain

startDrain sets clientAborted = true unconditionally. It is called both when the client cancels and when armPendingChunkDeadline fires after 60s of unconsumed chunk. In the deadline case the provider delivered data that the client simply has not consumed yet — setting clientAborted = true here causes wasClientAborted() to report a client fault, which may suppress an accurate circuit-breaker update blaming the provider.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/demand-driven-response-pump.ts
Line: 124-158

Comment:
**`clientAborted` conflates client cancel and server-side drain**

`startDrain` sets `clientAborted = true` unconditionally. It is called both when the client cancels and when `armPendingChunkDeadline` fires after 60s of unconsumed chunk. In the deadline case the provider delivered data that the client simply has not consumed yet — setting `clientAborted = true` here causes `wasClientAborted()` to report a client fault, which may suppress an accurate circuit-breaker update blaming the provider.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +220 to +250
TRACK_COST_ROLLING_WINDOW,
1,
key,
cost.toString(),
now.toString(),
windowMs.toString(),
requestId,
ttlSeconds.toString()
);
}

private static logCostPipelineErrors(
results: Array<[Error | null, unknown]> | null,
operation: "trackCost" | "trackUserDailyCost"
): void {
if (!results) {
logger.error("[RateLimit] Cost pipeline returned null", { operation });
return;
}

for (let commandIndex = 0; commandIndex < results.length; commandIndex += 1) {
const error = results[commandIndex]?.[0];
if (!error) continue;

logger.error("[RateLimit] Cost pipeline command failed", {
operation,
commandIndex,
error: error.message,
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Pipeline errors are logged but not propagated

logCostPipelineErrors logs individual command failures at error level but returns void. The trackCost caller discards the result, so any Redis pipeline failure causes silent cost-tracking drift — counters are neither decremented nor retried, leading to incorrect billing or rate-limit state with no observable error in the call stack.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/rate-limit/service.ts
Line: 220-250

Comment:
**Pipeline errors are logged but not propagated**

`logCostPipelineErrors` logs individual command failures at `error` level but returns `void`. The `trackCost` caller discards the result, so any Redis pipeline failure causes silent cost-tracking drift — counters are neither decremented nor retried, leading to incorrect billing or rate-limit state with no observable error in the call stack.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 321 to 390
}
}

/**
* 取消并等待 shutdown 时仍在飞的全部任务 settled。
*
* task 的 finally 可能在等待期间注册尾部任务,因此循环到 pending 集合为空;并发 shutdown
* 调用共享同一个 Promise,避免重复取消或提前返回。
*/
shutdownAll(): Promise<void> {
if (this.shutdownPromise) {
return this.shutdownPromise;
}

let resolveShutdown!: () => void;
let rejectShutdown!: (reason?: unknown) => void;
const shutdownPromise = new Promise<void>((resolve, reject) => {
resolveShutdown = resolve;
rejectShutdown = reject;
});
this.shutdownPromise = shutdownPromise;
this.lifecycleState = "draining";

// 先发布共享 Promise,再同步开始 abort;这样既保留既有同步取消语义,
// 同步 abort listener 重入时也会复用同一次 shutdown。
void (async () => {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}

while (true) {
if (this.pendingTasks.size === 0) {
this.lifecycleState = "closed";
return;
}

const activeTasks = Array.from(this.pendingTasks);
logger.info("[AsyncTaskManager] Cancelling and joining active tasks", {
count: activeTasks.length,
});

for (const taskInfo of activeTasks) {
if (!taskInfo.abortController.signal.aborted) {
taskInfo.abortController.abort();
}
}

await Promise.allSettled(activeTasks.map((taskInfo) => taskInfo.promise));

for (const taskInfo of activeTasks) {
this.cleanup(taskInfo.taskId, taskInfo);
}
}
})().then(resolveShutdown, (error) => {
this.lifecycleState = "closed";
rejectShutdown(error);
});

return shutdownPromise;
}

/**
* 获取当前活跃任务数
*/
getActiveTaskCount(): number {
return this.tasks.size;
return this.pendingTasks.size;
}

/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Post-allSettled cleanup calls in shutdownAll loop are always no-ops

After Promise.allSettled, the loop iterates pendingTasks and calls pendingTasks.delete(task) for each settled task. But tasks self-delete from pendingTasks inside their own .finally() chain before the allSettled resolves, so by the time the loop runs, pendingTasks is already empty. The explicit cleanup is dead code and may create confusion about the actual cleanup path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/async-task-manager.ts
Line: 321-390

Comment:
**Post-`allSettled` cleanup calls in `shutdownAll` loop are always no-ops**

After `Promise.allSettled`, the loop iterates `pendingTasks` and calls `pendingTasks.delete(task)` for each settled task. But tasks self-delete from `pendingTasks` inside their own `.finally()` chain before the `allSettled` resolves, so by the time the loop runs, `pendingTasks` is already empty. The explicit cleanup is dead code and may create confusion about the actual cleanup path.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread server.js
options.response.pause();
state.pressuredResponses.add(options.response);
}
if (!state.active || state.pendingBytes + bytes > MAX_PENDING_OUTBOUND_BYTES) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] safeSend() turns the 1 MiB backlog guard into a hard per-frame size limit

Why this is a problem: state.pendingBytes is 0 on the first send, so this branch rejects any single payload larger than MAX_PENDING_OUTBOUND_BYTES before ws.send() is even attempted. That means a legitimate non-stream response.completed frame bigger than 1 MiB now always closes the socket as outbound_backpressure, even when there is no queued backlog yet. I reproduced this by sending a >1 MiB JSON response through forwardToInternalHttp(): it immediately logged ws_send_failed and closed with 1011/outbound_backpressure.

Suggested fix:

const wouldOverflowQueuedBacklog =
  (state.inFlight || state.pending.length > 0) &&
  state.pendingBytes + bytes > MAX_PENDING_OUTBOUND_BYTES;

if (!state.active || wouldOverflowQueuedBacklog) {
  failOutboundSends(ws, state, {
    reason: "outbound_backpressure",
    onFailure: options.onFailure,
  });
  return false;
}

If you still want to cap single-frame payloads, that needs a separate explicit frame-size check so large terminal JSON responses do not get rejected by the backlog guard.

commitPromise = Promise.reject(error);
}

await raceWithTimeout(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] The timeout wrapper drops post-terminal-side-effects from shutdown tracking while the real side effect is still running

Why this is a problem: raceWithTimeout(...).catch(...) only waits for the 120s race, not for options.commit() itself. Once the timeout fires, this registered async task resolves and shutdownAllAsyncTasks() will stop waiting for it, but the original commitPromise keeps running in the background. These commits mutate session bindings and circuit state, so shutdown can now proceed to stopMessageRequestWriteBuffer() / closeDbPools() / closeRedis() while the side effect is still doing I/O against those resources.

Suggested fix:

const warningTimer = setTimeout(() => {
  logger.warn("[ResponseHandler] Post-terminal side effects did not complete", {
    taskId: options.taskId,
    providerId: options.providerId,
    sessionId: options.sessionId,
  });
}, STREAM_FINALIZATION_MAX_MS);
warningTimer.unref?.();

try {
  await commitPromise;
} finally {
  clearTimeout(warningTimer);
}

If these side effects really need a hard deadline, thread an AbortSignal into options.commit() and only resolve the registered task after that cancellation path has actually settled.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

This XL PR rewires several critical paths at once: WebSocket transport ownership, proxy finalization, shutdown orchestration, DB pool partitioning, and rate-limit settlement. I found two high-priority regressions in the new transport/finalization logic that should be addressed before merge.

PR Size: XL

  • Lines changed: 21441
  • Files changed: 112
  • Split suggestion: Separate the WebSocket transport / response-finalization changes (server.js, src/app/v1/_lib/proxy/*, related shutdown tests) from the persistence / infrastructure changes (src/drizzle/*, src/repository/message*, src/lib/rate-limit/*, src/lib/redis/*).
  • Split suggestion: Land the DB pool + durable writer work independently from the lease-settlement + Redis timeout work so failures can be isolated more easily.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 1 0 0
Security 0 0 0 0
Error Handling 0 1 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

  • None.

High Priority Issues (Should Fix)

  • server.js:208 — the new backlog guard now rejects any single outbound WebSocket payload over 1 MiB before the first ws.send() attempt, which breaks large non-stream response.completed frames by closing the socket as outbound_backpressure.
  • src/app/v1/_lib/proxy/response-handler.ts:216 — the timeout wrapper resolves the registered post-terminal-side-effects task after 120s even if the real side effect is still running, so shutdown can close the writer/DB/Redis while that I/O is still in flight.

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review Posted

  • Reviewed PR #1341, applied the size/XL label, and submitted the summary review plus 2 inline comments.

Key Findings

  • server.js:208 — the new outbound backpressure check now rejects any single WebSocket frame over 1 MiB before the first ws.send(), which breaks large non-stream response.completed payloads.
  • src/app/v1/_lib/proxy/response-handler.ts:216post-terminal-side-effects time out of AsyncTaskManager tracking after 120s even if the real side effect is still running, so shutdown can close DB/Redis while that work is still in flight.

If you want, I can also pull the exact posted comment text into a compact handoff note for the author.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core area:Rate Limit enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant