mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
Diagnosed while chasing the reused-output-index incident (see 705ac7335 / OpenClaw issue #123342): every call-log artifact showed a wire-clean response, even for requests that actually failed, because withEarlyStreamKeepalive injects its startup/keepalive/error frames directly into the outer response stream, entirely outside the request handler's own reqLogger. reqLogger.appendConvertedChunk (which populates pipeline.streamChunks.client) never sees those bytes — only what chatCore.ts's own SSE writer produced. The persisted artifact was answering "what did the handler generate," not "what did the client actually receive," which is the wrong question when diagnosing a client-visible stream defect. withEarlyStreamKeepalive wraps the handler's Promise from OUTSIDE its call tree; the reqLogger it needs to feed is created deep inside chatCore.ts, after routing/model/provider resolution, and doesn't exist yet when the keepalive frames are written. The two sides share no reference — only an identifier, if one is deliberately threaded through both. Fix: responses/route.ts now generates a correlationId before calling handleChat, passes it as handleChat's existing (already-supported, previously-unused-here) 4th positional arg — which chatCore.ts already threads into trackPendingRequest's metadata as entry.correlationId, zero changes needed there — and also into withEarlyStreamKeepalive's options. The wrapper buffers every direct- to-client write (startup frame, periodic ticks, in-band error frames) via the new earlyKeepaliveByteBuffer module, keyed by that same id. chatCore/attemptLogging.ts, which already has correlationId in scope right where it assembles the final pipeline payload before saveCallLog, takes the buffered bytes and prepends them into streamChunks.client in send order. The verbatim-forwarded real response body is deliberately NOT re-recorded here — the handler's own reqLogger already captures that; recording it twice would duplicate it in the artifact. The buffer is consumed exactly once per correlationId and swept on a 10-minute TTL so a request that never reaches the persist call (aborted, detailed logging disabled, a route that doesn't opt in) cannot leak entries forever. Scoped to /v1/responses only, where the incident actually happened. /v1/chat/completions and /v1/messages call withEarlyStreamKeepalive the same way and would need the identical two-line route change to opt in; left as a follow-up rather than bundled in sight-unseen. Test plan: - tests/unit/early-keepalive-byte-buffer.test.ts (new): record/take ordering, single-consumption, per-id isolation, empty-input no-ops, unbounded-growth cap - tests/unit/early-stream-keepalive.test.ts: two new tests — a correlationId records the startup frame and keepalive ticks but NOT the forwarded body; omitting correlationId is a true no-op - tests/unit/attempt-logging-early-keepalive-merge.test.ts (new): real temp-DB end-to-end proof against the actual persisted call-log row — early bytes prepended in send order, consumed exactly once, no-op without a correlationId, gated by detailedLoggingEnabled matching the existing streamChunks capture gate - tests/unit/chatcore-attempt-logging.test.ts (existing): unchanged, still passing — confirms the merge addition doesn't disturb existing persistence behavior - 44 passed total across the above plus earlyStreamKeepalive.test.ts, 2 pre-existing skips unrelated to this change - tsgo --noEmit: clean on all touched files