Skip to content

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

Merged
ding113 merged 20 commits into
devfrom
perf/full-path-benchmark-optimization
Jul 17, 2026
Merged

perf: optimize full-path concurrency and resource ownership#1341
ding113 merged 20 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 introduces a comprehensive set of concurrency, persistence, and resource-ownership improvements across the full request pipeline. The changes are split into 17 logical commits covering DB lane pooling, demand-driven response streaming, Redis batching, durable terminal persistence, and shutdown coordination.

  • DB lane isolation: Replaces a single PostgreSQL pool with bounded data/control/writer pools gated by an admission counter Proxy, including an apply trap for the tag-template-literal path, and adds statement/lock timeouts per connection.
  • Demand-driven pump: Replaces eager Response.clone()/tee() draining with a pull-based pump that tracks backpressure, bounded lookahead, drain/cancel ownership, and a markClientAborted=false fast path for the chunk-deadline timeout—fixing the previous clientAborted conflation issue.
  • Durable terminal persistence: Adds a commit-observing durable write path (enqueueDurably) with CAS fencing (status_code IS NULL), a 120 s timeout fallback, and onCommitted callbacks triggered only after the SQL batch is confirmed.
  • Shutdown coordination: shutdownAll in AsyncTaskManager now waits for all in-flight task generations to settle before pool close; stopMessageRequestWriteBuffer is a critical barrier before closeDbPools; cleanup failures propagate non-zero to the hard watchdog.

Confidence Score: 5/5

The PR is safe to merge; the correctness and resource-ownership invariants across the new durable-persistence and DB-lane paths are sound, and the changes build, type-check, lint, and pass the full test suite.

The three issues flagged in previous review rounds (tag-template-literal admission bypass, clientAborted conflation on chunk deadline, and AbortSignal unused in task factory) are all addressed. The two new observations are minor: a .then(() => false) that should also suppress the rejection case to avoid spurious error logs, and an unbounded while(true) shutdown loop that relies on the external hard watchdog. Neither affects correctness or data integrity under normal operation.

src/repository/message-write-buffer.ts (second-contender rejection path) and src/lib/async-task-manager.ts (shutdown loop iteration bound) warrant a second look.

Important Files Changed

Filename Overview
src/drizzle/admitted-client.ts New file: implements admission-controlled Proxy around the postgres.js client with apply, get(unsafe), and get(begin) traps; correctly releases the outstanding-query counter on promise settle, async-iterable exhaustion, and synchronous cancel of queued queries.
src/drizzle/db.ts Refactored to data/control/writer lane pools with budget splitting; closeDbPools is now a critical barrier; db Proxy correctly uses instance as the Reflect.get receiver (fixing a prior proxy-receiver inconsistency).
src/repository/message-write-buffer.ts Major extension: adds durable-ack path with CAS SQL fence, O(log n) EvictablePendingIndex for overflow, commit-callback tracking, and graceful stop with pending-guard; the second-contender path propagates first-ack rejection rather than returning false, which is functionally handled by the fallback but produces misleading error log entries.
src/repository/message.ts Adds updateMessageRequestDetailsDurably and updateMessageRequestDetailsIfUnfinalized; correctly routes terminal writes through the durable queue in async mode and publishes public-status rollups only after confirmed SQL commit.
src/app/v1/_lib/proxy/demand-driven-response-pump.ts New file: pull-based pump with correct state machine (client-active → draining → finalizing → closed), pending-chunk deadline with markClientAborted=false, single-reader drain loop, and settled/released reader guards.
src/app/v1/_lib/proxy/response-handler.ts Largest change: adds awaitTerminalPersistenceWithOwnership, schedulePostTerminalSideEffects, durable persistence for non-stream terminals, post-terminal side-effect ordering after committed SQL, and corrects clientAbortCompleteSuccess to not gate on streamEndedNormally.
src/lib/async-task-manager.ts Refactored to factory-based registration with a separate pendingTasks set; shutdownAll now loops until pendingTasks is empty to join all task generations; stale-cleanup only aborts, never removes from pending.
src/lib/lifecycle/shutdown.ts Steps 6-8 (async tasks → writer → DB pool) are now critical barriers that propagate errors rather than timing out silently; non-critical steps use best-effort helpers; total timeout is a warning-only timer.
src/lib/rate-limit/lease-service.ts New Lua script atomically settles all 12 lease dimensions in one round trip with idempotency marker; settlement decoding and error handling look correct.
src/lib/rate-limit/service.ts Batches Key/Provider/User cost counters into one pipeline with error logging; replaces full-window ZSET scans with cutoff+ZADD; pipeline errors are logged but not propagated (pre-existing pattern).
src/lib/langfuse/trace-proxy-request.ts Replaces raw header logging with redactLangfuseHeaders (filters x-cch-* then calls redactHeaders); sanitizes provider-chain header strings; removes the outdated 'safe to log' comment.
server.js Adds bounded 1 MiB per-client WebSocket outbound buffering with one-in-flight send, callback deadline, backpressure propagation, and turn-level resource ownership; request-body drain timeout added.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant ResponseHandler
    participant DemandPump
    participant WriterBuffer
    participant DB

    Client->>ResponseHandler: upstream stream arrives
    ResponseHandler->>DemandPump: createDemandDrivenResponsePump(source)
    loop pull-based backpressure
        Client->>DemandPump: pull()
        DemandPump->>DemandPump: ensureRead() → pendingChunk
        DemandPump-->>Client: enqueue(chunk)
    end
    Note over DemandPump: source.done=true → finishNormally()
    DemandPump-->>ResponseHandler: "completion{streamEndedNormally, clientAborted}"
    ResponseHandler->>ResponseHandler: finalizeDeferredStreaming (sync)
    ResponseHandler->>DB: updateMessageRequestDetailsDurably (durable CAS)
    DB-->>WriterBuffer: batch flush + RETURNING id
    WriterBuffer->>WriterBuffer: notifyDurableCommit → onCommitted callback
    WriterBuffer-->>ResponseHandler: ack resolved
    ResponseHandler->>ResponseHandler: schedulePostTerminalSideEffects (circuit + session)

    alt Client disconnects mid-stream
        Client->>DemandPump: "cancel(reason) → startDrain(reason, markClientAborted=true)"
        DemandPump->>DemandPump: "state=draining, drain remaining source"
        DemandPump-->>ResponseHandler: "completion{clientAborted=true}"
    end

    alt Chunk deadline fires (60 s unconsumed)
        DemandPump->>DemandPump: "startDrain(error, markClientAborted=false)"
        Note over DemandPump: clientAborted stays false → circuit-breaker not blamed on client
    end
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 Client
    participant ResponseHandler
    participant DemandPump
    participant WriterBuffer
    participant DB

    Client->>ResponseHandler: upstream stream arrives
    ResponseHandler->>DemandPump: createDemandDrivenResponsePump(source)
    loop pull-based backpressure
        Client->>DemandPump: pull()
        DemandPump->>DemandPump: ensureRead() → pendingChunk
        DemandPump-->>Client: enqueue(chunk)
    end
    Note over DemandPump: source.done=true → finishNormally()
    DemandPump-->>ResponseHandler: "completion{streamEndedNormally, clientAborted}"
    ResponseHandler->>ResponseHandler: finalizeDeferredStreaming (sync)
    ResponseHandler->>DB: updateMessageRequestDetailsDurably (durable CAS)
    DB-->>WriterBuffer: batch flush + RETURNING id
    WriterBuffer->>WriterBuffer: notifyDurableCommit → onCommitted callback
    WriterBuffer-->>ResponseHandler: ack resolved
    ResponseHandler->>ResponseHandler: schedulePostTerminalSideEffects (circuit + session)

    alt Client disconnects mid-stream
        Client->>DemandPump: "cancel(reason) → startDrain(reason, markClientAborted=true)"
        DemandPump->>DemandPump: "state=draining, drain remaining source"
        DemandPump-->>ResponseHandler: "completion{clientAborted=true}"
    end

    alt Chunk deadline fires (60 s unconsumed)
        DemandPump->>DemandPump: "startDrain(error, markClientAborted=false)"
        Note over DemandPump: clientAborted stays false → circuit-breaker not blamed on client
    end
