IBEX Harness
DocsBenchmarksBlogChangelogRoadmap
GitHub
IBEX Harness

Documentation

Architecture Decision RecordsADR-0002: Repository foundation bootstrapADR-0003: Branch protection and merge policyADR-0004: Protobuf and code generation policyADR-0005: Postgres migration strategyADR-0006: Auth protobuf contract (`ibex.auth.v1`)ADR-0007: Auth token validation implementationADR-0008: Security scanning and CI quality gatesADR-0009: Permission bitmap layoutADR-0010: Cryptography policyADR-0011: Proxy auth gRPC client and middlewareADR-0012: Proxy request normalization (OpenAI chat)ADR-0013: Proxy input validation and stable error envelopeADR-0014: Core domain migration sequencingADR-0015: Proxy rate limit skeleton (Phase 1)ADR-0016: Proxy agent identity verification (Phase 1)ADR-0017: Request ID and trace context strategy (Phase 1)ADR-0018: Graceful shutdown contract (Phase 1)ADR-0019: OpenTelemetry provider configuration (Phase 1)ADR-0020: Shared package boundaries — `packages/config` and `packages/apierror`ADR-0021: Prometheus Metric Catalog (Phase 1)ADR-0022: Health check contract (Phase 1)ADR-0023: Docs site architecture (Phase 1.5)ADR-0024: Benchmark data publishing modelADR-0025: LLM provider abstractionADR-0026: OpenAI client designADR-0027: Streaming dual-write strategyADR-0028: Auth cache designADR-0029: Token revocation propagation via Redis pub/subADR-0030: Directive versioning strategyADR-0031: System prompt injection strategyADR-0032: Session data model and retentionADR-0033: ClickHouse llm_traces schema and retentionADR-0034: Proxy overhead performance measurement methodologyADR-0035: Chat Idempotency-Key Redis dedupeADR-0038: Context assembly service design and gRPC contractADR-0039: Proxy Postgres ownership for session and directive storesADR-0040: Anthropic provider adapterADR-0041: Model capability registryADR-0042: Self-hosted OpenAI-compatible LLM adapterADR-0043: Tokenizer registry architectureADR-0044: Non-streaming response pipelineADR-0045: Streaming response transformationADR-0046: Embedder interface and profile registryADR-0047: Memory temporal validity foundationADR-0048: Memory multi-label categoriesADR-0049: Memory relationship graph readinessADR-0050: MCP server skeleton (transport, auth, audit)ADR-0051: Local LGTM observability stack (Phase 2.5 exit pull-forward)ADR-0052: Memory schema v2 expand (HNSW, quality columns)ADR-0053: Vector store abstraction and composite scoring v2
ADRs›ADR-0045: Streaming response transformation
ADRs

ADR-0045: Streaming response transformation

Architecture decision record 0045 — physical flush constraint, token-window buffering for pattern stages, post-hoc semantic correction, and Phase 3 stream-stage contract sketch.

ADR-0045: Streaming response transformation

  • Status: Accepted
  • Date: 2026-08-23
  • Authors: IBEX Harness team
  • Milestone: 2.5.G3.M2 Streaming response pipeline design

Context

ADR-0044 ships a typed non-streaming response pipeline. Streaming remains verbatim dual-write per ADR-0027: flush-per-SSE-event, TeeReader into StreamAccumulator, no JSON error envelopes after headers start.

Phase 3 will want pattern-based guardrails (PII / secrets / quarantined-string detection) and, separately, semantic review. A stage that needs the complete response cannot rewrite bytes already flushed to the client.

Industry practice for streaming PII gateways (bounded holdback / sliding window, safe flush of non-partial prefixes, fail-closed under overload) confirms the milestone options rather than inventing a fourth path.

Decision

1) Physical constraint (non-negotiable)

Once an SSE event has been written and flushed to the client, those bytes are immutable from the proxy’s perspective. There is no correct transparent way to “unsend” or rewrite them.

2) Options evaluated

OptionSummaryTradeoff
1 Token-window bufferingHold N runes behind the live edge; run cheap pattern checks; flush only the safe prefixAdds TTFT proportional to holdback; works for regex/pattern class only
2 Post-hoc correctionStream unmodified; flag problems to observability / human review after the factHonest about irreversibility; no client mutation
3 Disable streamingForce stream=false for agents that require full-document rewriteCorrect when rewrite is mandatory; needs per-agent config (not in G3.M2)

3) Adopted approach (hybrid)

  • Pattern-class stages (Phase 3): Option 1 — bounded token/rune window on assistant content deltas (not raw SSE framing). Holdback must be ≥ longest pattern. Production holdback default target: 64 runes (tunable); prototype enforces holdback ≥ 14 for its synthetic matcher.
  • Semantic / LLM-judged stages: Option 2 — post-hoc signals only (metrics, drift/alerts, dashboard). Never mutate already-flushed stream bytes.
  • Mandatory full rewrite: Option 3 — document as a Phase 3 operator escape hatch (per-agent / per-directive force non-streaming). Not implemented in this ADR’s milestone.

Explicit non-goal: seamless full-content correction on a live streaming response.

4) SSE framing vs content buffer

Holdback applies to decoded content delta text. The proxy must not re-frame OpenAI SSE, inject events, or rewrite tool-call deltas unless a future ADR revisits ADR-0027’s verbatim policy. Pattern stages extract/re-emit content fields only.

5) Stream-stage contract (Phase 3 sketch — not shipped)

packages/responsepipeline.Stage / ChatResponse remain non-streaming full-document APIs. Streaming needs a separate contract, for example:

Go
type StreamStage interface {
    Name() string
    Feed(ctx context.Context, delta string) (emit string, err error)
    Flush(ctx context.Context) (emit string, err error)
}

Security-critical stream stages fail closed by aborting upstream read and stopping client writes (incomplete stream) or masking the remaining buffer — never returning a JSON error envelope after headers (ADR-0027 §4). Non-critical stages may skip/passthrough with metrics (mirror ADR-0044 fail-open where possible).

6) Checkpoint / accumulator policy

Today TeeReader accumulates upstream bytes. When Phase 3 transforms client-visible content, the default is: accumulate client-visible content so session/trace text matches what the user saw. Dual-track upstream+client is deferred unless product requires both.

7) Metrics

MetricStreaming + window behavior
ibex_proxy_stream_duration_secondsUnchanged definition: headers → copy end (includes buffer hold time)
ibex_proxy_stream_backpressure_events_totalUnchanged: client write/flush ≥ 50ms. Holdback CPU alone must not invent backpressure
Added TTFT (Phase 3 budget input)Separate measurement: time until first non-empty client emit under holdback; not a new Prometheus series in G3.M2

8) Security and DoS

  • Never log raw stream content or matched substrings (ADR-0044 posture).
  • Hard per-request buffer cap (MaxBufferRunes) and absolute ceiling (prototypeAbsoluteMaxBufferRunes); overflow / oversize config → fail-closed (ErrPrototypeBufferOverflow / ErrPrototypeInvalidConfig).
  • Reject unbounded “buffer entire stream then transform” designs for the default path.

9) Prototype (G3.M2 deliverable)

Throwaway implementation in services/proxy/internal/http/stream_transform_prototype.go — not wired into forwardSSEStream. Synthetic matcher SECRET[0-9]{1,8} only.

Prototype measurements (local, 2026-08-23)

Environment: linux/amd64, Intel Core Ultra 7 155H. Command: go test ./services/proxy/internal/http/ -bench BenchmarkPrototypeWindow -benchtime=50ms.

MeasurementResultPhase 3 implication
First emit CPU (holdback 16)~1.5–2 µs / first emitBuffer CPU is negligible vs model token latency
First emit CPU (holdback 64)~6–8 µsDefault holdback is cheap in-process
First emit CPU (holdback 256)~45–54 µsStill ≪ 1 ms; perceptual TTFT dominated by waiting for N model tokens
Steady Feed (holdback 64)~1.6–2 µs/opFits ADR-0034 overhead discipline for the buffer itself
Client-visible TTFB delta (holdback 64 vs near-passthrough 14; 4-rune chunks)+13 feeds until first emit; ~13 ms modeled wall delta at 1 ms inter-chunkPhase 3 SLOs should budget ≈ (holdback/chunk_runes) × inter_token_ms added TTFT
Per-request memory (holdback 64, ASCII)64 bytes retained content buffer at steady state; ~64 runes/request across 100 windowsBudget ≈ holdback bytes (ASCII) + small struct/alloc overhead per in-flight stream
100 concurrent windows construct~91 KiB allocs/op in benchDominated by builder growth during prime, not steady retained size
Max Feed vs 50 ms backpressure thresholdmax feed ≪ 50 msHoldback does not invent ADR-0027 backpressure; slow client flush remains the signal

Perceptual added TTFT (client-visible): modeled as feeds_until_emit_delta × assumed_inter_chunk. With 4-rune chunks and 1 ms inter-chunk, default holdback 64 adds ~13 ms vs the minimum legal holdback (14). Industry reports for similar windows often cite ~30–80 ms wall delay when the model is already streaming; Phase 3 must measure against real providers before setting SLOs.

Consequences

Positive:

  • Phase 3 cannot accidentally promise full-stream rewrite.
  • Pattern redaction has a validated algorithm + fail-closed overflow.
  • Metrics semantics stay compatible with ADR-0027.

Negative / follow-ups:

  • Production PII packs, stream-stage wiring, and per-agent force-non-streaming remain Phase 3+.
  • Content-delta extraction from OpenAI SSE (without breaking tool calls) needs careful Phase 3 design.
  • Client-visible accumulator change is a behavior change for checkpoints/traces when transforms land.

References

  • ADR-0027 Streaming dual-write
  • ADR-0044 Non-streaming response pipeline
  • ADR-0034 Performance methodology
  • Milestone 2.5.G3.M2

Was this page helpful?

Edit on GitHub

Last updated on

PreviousADR-0044: Non-streaming response pipelineNextADR-0046: Embedder interface and profile registry

On this page

  • Context
  • Decision
  • 1) Physical constraint (non-negotiable)
  • 2) Options evaluated
  • 3) Adopted approach (hybrid)
  • 4) SSE framing vs content buffer
  • 5) Stream-stage contract (Phase 3 sketch — not shipped)
  • 6) Checkpoint / accumulator policy
  • 7) Metrics
  • 8) Security and DoS
  • 9) Prototype (G3.M2 deliverable)
  • Prototype measurements (local, 2026-08-23)
  • Consequences
  • References
0%