Skip to content

[Feature]: add durable stream-stage timeline and failure attribution to request history #1217

Description

@ShannwYang

Area

Streaming / request history / observability

Current status

OpenCodex already has several pieces of correlation-safe request observability:

  • every request has an OpenCodex requestId;
  • request history is durably backed by usage.jsonl and exposed through the paginated /api/request-history API;
  • firstOutputMs records semantic TTFT;
  • failure rows can persist fields such as errorCode, terminalStatus, closeReason, and redacted upstreamError;
  • durable routeDecision traces explain why a provider/model/account route was selected;
  • the live request-log context can classify some terminals using transportPhase and terminalSource.

However, the durable request-history record still cannot reconstruct the end-to-end streaming timeline.

In particular, transportPhase and terminalSource are currently live request-log diagnostics rather than durable usage.jsonl fields, and they do not identify the actual failure side precisely enough.

This issue tracks the remaining durable streaming-stage observability.

Problem

When a Codex turn remains on “thinking” or ends with a stream-disconnected error, an operator should be able to determine from one request-history record whether the delay or failure occurred:

  • before OpenCodex dispatched upstream;
  • while waiting for upstream response headers;
  • while reading the upstream response body/SSE stream;
  • while transforming or relaying upstream data;
  • while writing downstream;
  • because the client cancelled;
  • during terminal-event delivery.

Today several of these cases can collapse into the same terminal status.

For example:

terminalSource = synthetic
transportPhase = mid_stream
status = 502

does not prove that the provider caused the failure.

A synthetic terminal identifies who manufactured the terminal event, while a broad relay catch may cover both upstream reads and local relay/rewriting work.

Required durable timeline

Add a bounded, payload-free stream timeline to the canonical request-history row.

At minimum record elapsed milliseconds from request acceptance for:

upstreamDispatchMs
upstreamHeadersMs
upstreamFirstByteMs
upstreamFirstSemanticOutputMs
downstreamFirstWriteMs
upstreamEndMs
downstreamEndMs

Existing firstOutputMs may satisfy upstreamFirstSemanticOutputMs if the naming/relationship is explicit and backwards compatible.

All timings are best-effort observability only.

Failure to record a timing must never affect request delivery.

Failure attribution

Add stable bounded enums that identify where the terminal condition originated.

For example:

failureSide:
  upstream
  relay
  downstream
  client
  local

failureStage:
  pre_dispatch
  upstream_wait_headers
  upstream_read
  relay_transform
  downstream_write
  client_cancel
  terminal_delivery

Exact enum names may follow repository conventions, but the contract must distinguish these cases without parsing free-form error text.

transportPhase and terminalSource may remain for compatibility, but they must not be treated as substitutes for causal attribution.

Interpretation

The resulting record should support deterministic conclusions such as:

no upstreamDispatchMs
  -> failure occurred before upstream dispatch

upstreamDispatchMs but no upstreamHeadersMs
  -> provider/network failure before response headers

upstreamFirstByteMs but no downstreamFirstWriteMs
  -> OpenCodex relay/transform path

downstreamFirstWriteMs then upstream_read failure
  -> upstream/network mid-stream failure

upstreamEndMs but no downstreamEndMs
  -> downstream relay/client path

Example

{
  "requestId": "ocx-...",
  "status": 502,
  "durationMs": 61342,
  "firstOutputMs": 9107,
  "streamTimeline": {
    "upstreamDispatchMs": 12,
    "upstreamHeadersMs": 4410,
    "upstreamFirstByteMs": 4421,
    "downstreamFirstWriteMs": 4423,
    "upstreamEndMs": 61340,
    "downstreamEndMs": 61342
  },
  "failureSide": "upstream",
  "failureStage": "upstream_read",
  "transportPhase": "mid_stream",
  "terminalSource": "synthetic"
}

Persistence

The timeline and attribution must be part of the canonical durable request-history representation, not only the in-memory /api/logs ring.

They must therefore survive:

ocx stop
ocx start

and remain available from:

GET /api/request-history/:requestId

The request-history SQLite index remains a derived projection; usage.jsonl stays canonical unless the underlying storage contract is intentionally changed separately.

Protocol coverage

Cover both:

  • native Responses passthrough;
  • translated/provider-adapted streaming paths.

Instrumentation should be defined at shared transport/relay boundaries where possible rather than independently inventing incompatible timelines per adapter.

Non-streaming requests may omit stream-only fields.

Request correlation

The existing OpenCodex requestId remains the primary correlation key.

Where protocol-compatible and not already present, return it to clients as:

x-opencodex-request-id

so a client-visible failure can be matched to the durable request-history record without exposing internal state.

Privacy and bounds

The feature must not persist:

  • authorization headers or credentials;
  • prompts or request bodies;
  • images;
  • raw SSE payloads;
  • account identifiers that are not already part of the approved history contract;
  • resolved peer addresses;
  • unbounded exception text;
  • arbitrary stack traces.

Timeline fields are numeric timestamps/durations and failure attribution uses closed enums.

Existing redaction and bounded-metadata rules continue to apply.

Relationship to existing work

Already delivered and not part of this issue:

  • durable request-history indexing and pagination;
  • firstOutputMs TTFT;
  • durable route-decision traces;
  • existing terminal status/error diagnostics;
  • existing live transportPhase / terminalSource classification.

Related issue #919 established why terminalSource="synthetic" is insufficient for causal attribution: the synthetic event producer is not necessarily the underlying failure source.

#820 remains the broader architecture/performance tracker. This issue owns only correlation-safe streaming-stage observability.

Acceptance criteria

