feat(discovery): merge bounded Discovery stack#1359
Conversation
feat(proxy): add bounded streaming discovery
…0723' into agent/discovery-1349-on-integration # Conflicts: # drizzle/meta/_journal.json # src/app/v1/_lib/proxy/forwarder.ts # src/app/v1/_lib/proxy/response-handler.ts # tests/unit/proxy/discovery-validity.test.ts # tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts
The reactive rectifier call in the hedge wave failure handler omitted the required `error` argument, causing a TS2741 type error that broke CI. Supply lastError, matching the two sibling call sites. CI Run: https://github.com/ding113/claude-code-hub/actions/runs/29968232269 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat(settings): configure bounded streaming discovery
…0723' into agent/discovery-1351-on-integration # Conflicts: # messages/en/dashboard.json # messages/ja/dashboard.json # messages/ru/dashboard.json # messages/zh-CN/dashboard.json # messages/zh-TW/dashboard.json # src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx # src/app/v1/_lib/proxy/forwarder.ts # tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts
…0723' into agent/discovery-1353-on-integration # Conflicts: # messages/en/dashboard.json # messages/ja/dashboard.json # messages/ru/dashboard.json # messages/zh-CN/dashboard.json # messages/zh-TW/dashboard.json # src/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsx # src/app/v1/_lib/proxy/forwarder.ts # tests/unit/proxy/response-handler-endpoint-circuit-isolation.test.ts
…race feat(observability): add request-level Discovery routing trace
…0723' into agent/discovery-1353-on-integration
…cue-billing fix(discovery): preserve final rescue and bill ready losers
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
📝 WalkthroughWalkthrough新增有界流式 Discovery 路由、版本化会话绑定与 Redis 租约机制,并将 routing trace 贯通请求处理、持久化、outbox 回放及 Dashboard 日志展示。同时加入配置校验、数据库迁移、错误码、本地化文案和测试覆盖。 ChangesBounded Discovery and routing trace
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
🧪 测试结果
总体结果: ✅ 所有测试通过 |
| push(chunk: Uint8Array | string): DiscoveryValidity { | ||
| if (this._error) return this.result; | ||
| this.bytesSeen += | ||
| typeof chunk === "string" | ||
| ? DISCOVERY_TEXT_ENCODER.encode(chunk).byteLength | ||
| : chunk.byteLength; |
There was a problem hiding this comment.
The byte counter for string chunks calls
DISCOVERY_TEXT_ENCODER.encode(chunk).byteLength on every push, which allocates and immediately discards a Uint8Array. This allocation happens even after _ready becomes true and the limit check no longer fires. Buffer.byteLength resolves the UTF-8 size in native code without an allocation.
| push(chunk: Uint8Array | string): DiscoveryValidity { | |
| if (this._error) return this.result; | |
| this.bytesSeen += | |
| typeof chunk === "string" | |
| ? DISCOVERY_TEXT_ENCODER.encode(chunk).byteLength | |
| : chunk.byteLength; | |
| push(chunk: Uint8Array | string): DiscoveryValidity { | |
| if (this._error) return this.result; | |
| this.bytesSeen += | |
| typeof chunk === "string" | |
| ? Buffer.byteLength(chunk, "utf8") | |
| : chunk.byteLength; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/discovery-validity.ts
Line: 202-207
Comment:
The byte counter for string chunks calls `DISCOVERY_TEXT_ENCODER.encode(chunk).byteLength` on every push, which allocates and immediately discards a `Uint8Array`. This allocation happens even after `_ready` becomes `true` and the limit check no longer fires. `Buffer.byteLength` resolves the UTF-8 size in native code without an allocation.
```suggestion
push(chunk: Uint8Array | string): DiscoveryValidity {
if (this._error) return this.result;
this.bytesSeen +=
typeof chunk === "string"
? Buffer.byteLength(chunk, "utf8")
: chunk.byteLength;
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/app/v1/_lib/proxy/response-handler.ts (1)
1811-1813: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win复用已解析的
completionInspection,避免对同一份allContent二次解析。第 1742 行已计算
completionInspection = inspectStreamCompletion(allContent, session.originalFormat),此处hasStreamCompletionMarker(allContent, session.originalFormat)会再次完整解析同一份内容(parseSSEData及 Gemini 的extractJsonChunks)。两者入参完全一致且语义等价(协议错误分支同样返回hasMarker=false),在客户端中断的成功判定路径上对大流式响应重复解析属于不必要开销。♻️ 复用已有解析结果
- if (!hasStreamCompletionMarker(allContent, session.originalFormat)) { + if (!completionInspection.hasMarker) { return false; }🤖 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 1811 - 1813, 在客户端中断的成功判定流程中,复用前面已计算的 completionInspection 结果替代 hasStreamCompletionMarker(allContent, session.originalFormat) 的重复解析;根据其 hasMarker 字段保持现有无完成标记时返回 false 的行为不变。src/lib/ledger-backfill/trigger.sql (1)
260-290: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value将
routing_trace补入UPDATE OF列集合。
fn_upsert_usage_ledger()不会根据routing_trace重算 ledger,但当前触发语句的列集合缺失routing_trace,若仅更新该字段会再次触发并增加不必要的冲突路径;将其显式纳入列集合后,该列更新不再触发重算。🤖 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/ledger-backfill/trigger.sql` around lines 260 - 290, Update the INSERT/UPDATE trigger definition for message_request by adding routing_trace to the UPDATE OF column list, so routing_trace-only updates do not invoke fn_upsert_usage_ledger().
🤖 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.
Nitpick comments:
In `@src/app/v1/_lib/proxy/response-handler.ts`:
- Around line 1811-1813: 在客户端中断的成功判定流程中,复用前面已计算的 completionInspection 结果替代
hasStreamCompletionMarker(allContent, session.originalFormat) 的重复解析;根据其
hasMarker 字段保持现有无完成标记时返回 false 的行为不变。
In `@src/lib/ledger-backfill/trigger.sql`:
- Around line 260-290: Update the INSERT/UPDATE trigger definition for
message_request by adding routing_trace to the UPDATE OF column list, so
routing_trace-only updates do not invoke fn_upsert_usage_ledger().
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3a772b4a-a134-4270-9f0d-ab5926c4dbf5
📒 Files selected for processing (120)
.env.exampledocs/streaming-discovery.mddrizzle/0110_daffy_rawhide_kid.sqldrizzle/0111_happy_mauler.sqldrizzle/meta/0110_snapshot.jsondrizzle/meta/0111_snapshot.jsondrizzle/meta/_journal.jsonmessages/en/dashboard.jsonmessages/en/settings/config.jsonmessages/ja/dashboard.jsonmessages/ja/settings/config.jsonmessages/ru/dashboard.jsonmessages/ru/settings/config.jsonmessages/zh-CN/dashboard.jsonmessages/zh-CN/settings/config.jsonmessages/zh-TW/dashboard.jsonmessages/zh-TW/settings/config.jsonpackage.jsonsrc/actions/system-config.tssrc/actions/usage-logs.tssrc/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsxsrc/app/[locale]/dashboard/logs/_components/error-details-dialog/components/DiscoveryTraceView.tsxsrc/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsxsrc/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsxsrc/app/[locale]/dashboard/logs/_components/error-details-dialog/components/index.tssrc/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsxsrc/app/[locale]/dashboard/logs/_components/error-details-dialog/types.tssrc/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsxsrc/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-table.test.tsxsrc/app/[locale]/dashboard/logs/_components/usage-logs-table.tsxsrc/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsxsrc/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsxsrc/app/[locale]/settings/config/_components/system-settings-form.tsxsrc/app/[locale]/settings/config/page.tsxsrc/app/api/admin/system-config/route.tssrc/app/api/v1/resources/system/handlers.tssrc/app/api/v1/resources/system/router.tssrc/app/v1/_lib/proxy/discovery-coordinator.tssrc/app/v1/_lib/proxy/discovery-validity.tssrc/app/v1/_lib/proxy/error-handler.tssrc/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/provider-selector.tssrc/app/v1/_lib/proxy/response-handler.tssrc/app/v1/_lib/proxy/session.tssrc/app/v1/_lib/proxy/stream-finalization.tssrc/drizzle/schema.tssrc/instrumentation.tssrc/lib/api-client/v1/openapi-types.gen.tssrc/lib/api/v1/_shared/error-envelope.tssrc/lib/api/v1/_shared/request-body.tssrc/lib/api/v1/schemas/system-config.tssrc/lib/config/env.schema.tssrc/lib/config/system-settings-cache.tssrc/lib/ledger-backfill/trigger.sqlsrc/lib/lifecycle/shutdown.tssrc/lib/observability/discovery-metrics.tssrc/lib/redis/client.tssrc/lib/redis/live-chain-store.storage.test.tssrc/lib/redis/live-chain-store.test.tssrc/lib/redis/live-chain-store.tssrc/lib/redis/lua-scripts.tssrc/lib/redis/session-binding.tssrc/lib/session-manager.tssrc/lib/session-tracker.tssrc/lib/utils/provider-chain-display.test.tssrc/lib/utils/provider-chain-display.tssrc/lib/validation/discovery-settings.tssrc/lib/validation/schemas.tssrc/repository/_shared/transformers.tssrc/repository/message-write-buffer.tssrc/repository/message.tssrc/repository/routing-trace-outbox.tssrc/repository/routing-trace-persistence.tssrc/repository/system-config.tssrc/repository/usage-logs.tssrc/types/message.tssrc/types/routing-trace.tssrc/types/system-config.tstests/api/v1/system/system-config.test.tstests/configs/integration.config.tstests/configs/session-binding.config.tstests/integration/proxy-hedge-lifecycle.test.tstests/integration/session-binding-versioning-redis.test.tstests/unit/actions/system-config-save.test.tstests/unit/api/admin-system-config-route.test.tstests/unit/lib/redis/client.test.tstests/unit/lib/redis/live-chain-store.test.tstests/unit/lib/redis/session-binding.test.tstests/unit/lib/session-manager-binding-smart.test.tstests/unit/lib/session-manager-content-hash.test.tstests/unit/lib/session-manager-terminate-provider-sessions.test.tstests/unit/lib/session-manager-terminate-session.test.tstests/unit/lib/session-manager-versioned-binding.test.tstests/unit/lib/session-tracker-cleanup.test.tstests/unit/lib/shutdown.test.tstests/unit/proxy/discovery-coordinator.test.tstests/unit/proxy/discovery-validity.test.tstests/unit/proxy/error-handler-durable-persistence.test.tstests/unit/proxy/hedge-winner-dedup.test.tstests/unit/proxy/provider-selector-cross-type-model.test.tstests/unit/proxy/provider-selector-group-priority.test.tstests/unit/proxy/provider-selector-model-mismatch-binding.test.tstests/unit/proxy/proxy-forwarder-hedge-first-byte.test.tstests/unit/proxy/proxy-forwarder-provider-session-release.test.tstests/unit/proxy/response-handler-abort-listener-cleanup.test.tstests/unit/proxy/response-handler-client-abort-drain.test.tstests/unit/proxy/response-handler-endpoint-circuit-isolation.test.tstests/unit/proxy/response-handler-exported-finalizers.test.tstests/unit/proxy/response-handler-gemini-stream-passthrough-timeouts.test.tstests/unit/proxy/routing-trace.test.tstests/unit/proxy/terminal-outcome-contract.test.tstests/unit/repository/message-terminal-write-apis.test.tstests/unit/repository/message-write-buffer.test.tstests/unit/repository/routing-trace-outbox.test.tstests/unit/repository/system-config-degradation-ladder.test.tstests/unit/repository/system-config-update-missing-columns.test.tstests/unit/types/routing-trace.test.tstests/unit/usage-ledger/trigger.test.tstests/unit/validation/system-settings-discovery.test.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 671870b6f3
ℹ️ 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".
| sessionId: string, | ||
| keyId: number | ||
| ): Promise<SessionBindingResult> { | ||
| const redis = getRedisClient(); |
There was a problem hiding this comment.
Keep Discovery Redis usable when rate limits are disabled
When ENABLE_RATE_LIMIT=false but Redis is configured for session tracking, the capability probe can still succeed because currentRedisClient() passes allowWhenRateLimitDisabled: true; however this wrapper reopens Redis through the default getRedisClient(), so prepareStreamingDiscovery() gets redis_not_ready from getSessionBindingSnapshot() and skips Discovery before taking a lease. Pass the same override in the versioned binding/lease wrappers so Discovery works independently of the rate-limit toggle.
Useful? React with 👍 / 👎.
| } | ||
| this.deferredOrdinary.delete(acknowledgement.id); | ||
| const existing = this.pending.get(acknowledgement.id); | ||
| this.setPending( |
There was a problem hiding this comment.
[HIGH] [LOGIC-BUG] Deferred ordinary patches can still update a finalized row after the post-terminal routing-trace ACK settles.
Why this is a problem: The new deferral path is supposed to keep late ordinary fields out of the trace-only write so terminal and billing data stay behind the terminal status fence. releaseDeferredOrdinary() requeues that deferred patch as a normal write, though, so the follow-up flush runs without the status_code IS NULL fence. A late costUsd, durationMs, or other ordinary patch can therefore still mutate an already-finalized request after updateMessageRequestRoutingTrace() starts using the post-terminal path.
Suggested fix:
private releaseDeferredOrdinary(acknowledgement: DurableAcknowledgement): void {
if (acknowledgement.writeScope !== "post-terminal-metadata") return;
const patch = this.deferredOrdinary.get(acknowledgement.id);
if (!patch) return;
this.deferredOrdinary.delete(acknowledgement.id);
void this.enqueueDurably(acknowledgement.id, patch, { writeScope: "terminal" }).catch(
(error) => {
logger.error("[MessageRequestWriteBuffer] Failed to replay deferred terminal patch", {
messageRequestId: acknowledgement.id,
error: error instanceof Error ? error.message : String(error),
});
}
);
}| return Response.json({ error: "discoveryWindowInvalid", errorCode }, { status: 400 }); | ||
| } | ||
| const firstError = error.issues[0]; | ||
| return Response.json({ error: firstError.message || "数据验证失败" }, { status: 400 }); |
There was a problem hiding this comment.
[MEDIUM] [LOGIC-BUG] The generic Discovery validation branch drops the structured errorCode and returns the raw token instead.
Why this is a problem: getDiscoveryValidationErrorCode() already distinguishes DISCOVERY_SETTINGS_INVALID from the window check, but this branch falls through to firstError.message. That makes /api/admin/system-config respond with { "error": "DISCOVERY_SETTINGS_INVALID" } for out-of-range Discovery fields instead of the stable { errorCode: "DISCOVERY_SETTINGS_INVALID" } contract used everywhere else in this PR. Clients cannot reliably localize or branch on that failure anymore.
Suggested fix:
if (error instanceof z.ZodError) {
const errorCode = getDiscoveryValidationErrorCode(error.issues);
if (errorCode === DISCOVERY_WINDOW_INVALID_ERROR_CODE) {
return Response.json({ error: "discoveryWindowInvalid", errorCode }, { status: 400 });
}
if (errorCode === DISCOVERY_SETTINGS_INVALID_ERROR_CODE) {
return Response.json({ error: "discoverySettingsInvalid", errorCode }, { status: 400 });
}
const firstError = error.issues[0];
return Response.json({ error: firstError.message || "数据验证失败" }, { status: 400 });
}There was a problem hiding this comment.
Code Review Summary
This PR lands a very large Discovery stack across proxy runtime, Redis/session binding, persistence, and dashboard surfaces. I focused on the new code paths that own request finalization and the new Discovery settings validation flow. The main issues I found are a finalized-row ownership regression in the async message writer and an inconsistent admin API error contract for Discovery validation.
PR Size: XL
- Lines changed: 37116
- Files changed: 120
- Split suggestions: separate Redis/session-binding + migrations, proxy Discovery runtime/finalization, and dashboard/API/i18n surface changes into smaller review stacks where possible.
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 1 | 1 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 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)
src/repository/message-write-buffer.ts:807- deferred ordinary patches are replayed as unfenced writes after a post-terminal routing-trace ACK, so latecostUsd/durationMsupdates can still mutate finalized rows.src/app/api/admin/system-config/route.ts:150- the admin config route drops the structuredDISCOVERY_SETTINGS_INVALIDerrorCodeand returns the raw token instead, making Discovery validation failures harder to localize and handle consistently.
Review Coverage
- Logic and correctness
- Security (OWASP Top 10)
- Error handling
- Type safety
- Documentation accuracy
- Test coverage
- Code clarity
Automated review by Codex AI
There was a problem hiding this comment.
Reviewed PR #1359, applied the size/XL label, and posted the summary review plus 2 inline comments.
Main findings:
src/repository/message-write-buffer.ts:807replays deferred ordinary patches as unfenced writes after post-terminal routing-trace ACKs, so latecostUsd/durationMsupdates can still mutate finalized rows.src/app/api/admin/system-config/route.ts:150returns the rawDISCOVERY_SETTINGS_INVALIDtoken instead of a structurederrorCode, which breaks the new Discovery validation contract.
If you want, I can also draft the follow-up tests for those two paths.
🧪 测试结果
总体结果: ✅ 所有测试通过 |
Summary
This PR is the final integration of the bounded Discovery stack into
dev.It includes:
The five stacked PRs were retargeted to and merged into
integration/discovery-stack-20260723. Merge conflicts were resolved on that branch while preserving both Discovery trace and Codex reasoning-effort behavior.Migration order is preserved as:
#1338 is intentionally not included.
Validation
bun run test: 7504 passed, 13 skipped, 0 failedbun run typecheckbun run validate:migrations: 113 migrations passedbun run lint: exit 0, existing warnings onlybun run build: Next.js 16.2.11, 187/187 static pages generatedgit diff --checkThe integration worktree is clean at
104e42d67819302d98f093f9633c12a41eb76cd5.Greptile Summary
This PR integrates the complete bounded Discovery stack: a new provider-racing system that runs multiple concurrent upstream attempts within configurable SLA windows, validates stream prefixes for protocol correctness before committing a winner, and manages per-session sticky bindings through versioned Redis operations with atomic Lua CAS scripts.
discovery-coordinator.ts,discovery-validity.ts): pure state machine for round/SLA lifecycle and per-chunk SSE parsing across Anthropic, OpenAI, and Gemini protocols; both are dependency-free and unit-tested.session-binding.ts,lua-scripts.ts): eight new Lua scripts implement compare-and-set, touch, clear (with cooldown), terminate, and discovery-lease operations; rolling-upgrade reconciliation between the new canonical hash and the legacysession:*:providermirror is handled atomically inREAD_OR_RECONCILE_SESSION_BINDING.routing-trace-outbox.ts,routing-trace-persistence.ts, migration0111): therouting_traceJSONB column is added tomessage_request; post-terminal traces are staged in a Redis hash outbox and replayed by a background scheduler so a shutdown-time DB outage cannot lose trace data; the ledger trigger is narrowed to exclude the new column.0110,0111): seven newsystem_settingscolumns add Discovery tunables (disabled by default); migration order is preserved and validated.Confidence Score: 5/5
Safe to merge. Discovery is disabled by default (
discovery_enabled = falsein the new settings columns), the outbox/binding operations degrade gracefully when Redis is unavailable, and the ledger trigger is correctly narrowed to exclude the new routing-trace column.No correctness issues found across the coordinator state machine, Lua binding scripts, outbox replay, or stream validity parser. The five constituent PRs represent a large but cohesive change; the 7504-test suite provides broad coverage of the new paths, and every resource-lifecycle edge case (lease renewal, loser cancellation, setup-reservation cleanup) has explicit guards. The only outstanding concern from prior review rounds (doubled Redis reads in
readLiveChainBatch) is a non-blocking performance note.No files require special attention. The new
forwarder.tsorchestration (~2900 added lines) is the densest area, but its structure is clearly factored and covered by dedicated unit tests.Important Files Changed
ensureOwnedbefore any binding mutation.normalizeRoutingTracedeserializer that treats persisted JSON as untrusted and whitelist-validates every field.routing_traceJSONB column and correctly narrows thetrg_upsert_usage_ledgertrigger's UPDATE OF column list to exclude routing_trace, preventing trace-only patches from triggering unnecessary ledger recomputes.routingTraceStorekeyed identically to the existing chain store. Read paths now issue 2 Redis reads per session (noted as a P2 concern in a previous review). ThedeleteLiveChaincleanup correctly removes both keys.updateMessageRequestRoutingTracewith outbox staging + monotonic DB persistence fallback. Both sync and async write modes are covered. Outbox receipt is acknowledged only after successful DB write, ensuring at-least-once delivery.Sequence Diagram
sequenceDiagram participant Client participant Forwarder as ProxyForwarder participant Coord as DiscoveryCoordinator participant Redis as Redis(Binding+Lease) participant Upstream as UpstreamProviders(N) participant Parser as DiscoveryValidityParser participant DB as PostgreSQL Client->>Forwarder: "POST /v1/messages (stream=true)" Forwarder->>Redis: readOrReconcile binding snapshot Forwarder->>Redis: acquireSessionDiscoveryLease Forwarder->>Coord: startStickyProbe / startDiscoveryRacing loop Each round (max maxRounds) Forwarder->>Upstream: Launch concurrent attempts (concurrency N) Upstream-->>Parser: SSE chunks Parser-->>Forwarder: "DiscoveryValidity {ready, terminal, error}" alt Attempt ready (valid prefix) Forwarder->>Coord: markReady(attemptId) Coord-->>Forwarder: commit_normal / none (priority gate) else Round SLA expires Forwarder->>Coord: onRoundBoundary Coord-->>Forwarder: promote_fallback / launch next round / cancel end alt Winner committed Forwarder->>Forwarder: cancelLosers + cancelSetupReservations Forwarder->>Redis: CAS session binding (sticky) Forwarder-->>Client: stream winner response end end alt All rounds exhausted Coord-->>Forwarder: terminal_failure / promote_fallback Forwarder-->>Client: stream fallback or error end Forwarder->>Redis: releaseDiscoveryLease Forwarder->>DB: updateMessageRequestRoutingTrace (outbox-backed)Reviews (2): Last reviewed commit: "fix(discovery): address final integratio..." | Re-trigger Greptile