Loading

Reviews (2): Last reviewed commit: "fix(shutdown): await scheduler quiescenc..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

本次变更重构数据库连接池、代理流式处理、终态持久化、Redis 计费结算、异步任务关停和 WebSocket 传输控制,并新增大量单元与集成测试覆盖相关竞态、背压、超时和恢复路径。

Changes

运行时与基础设施

Layer / File(s) Summary
数据库池与请求作用域
src/drizzle/*, src/app/v1*/[...route]/route.ts, tests/unit/drizzle/*, tests/integration/db-pool-*
数据库改为 data/control/writer 多池,并通过 AsyncLocalStorage 路由;新增 admission 限制、超时参数、连接池关闭状态机和路由作用域封装。
代理流与 WebSocket 传输
server.js, src/app/v1/_lib/proxy/demand-driven-response-pump.ts, src/app/v1/_lib/proxy/node-stream-to-web.ts, src/app/v1/_lib/proxy/response-handler.ts
新增需求驱动响应泵、Node/Web 流背压与取消保护、WebSocket 出站队列、终态发送确认、请求体 drain 超时和内部请求销毁。
终态写入与计费结算
src/repository/message*.ts, src/lib/rate-limit/*, src/app/v1/_lib/proxy/response-handler.ts
终态消息更新改为 durable acknowledgement 与 CAS fence;公共状态 rollup 延迟至提交后;Redis 成本写入改为 pipeline,租约预算改为幂等批量结算。
错误、绑定与诊断安全
src/app/v1/_lib/proxy/{errors,error-handler,forwarder}.ts, src/lib/session-manager.ts, src/lib/langfuse/*, src/instrumentation.ts
新增数据库 admission 错误分类与安全错误提取,定向清理 provider binding,过滤内部请求头,并对 Langfuse 与崩溃报告进行脱敏。
异步任务与关停流程
src/lib/async-task-manager.ts, src/lib/lifecycle/shutdown.ts, src/lib/provider-endpoints/*, src/lib/public-status/scheduler.ts, src/lib/ledger-backfill/service.ts
任务注册改为 factory 与 AbortSignal;shutdown 等待所有 pending generation;调度器避免重入并等待当前周期;回填流程支持取消。
配置、部署与验证
.env.example, deploy/*, tests/unit/*, tests/integration/*
新增数据库/Redis 超时配置,调整 Kubernetes 连接池预算与诊断报告启动参数,并扩展代理、持久化、Redis、数据库和关停测试。

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.45% 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
Title check ✅ Passed 标题简洁且准确概括了这次以并发优化和资源所有权为核心的改动。
Description check ✅ Passed 描述与变更内容高度一致,覆盖了主要优化目标、验证结果和未声明的范围。
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.
✨ 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 Outdated
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 Outdated
...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 Outdated
Comment thread src/app/v1/_lib/proxy/response-handler.ts Outdated
Comment thread src/app/v1/_lib/proxy/response-handler.ts
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 thread tests/unit/proxy/error-handler-client-message.test.ts
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 thread tests/unit/repository/message-public-status-rollup.test.ts
Comment thread tests/unit/repository/message-public-status-rollup.test.ts

@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 Outdated
? 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.

ding113 added 14 commits July 17, 2026 14:25
…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.
ding113 and others added 6 commits July 17, 2026 14:25
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.
Diagnostic reports now exclude the entire process environment via
--report-exclude-env in both Dockerfiles. Crash handlers wrap database
errors through findSafeDatabaseError before writing report files or
stderr fallbacks, preventing DrizzleQueryError SQL text and bound
parameters from reaching any public boundary.

The proxy error handler returns a generic 503/500 for database failures
without loading system settings or awaiting durable persistence.
Langfuse traces and session metadata strip x-cch-* reserved internal
headers. clearSessionProvider uses a Redis CAS compare so late cleanup
from a prior provider cannot delete a binding already moved to a new
one.
The first durable write claimant owns the terminal patch; later
contenders observe its acknowledgement but cannot overwrite the
committed outcome. Request duration is merged into the same CAS patch
so overflow or process exit cannot leave a permanently active record.

Post-terminal side effects (provider circuit mutation, session binding
clear, Codex cache binding) fire from an onCommitted callback after
the SQL write is acknowledged rather than speculatively before it.

server.js WebSocket turns track per-turn request, response, and settle
lifecycle: sendErrorAndClose aborts the active internal request before
queuing a fatal frame, and terminal events wait for their WS send
acknowledgement before releasing turn ownership.
Background schedulers (probe, public-status, log-cleanup) now expose
their in-flight promise so shutdown awaits true quiescence instead of
racing a fixed timeout. Bull queues are joined before DB pool and
Redis closure; failures are collected into an AggregateError so
critical cleanup still runs.

Ledger backfill registers as a cancellable AsyncTaskManager job with
abort-signal checkpoints between batches. The lease settlement Lua
script consumes the cached slice when a request exceeds the remaining
budget, preventing repeated overshoot within the refresh window.
@ding113
ding113 force-pushed the perf/full-path-benchmark-optimization branch from 8351fd9 to 048caed Compare July 17, 2026 06:26
@ding113
ding113 merged commit 3724315 into dev Jul 17, 2026
4 of 9 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Jul 17, 2026

@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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/session-manager.ts (1)

79-90: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

统一脱敏对象形式的 header snapshot。

当前只有 Headers 输入会掩码 authorization、cookie 等敏感值;对象输入和旧 JSON 记录只过滤内部头,因此可能把原始凭据写入 Redis或重新返回给调用方。两个路径都应复用 headersToSanitizedObject

建议修改
-    return record;
+    return headersToSanitizedObject(new Headers(record));
@@
-  const normalized = Object.fromEntries(
-    Object.entries(headers).filter(
-      ([key, value]) => typeof value === "string" && !isReservedInternalHeader(key)
-    )
-  );
+  const normalized = headersToSanitizedObject(new Headers(headers));

Also applies to: 130-145

🤖 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/lib/session-manager.ts` around lines 79 - 90, 统一 parseHeaderRecord
与对象输入路径的脱敏逻辑:不要直接保留对象或旧 JSON 记录中的 header 值,而应先转换为 Headers,再复用
headersToSanitizedObject,确保 authorization、cookie 等敏感字段一致被掩码,同时继续过滤内部 header,并让
Redis 写入和返回调用方的快照都使用该统一结果。
🧹 Nitpick comments (2)
src/app/v1/_lib/proxy/response-handler.ts (1)

53-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

使用 @/ 路径别名导入 demand-driven pump。

Line 53-56 新增了相对路径导入,不符合项目统一的源码导入约定。

建议修改
 import {
   createDemandDrivenResponsePump,
   type DemandDrivenResponsePump,
-} from "./demand-driven-response-pump";
+} from "`@/app/v1/_lib/proxy/demand-driven-response-pump`";

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

🤖 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 53 - 56, Update the
demand-driven response pump import in response-handler.ts to use the project’s
`@/` alias mapped to ./src/ instead of the relative
"./demand-driven-response-pump" path, preserving the imported symbols
createDemandDrivenResponsePump and DemandDrivenResponsePump.

Source: Coding guidelines

src/repository/message.ts (1)

492-496: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

移除 TypeScript 注释中的 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 492 - 496, Remove the emoji
decoration characters from the comments on the model and providerId fields in
the relevant message type, while preserving the comments’ descriptive text and
field behavior.

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 `@src/app/v1/_lib/proxy/error-handler.ts`:
- Around line 298-307: Update the terminal persistence flow in ProxyErrorHandler
so both database-error and normal branches attempt the durable write via
logErrorToDatabase, while treating any persistence failure as best-effort and
preserving the original error response. Wrap the write attempt in failure
handling and always call endRequestTracking(session) from a finally block,
covering the related flow around the alternate lines as well.
- Around line 253-264: Update the databaseError fallback in the error-handler
flow to use a five-language static message map keyed by the request locale
instead of hardcoded English. Preserve the existing
getLocale/getErrorMessageServer path, and when locale resolution or translation
fails, select the matching supported-language message with a safe default
locale.

In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 328-341: 更新 runPostTerminalSideEffects,保留 Promise.allSettled
以等待所有后置副作用完成,但不要丢弃其结果;检查 rejected 结果并将失败向外传播,使 session 清理或 circuit
更新失败能够触发外层错误日志,同时维持 signal.aborted 时直接返回的行为。

In `@src/instrumentation.ts`:
- Around line 452-468: Move the __CCH_STOP_BACKGROUND_QUEUES__ hook assignment
before either background queue is initialized, then initialize the cleanup and
notification queues sequentially. Ensure a failure during notification queue
startup still leaves the previously installed hook available to stop the cleanup
queue.
- Around line 362-372: 调整 src/instrumentation.ts 的 362-372 和 533-543
行:在启动生命周期中等待对应模块导入完成后,立即同步调用 AsyncTaskManager.register 注册回填任务,不要仅创建未被等待的 .then()
链。生产环境和开发环境均应保持相同的注册时序,并保留现有 startup-ledger-backfill 配置及 backfillUsageLedger 调用。

In `@src/lib/lifecycle/shutdown.ts`:
- Around line 142-163: Make queue shutdown a resource-ownership barrier: in the
background-queue stop flow around awaitWithWarning, propagate any stop failure
immediately and prevent writer, database, and Redis cleanup from continuing
while queues may remain active. Update the shutdown error propagation at
src/lib/lifecycle/shutdown.ts lines 266-268 accordingly, and adjust
tests/unit/lib/shutdown.test.ts lines 265-309 to assert dependent resources are
not closed after an unproven queue shutdown.

In `@src/repository/message.ts`:
- Around line 586-593: Update the terminal compare-and-set condition in the
onlyIfUnfinalized path of the message update flow to also require
messageRequest.deletedAt to be null, matching the batch durable path. Preserve
the existing id and statusCode predicates so soft-deleted records are excluded
from updates and subsequent publication.
- Around line 510-516: 在终态分支中更新 message request details 时,返回
updateMessageRequestDetailsDurably(id, details) 的实际布尔结果,而不是无条件返回 true;保留 await
以确保调用完成,并维持非终态 metadata 的现有处理路径。

In `@tests/unit/proxy/response-handler-gemini-terminal.test.ts`:
- Around line 10-16: Update the mocks in the hoisted `mocks` object so `details`
and `conditional` both preserve the `Promise<boolean>` return contract of
`updateMessageRequestDetails` and `updateMessageRequestDetailsIfUnfinalized`;
adjust the conditional mock’s pending implementation to resolve `true` rather
than `void`.

---

Outside diff comments:
In `@src/lib/session-manager.ts`:
- Around line 79-90: 统一 parseHeaderRecord 与对象输入路径的脱敏逻辑:不要直接保留对象或旧 JSON 记录中的
header 值,而应先转换为 Headers,再复用 headersToSanitizedObject,确保 authorization、cookie
等敏感字段一致被掩码,同时继续过滤内部 header,并让 Redis 写入和返回调用方的快照都使用该统一结果。

---

Nitpick comments:
In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 53-56: Update the demand-driven response pump import in
response-handler.ts to use the project’s `@/` alias mapped to ./src/ instead of
the relative "./demand-driven-response-pump" path, preserving the imported
symbols createDemandDrivenResponsePump and DemandDrivenResponsePump.

In `@src/repository/message.ts`:
- Around line 492-496: Remove the emoji decoration characters from the comments
on the model and providerId fields in the relevant message type, while
preserving the comments’ descriptive text and field behavior.
🪄 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: 04569eef-cd66-4818-98f0-a60a8910a25a

📥 Commits

Reviewing files that changed from the base of the PR and between 2e2b059 and 048caed.

📒 Files selected for processing (110)
  • .env.example
  • Dockerfile
  • deploy/Dockerfile
  • 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/provider-selector.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/instrumentation.ts
  • src/lib/async-task-manager.ts
  • src/lib/config/env.schema.ts
  • src/lib/langfuse/emit-proxy-trace.ts
  • src/lib/langfuse/trace-proxy-request.ts
  • src/lib/ledger-backfill/service.ts
  • src/lib/lifecycle/shutdown.ts
  • src/lib/price-sync/cloud-price-updater.ts
  • src/lib/provider-endpoints/probe-log-cleanup.ts
  • src/lib/provider-endpoints/probe-scheduler.ts
  • src/lib/public-status/scheduler.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/session-manager.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/dashboard/user-insights-page.test.tsx
  • tests/unit/deploy-dockerfile-contract.test.ts
  • 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/instrumentation-crash-handler.test.ts
  • tests/unit/langfuse/emit-proxy-trace.test.ts
  • tests/unit/langfuse/langfuse-trace.test.ts
  • tests/unit/lib/async-task-manager-edge-runtime.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/session-manager-helpers.test.ts
  • tests/unit/lib/session-manager-terminate-session.test.ts
  • tests/unit/lib/shutdown.test.ts
  • tests/unit/price-sync/cloud-price-updater.test.ts
  • tests/unit/proxy/build-request-details-redaction.test.ts
  • tests/unit/proxy/client-abort-vs-upstream-499.test.ts
  • tests/unit/proxy/connected-non-reader-lifetime.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/pricing-no-price.test.ts
  • tests/unit/proxy/provider-selector-cross-type-model.test.ts
  • tests/unit/proxy/provider-selector-model-mismatch-binding.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/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
  • tests/unit/usage-ledger/backfill.test.ts
🚧 Files skipped from review as they are similar to previous changes (66)
  • deploy/k8s/app/deployment.yaml
  • tests/unit/proxy/client-abort-vs-upstream-499.test.ts
  • src/app/v1beta/[...route]/route.ts
  • tests/unit/lib/rate-limit/rolling-window-cache-warm.test.ts
  • tests/unit/dashboard/user-insights-page.test.tsx
  • tests/configs/integration.config.ts
  • tests/unit/proxy/response-handler-bill-non-success.test.ts
  • src/app/v1/[...route]/route.ts
  • src/app/v1/_lib/proxy-handler.ts
  • tests/unit/proxy/response-handler-non200.test.ts
  • src/lib/redis/client.ts
  • tests/unit/repository/message-public-readback.test.ts
  • src/lib/price-sync/cloud-price-updater.ts
  • tests/unit/proxy/error-handler-terminal-status.test.ts
  • tests/unit/drizzle/db-scope.test.ts
  • tests/unit/repository/message-terminal-cost-accounting.test.ts
  • tests/unit/repository/message-query-test-support.ts
  • tests/unit/proxy/pricing-no-price.test.ts
  • tests/unit/proxy/proxy-handler-concurrency-ownership.test.ts
  • tests/unit/proxy/response-handler-abort-listener-cleanup.test.ts
  • tests/unit/proxy/response-handler-exported-finalizers.test.ts
  • tests/unit/repository/message-terminal-public-status-seam.test.ts
  • tests/integration/proxy-hedge-lifecycle.test.ts
  • tests/unit/repository/message-aggregate-multiple-session-stats.test.ts
  • tests/unit/langfuse/langfuse-trace.test.ts
  • src/app/v1/_lib/proxy/node-stream-to-web.test.ts
  • tests/unit/proxy/error-handler-overrides.test.ts
  • tests/integration/rolling-cost-redis.test.ts
  • src/app/v1/_lib/proxy/node-stream-to-web.ts
  • tests/unit/proxy/proxy-handler-public-success.test.ts
  • tests/unit/proxy/response-handler-public-dispatch.test.ts
  • tests/unit/lib/rate-limit/lease-service.test.ts
  • src/lib/config/env.schema.ts
  • tests/unit/repository/message-aggregate-session-stats.test.ts
  • tests/unit/repository/message-terminal-write-apis.test.ts
  • tests/unit/proxy/proxy-handler-public-errors.test.ts
  • tests/integration/db-pool-slow-close-postgres.test.ts
  • tests/integration/db-pool-isolation-postgres.test.ts
  • tests/unit/repository/message-usage-logs-query.test.ts
  • tests/unit/proxy/connected-non-reader-lifetime.test.ts
  • src/lib/redis/lua-scripts.ts
  • tests/unit/proxy/response-handler-hedge-loser-priority.test.ts
  • tests/unit/repository/message-public-status-rollup.test.ts
  • tests/unit/lib/redis/client.test.ts
  • tests/unit/proxy/error-handler-langfuse-trace.test.ts
  • tests/unit/drizzle/db-shutdown.test.ts
  • tests/unit/lib/rate-limit/service-extra.test.ts
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
  • src/lib/rate-limit/service.ts
  • src/app/v1/_lib/proxy/demand-driven-response-pump.ts
  • tests/unit/repository/message-write-buffer.test.ts
  • tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts
  • src/lib/async-task-manager.ts
  • tests/integration/lease-settlement-redis.test.ts
  • tests/unit/lib/async-task-manager-edge-runtime.test.ts
  • tests/unit/lib/rate-limit/rolling-window-5h.test.ts
  • tests/integration/billing-model-source.test.ts
  • tests/unit/server-shutdown.test.ts
  • tests/unit/proxy/response-handler-lease-decrement.test.ts
  • tests/unit/drizzle/db-pool-config.test.ts
  • src/drizzle/db.ts
  • tests/unit/proxy/response-handler-gemini-stream-passthrough-timeouts.test.ts
  • src/app/v1/_lib/proxy/demand-driven-response-pump.test.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • tests/unit/proxy/response-handler-client-abort-drain.test.ts
  • tests/unit/proxy/error-handler-durable-persistence.test.ts

Comment on lines +253 to +264
if (databaseError) {
// Drizzle wraps the admission cause with SQL text and bound parameters.
// Never let that wrapper cross the public, log, or observability boundary.
try {
const { getLocale } = await import("next-intl/server");
clientErrorMessage = await getErrorMessageServer(
await getLocale(),
ERROR_CODES.DATABASE_ERROR
);
} catch {
clientErrorMessage = "An error occurred";
}

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 | 🟡 Minor | ⚡ Quick win

数据库错误兜底仍然绕过 i18n。

Line 263 的硬编码英文会让非英语用户在 locale 解析失败时收到错误语言。请提供基于请求 locale 的五语言静态兜底,而不是直接返回英文。

依据编码规范:“All user-facing strings must use i18n ... Never hardcode display text”。

🤖 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/error-handler.ts` around lines 253 - 264, Update the
databaseError fallback in the error-handler flow to use a five-language static
message map keyed by the request locale instead of hardcoded English. Preserve
the existing getLocale/getErrorMessageServer path, and when locale resolution or
translation fails, select the matching supported-language message with a safe
default locale.

Source: Coding guidelines

Comment on lines +298 to +307
if (databaseError) {
ProxyErrorHandler.endRequestTracking(session);
} else {
await ProxyErrorHandler.logErrorToDatabase(
session,
logErrorMessage,
finalResponse.status,
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

让终态持久化成为 best-effort,并始终结束请求追踪。

数据库错误分支完全跳过 durable patch;普通分支若写入失败,则会在 Line 675 前抛出。两种情况都可能留下永久 active 记录,后者还会用二次持久化异常替换原错误响应。应统一尝试 durable 写入、捕获失败,并在 finally 中结束 tracking。

建议修改
-      if (databaseError) {
-        ProxyErrorHandler.endRequestTracking(session);
-      } else {
+      try {
         await ProxyErrorHandler.logErrorToDatabase(
           session,
           logErrorMessage,
           finalResponse.status,
-          null
+          null,
         );
+      } catch (persistenceError) {
+        const safeError = findSafeDatabaseError(persistenceError);
+        logger.warn("ProxyErrorHandler: failed to persist terminal error", {
+          error: safeError?.message ?? "Terminal error persistence failed",
+          databaseCode: safeError?.code,
+        });
+      } finally {
+        ProxyErrorHandler.endRequestTracking(session);
       }
@@
-    ProxyErrorHandler.endRequestTracking(session);

Also applies to: 663-675

🤖 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/error-handler.ts` around lines 298 - 307, Update the
terminal persistence flow in ProxyErrorHandler so both database-error and normal
branches attempt the durable write via logErrorToDatabase, while treating any
persistence failure as best-effort and preserving the original error response.
Wrap the write attempt in failure handling and always call
endRequestTracking(session) from a finally block, covering the related flow
around the alternate lines as well.

Comment on lines +328 to +341
async function runPostTerminalSideEffects(
effects: ReadonlyArray<() => Promise<void>>,
signal: AbortSignal
): Promise<void> {
if (signal.aborted) return;
await Promise.allSettled(
effects.map((effect) => {
try {
return effect();
} catch (error) {
return Promise.reject(error);
}
})
);

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

不要静默吞掉后置副作用失败。

Line 333 的 Promise.allSettled() 结果被直接丢弃,因此 session 清理或 circuit 更新失败时,外层错误日志也不会触发,状态可能长期不一致。

建议修改
-  await Promise.allSettled(
+  const results = await Promise.allSettled(
     effects.map((effect) => {
       try {
         return effect();
       } catch (error) {
         return Promise.reject(error);
       }
     })
   );
+  const failures = results
+    .filter((result): result is PromiseRejectedResult => result.status === "rejected")
+    .map((result) => result.reason);
+  if (failures.length > 0) {
+    throw new AggregateError(failures, "Post-terminal side effects failed");
+  }
📝 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
async function runPostTerminalSideEffects(
effects: ReadonlyArray<() => Promise<void>>,
signal: AbortSignal
): Promise<void> {
if (signal.aborted) return;
await Promise.allSettled(
effects.map((effect) => {
try {
return effect();
} catch (error) {
return Promise.reject(error);
}
})
);
async function runPostTerminalSideEffects(
effects: ReadonlyArray<() => Promise<void>>,
signal: AbortSignal
): Promise<void> {
if (signal.aborted) return;
const results = await Promise.allSettled(
effects.map((effect) => {
try {
return effect();
} catch (error) {
return Promise.reject(error);
}
})
);
const failures = results
.filter((result): result is PromiseRejectedResult => result.status === "rejected")
.map((result) => result.reason);
if (failures.length > 0) {
throw new AggregateError(failures, "Post-terminal side effects failed");
}
🤖 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 328 - 341, 更新
runPostTerminalSideEffects,保留 Promise.allSettled 以等待所有后置副作用完成,但不要丢弃其结果;检查
rejected 结果并将失败向外传播,使 session 清理或 circuit 更新失败能够触发外层错误日志,同时维持 signal.aborted
时直接返回的行为。

Comment thread src/instrumentation.ts
Comment on lines +362 to +372
Promise.all([import("@/lib/async-task-manager"), import("@/lib/ledger-backfill")])
.then(([{ AsyncTaskManager }, { backfillUsageLedger }]) => {
AsyncTaskManager.register(
"startup-ledger-backfill",
async (signal) => {
const result = await backfillUsageLedger(signal);
logger.info("[Instrumentation] Ledger backfill complete", result);
},
{ taskType: "startup-ledger-backfill", staleTimeoutMs: Number.POSITIVE_INFINITY }
);
})

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

请在启动生命周期内完成回填任务注册。 两处 .then() 都留下了关停先于注册完成的窗口,可能导致数据库池关闭后才启动回填。

  • src/instrumentation.ts#L362-L372:先等待模块导入,再同步注册生产环境回填任务。
  • src/instrumentation.ts#L533-L543:对开发环境采用相同的注册时序。
📍 Affects 1 file
  • src/instrumentation.ts#L362-L372 (this comment)
  • src/instrumentation.ts#L533-L543
🤖 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/instrumentation.ts` around lines 362 - 372, 调整 src/instrumentation.ts 的
362-372 和 533-543 行:在启动生命周期中等待对应模块导入完成后,立即同步调用 AsyncTaskManager.register
注册回填任务,不要仅创建未被等待的 .then() 链。生产环境和开发环境均应保持相同的注册时序,并保留现有 startup-ledger-backfill
配置及 backfillUsageLedger 调用。

Comment thread src/instrumentation.ts
Comment on lines +452 to +468
(
globalThis as typeof globalThis & {
__CCH_STOP_BACKGROUND_QUEUES__?: () => Promise<void>;
}
).__CCH_STOP_BACKGROUND_QUEUES__ = async () => {
const [{ stopCleanupQueue }, { stopNotificationQueue }] = await Promise.all([
import("@/lib/log-cleanup/cleanup-queue"),
import("@/lib/notification/notification-queue"),
]);
const results = await Promise.allSettled([stopCleanupQueue(), stopNotificationQueue()]);
const failures = results.flatMap((result) =>
result.status === "rejected" ? [result.reason] : []
);
if (failures.length > 0) {
throw new AggregateError(failures, "Failed to stop background queues");
}
};

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

请在启动队列前安装停止钩子。

若清理队列启动成功但通知队列启动失败,此赋值不会执行,已创建的清理队列便无法被关停流程关闭。应先安装钩子,再依次初始化队列。

🤖 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/instrumentation.ts` around lines 452 - 468, Move the
__CCH_STOP_BACKGROUND_QUEUES__ hook assignment before either background queue is
initialized, then initialize the cleanup and notification queues sequentially.
Ensure a failure during notification queue startup still leaves the previously
installed hook available to stop the cleanup queue.

Comment on lines +142 to +163
// 5. Bull queues own Redis connections and may still ACK jobs or emit DB work.
// Join them before closing either backing resource.
try {
await awaitWithWarning(
(async () => {
const stopQueues = (
globalThis as typeof globalThis & {
__CCH_STOP_BACKGROUND_QUEUES__?: () => Promise<void>;
}
).__CCH_STOP_BACKGROUND_QUEUES__;
if (stopQueues) await stopQueues();
})(),
stepMs,
"stopBackgroundQueues"
);
} catch (error) {
const queueError = error instanceof Error ? error : new Error(String(error));
deferredErrors.push(queueError);
logger.error("[Shutdown] background queues failed to stop; continuing critical cleanup", {
error: queueError.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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

队列停止失败后仍关闭其依赖,破坏了资源所有权屏障。

  • src/lib/lifecycle/shutdown.ts#L142-L163:只有在所有队列 worker 已完成 join 后,才能继续 writer、数据库及 Redis 回收。
  • src/lib/lifecycle/shutdown.ts#L266-L268:不要等依赖关闭后才传播无法证明静默的队列错误。
  • tests/unit/lib/shutdown.test.ts#L265-L309:调整测试,禁止在队列仍可能活跃时关闭其依赖资源。
📍 Affects 2 files
  • src/lib/lifecycle/shutdown.ts#L142-L163 (this comment)
  • src/lib/lifecycle/shutdown.ts#L266-L268
  • tests/unit/lib/shutdown.test.ts#L265-L309
🤖 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/lib/lifecycle/shutdown.ts` around lines 142 - 163, Make queue shutdown a
resource-ownership barrier: in the background-queue stop flow around
awaitWithWarning, propagate any stop failure immediately and prevent writer,
database, and Redis cleanup from continuing while queues may remain active.
Update the shutdown error propagation at src/lib/lifecycle/shutdown.ts lines
266-268 accordingly, and adjust tests/unit/lib/shutdown.test.ts lines 265-309 to
assert dependent resources are not closed after an unproven queue shutdown.

Comment thread src/repository/message.ts
Comment on lines +510 to 516
if (getEnvConfig().MESSAGE_REQUEST_WRITE_MODE === "async" && !options.onlyIfUnfinalized) {
// 终态 patch 必须观察 SQL commit 后再发布 public-status rollup。
// 非终态 metadata 仍保持轻量 enqueue,但不能伪称已提交。
if (details.statusCode !== undefined) {
await updateMessageRequestDetailsDurably(id, details);
return true;
}

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 | 🟡 Minor | ⚡ Quick win

返回耐久写入的实际提交结果。

另一终态写入已取得所有权时,updateMessageRequestDetailsDurably() 会返回 false;当前代码却无条件返回 true,破坏了新布尔返回值的 CAS 语义。

建议修改
     if (details.statusCode !== undefined) {
-      await updateMessageRequestDetailsDurably(id, details);
-      return true;
+      return updateMessageRequestDetailsDurably(id, details);
     }
📝 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
if (getEnvConfig().MESSAGE_REQUEST_WRITE_MODE === "async" && !options.onlyIfUnfinalized) {
// 终态 patch 必须观察 SQL commit 后再发布 public-status rollup。
// 非终态 metadata 仍保持轻量 enqueue,但不能伪称已提交。
if (details.statusCode !== undefined) {
await updateMessageRequestDetailsDurably(id, details);
return true;
}
if (getEnvConfig().MESSAGE_REQUEST_WRITE_MODE === "async" && !options.onlyIfUnfinalized) {
// 终态 patch 必须观察 SQL commit 后再发布 public-status rollup。
// 非终态 metadata 仍保持轻量 enqueue,但不能伪称已提交。
if (details.statusCode !== undefined) {
return updateMessageRequestDetailsDurably(id, details);
}
🤖 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 510 - 516, 在终态分支中更新 message request
details 时,返回 updateMessageRequestDetailsDurably(id, details) 的实际布尔结果,而不是无条件返回
true;保留 await 以确保调用完成,并维持非终态 metadata 的现有处理路径。

Comment thread src/repository/message.ts
Comment on lines +586 to +593
if (options.onlyIfUnfinalized) {
const terminalDb =
getEnvConfig().MESSAGE_REQUEST_WRITE_MODE === "async" ? getMessageWriterDb() : db;
const updated = await terminalDb
.update(messageRequest)
.set(updateData)
.where(and(eq(messageRequest.id, id), isNull(messageRequest.statusCode)))
.returning({ id: messageRequest.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

终态 CAS 必须排除已软删除记录。

批量耐久路径包含 deleted_at IS NULL,但此回退路径仅检查 status_code。因此软删除且尚未终态化的记录仍可被更新,并触发提交观察者和公共状态发布。

建议修改
       .update(messageRequest)
       .set(updateData)
-      .where(and(eq(messageRequest.id, id), isNull(messageRequest.statusCode)))
+      .where(
+        and(
+          eq(messageRequest.id, id),
+          isNull(messageRequest.statusCode),
+          isNull(messageRequest.deletedAt)
+        )
+      )
       .returning({ id: messageRequest.id });
📝 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
if (options.onlyIfUnfinalized) {
const terminalDb =
getEnvConfig().MESSAGE_REQUEST_WRITE_MODE === "async" ? getMessageWriterDb() : db;
const updated = await terminalDb
.update(messageRequest)
.set(updateData)
.where(and(eq(messageRequest.id, id), isNull(messageRequest.statusCode)))
.returning({ id: messageRequest.id });
if (options.onlyIfUnfinalized) {
const terminalDb =
getEnvConfig().MESSAGE_REQUEST_WRITE_MODE === "async" ? getMessageWriterDb() : db;
const updated = await terminalDb
.update(messageRequest)
.set(updateData)
.where(
and(
eq(messageRequest.id, id),
isNull(messageRequest.statusCode),
isNull(messageRequest.deletedAt)
)
)
.returning({ id: messageRequest.id });
🤖 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 586 - 593, Update the terminal
compare-and-set condition in the onlyIfUnfinalized path of the message update
flow to also require messageRequest.deletedAt to be null, matching the batch
durable path. Preserve the existing id and statusCode predicates so soft-deleted
records are excluded from updates and subsequent publication.

Comment on lines +10 to +16
const mocks = vi.hoisted(() => ({
conditional:
vi.fn<(id: number, details: object, options?: TerminalWriterOptions) => Promise<boolean>>(),
details: vi.fn<(id: number, details: object) => Promise<void>>(),
durable:
vi.fn<(id: number, details: object, options?: TerminalWriterOptions) => Promise<boolean>>(),
tasks: Array.from<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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

ast-grep outline tests/unit/proxy/response-handler-gemini-terminal.test.ts \
  --items all --match 'mocks|beforeEach'

rg -n -C2 \
  'Promise<void>|mockResolvedValue\(undefined\)' \
  tests/unit/proxy/response-handler-gemini-terminal.test.ts

fd -a -t f '^tsconfig.*\.json$' . \
  --exec sh -c 'echo "--- $1"; cat "$1"' _ {}

Repository: ding113/claude-code-hub

Length of output: 2728


🏁 Script executed:

#!/bin/bash
set -eu

echo "== test file context =="
sed -n '1,280p' tests/unit/proxy/response-handler-gemini-terminal.test.ts | cat -n

echo
echo "== production symbols =="
rg -n -C3 'conditional|details|durable|TerminalWriterOptions|ProxyResponseHandler' src tests -g '!**/dist/**' -g '!**/.next/**'

Repository: ding113/claude-code-hub

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -eu

echo "== find relevant interfaces/usages =="
rg -n -C4 'details\s*:\s*vi\.fn|conditional\s*:\s*vi\.fn|durable\s*:\s*vi\.fn|Promise<boolean>|Promise<void>' \
  tests src -g '!**/dist/**' -g '!**/.next/**'

echo
echo "== locate terminal writer / proxy handler definitions =="
fd -a -t f 'response-handler|terminal' src tests

Repository: ding113/claude-code-hub

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -eu

echo "== search for the likely methods =="
rg -n -C3 '\bconditional\b|\bdetails\b|\bdurable\b' src tests | sed -n '1,220p'

echo
echo "== search for writer interfaces =="
rg -n -C4 'interface .*Writer|type .*Writer|TerminalWriterOptions|onCommitted' src tests

Repository: ding113/claude-code-hub

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -eu

sed -n '1,260p' tests/unit/proxy/response-handler-gemini-terminal.test.ts | cat -n

Repository: ding113/claude-code-hub

Length of output: 10470


🏁 Script executed:

#!/bin/bash
set -eu

sed -n '560,710p' src/repository/message.ts | cat -n

Repository: ding113/claude-code-hub

Length of output: 6423


让这两个 mock 保持 Promise<boolean> 契约。 updateMessageRequestDetails / updateMessageRequestDetailsIfUnfinalized 都返回 Promise<boolean>,这里的 details 仍是 Promise<void>conditional 的挂起实现也应返回 Promise<boolean> 并 resolve true,否则测试会偏离当前持久化路径的返回值。

🤖 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 10 -
16, Update the mocks in the hoisted `mocks` object so `details` and
`conditional` both preserve the `Promise<boolean>` return contract of
`updateMessageRequestDetails` and `updateMessageRequestDetailsIfUnfinalized`;
adjust the conditional mock’s pending implementation to resolve `true` rather
than `void`.

@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: 048caedb62

ℹ️ 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".

Comment thread src/instrumentation.ts
Comment on lines +364 to +367
AsyncTaskManager.register(
"startup-ledger-backfill",
async (signal) => {
const result = await backfillUsageLedger(signal);

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 Don't join the startup ledger backfill without canceling SQL

In production startup, a pod that receives SIGTERM while this registered backfill is inside its 10k-row tx.execute batch will have shutdownAllAsyncTasks() abort and then join the task, but backfillUsageLedger only observes the signal between batches and cannot cancel the running SQL. The DB statement timeout defaults to 90s while the server hard-exit watchdog defaults to 28s, so large backfills can turn normal rolling shutdowns into process.exit(1) before cleanup finishes; either keep this backfill detached or make the batch query actually abortable before joining it.

Useful? React with 👍 / 👎.

let errorCategory = await categorizeErrorAsync(error);
lastErrorCategory = errorCategory;
const statusCode = error instanceof ProxyError ? error.statusCode : undefined;
const databaseError = findSafeDatabaseError(error);

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 Treat hedge database errors as local overload

For hedged streaming attempts, a non-admission Drizzle query failure is recognized here as a safe database error, but errorCategory is not promoted the way the non-hedged path does. When the classifier leaves that local DB failure as SYSTEM_ERROR, the next block records endpoint failure and the attempt proceeds through provider failover, so a local database outage can penalize healthy provider endpoints and launch unnecessary alternatives; handle any databaseError as local overload before endpoint accounting.

Useful? React with 👍 / 👎.

Comment on lines +298 to +299
if (databaseError) {
ProxyErrorHandler.endRequestTracking(session);

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 Persist terminal rows for local DB overloads

When a request has already created a message_request and a local DB error comes from the data/control pool, this branch only ends in-memory tracking and skips the durable terminal write entirely. Since the writer pool is isolated for terminal updates, those 503/500 responses can leave status_code and duration_ms NULL permanently, making active/history views and rollups wrong; try the writer-backed terminal persistence and swallow only if that also fails.

Useful? React with 👍 / 👎.

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: Done

Development

Successfully merging this pull request may close these issues.

1 participant