Durable timeline

  • Canonical request history records upstream dispatch time.
  • Canonical request history records upstream response-header time.
  • Canonical request history records first upstream body/SSE receipt.
  • Existing semantic TTFT is explicitly represented.
  • Canonical request history records first successful downstream write.
  • Canonical request history records upstream body end/failure time.
  • Canonical request history records downstream end/cancel/failure time.
  • Timeline fields survive proxy restart.

Attribution

  • Stable bounded failure-side classification exists.
  • Stable bounded failure-stage classification exists.
  • Upstream header wait is distinguishable from upstream body-read failure.
  • Upstream body-read failure is distinguishable from relay/transform failure.
  • Relay/transform failure is distinguishable from downstream write failure.
  • Client cancellation is distinguishable from upstream failure.
  • Terminal-delivery failure is distinguishable from the underlying stream failure.
  • Attribution does not depend on parsing arbitrary exception text.

Coverage

  • Native Responses passthrough is covered.
  • Translated provider streaming is covered.
  • Eager and non-eager relay paths produce compatible stage semantics.
  • Requests with no semantic model delta remain diagnosable.
  • Non-streaming requests remain backwards compatible.

Safety

  • Instrumentation is best-effort and cannot fail request delivery.
  • No raw request or SSE payload is persisted.
  • No credential material is persisted.
  • No new unbounded text fields are introduced.
  • Added metadata remains within existing request-history size/bounding expectations.

API

  • GET /api/request-history/:requestId exposes the durable timeline and attribution.
  • Existing request-history rows without the new fields remain readable.
  • Existing pagination/index rebuild behavior remains compatible.
  • The OpenCodex request ID can be correlated back from the client response where protocol-compatible.

Activity

github-actions commented on Aug 7, 2026

@github-actions
Contributor

Automated translation bookkeeping — detected language: English.

Wibias commented on Aug 8, 2026

@Wibias
Contributor

Parent architecture/performance tracker: #820

This issue provides the correlation-safe stage observability needed to distinguish upstream, relay, downstream, cancellation, and runtime-pressure failures.

changed the title [-]Feature: Correlation-safe end-to-end streaming stage timeline[/-] [+][Feature]: add durable stream-stage timeline and failure attribution to request history[/+] on Aug 9, 2026

github-actions commented on Aug 9, 2026

@github-actions
Contributor

Maintainer decision respected

A maintainer has reopened this issue. The automated closure has been deactivated.

reopened this on Aug 9, 2026

lidge-jun commented on Aug 19, 2026

@lidge-jun
Owner

리뷰 · 우선순위 64 / 80

현재 dev의 요청 관측은 상관 ID와 TTFT까지는 있습니다. src/server/request-log.ts에 firstOutputMs, transportPhase(pre_headers | mid_stream | terminal_sse), terminalSource(upstream | synthetic)가 있고, /api/request-history와 usage.jsonl에는 firstOutputMs가 내려갑니다. 그러나 src/usage와 src/routing/history에는 transportPhase/terminalSource 필드가 없습니다. 라이브 로그 진단이지 내구성 있는 히스토리 행이 아닙니다.

이슈가 원하는 것은 한 행만 보고 실패 구간을 가르는 타임라인입니다. upstreamDispatchMs, upstreamHeadersMs, upstreamFirstByteMs, upstreamFirstSemanticOutputMs, downstreamFirstWriteMs, upstreamEndMs, downstreamEndMs를 요청 수락 시각 기준 경과 ms로 남기자는 요청입니다. 지금 terminalSource=synthetic + transportPhase=mid_stream + 502는 업스트림 잘못인지, 로컬 릴레이/변환인지, 클라이언트 취소인지 증명하지 못합니다.

네이티브 신뢰성 관점에서 이 공백은 증상 이슈(#314, #509)와 #820 메모리/동시성 프로그램의 관측 기반입니다. Windows/서비스에서 스트림이 thinking에 머물거나 끊길 때, 재현 없이 히스토리만으로 구간을 못 나누면 패치 우선순위를 잘못 잡습니다. 페이로드를 저장하라는 요청이 아니라 구간 시각만 남기라는 점이 위생적으로도 맞습니다.

구현 위치는 이미 있습니다. request-log의 라이브 컨텍스트에 구간 시각을 더하고, usage/log.ts 직렬화와 history indexer가 같은 필드를 선택적으로 보존하면 됩니다. firstOutputMs는 upstreamFirstSemanticOutputMs와 관계를 명시하고 호환을 깨지 말아야 합니다. 타이밍 기록 실패가 요청 전달을 막으면 안 된다는 이슈 제약도 현재 로그 경로의 best-effort 성격과 맞습니다.

해결방안은 observe-only 슬라이스입니다. 라이브 필드 확장 → usage.jsonl 선택 직렬화 → history API 노출 순으로 가고, 실패 귀속 라벨은 terminalSource와 별도로 “누가 바이트를 잃었는지”를 적어야 합니다. 패치는 추측하지 않으며, 현재 트리에 위 구간 ms 필드는 없습니다.

이 댓글은 grok-bot이 작성했습니다

added a commit that references this issue on Aug 22, 2026
8c74f36

58 remaining items

added 7 commits that reference this issue on Sep 16, 2026
9840471
380b651
277fab1
5318aaf
96451cc
4e85c9f
ecaabb3
added a commit that references this issue on Sep 17, 2026
bd44cb9
added 7 commits that reference this issue on Sep 19, 2026
588eae7
d94317d
2451e93
4d1e015
088fc38
e328774
6fc0a8f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requeststreamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions