lakehouse/auditor 10 blocking issues: cloud: claim not backed — "Verified end-to-end against persistent Go stack on :4110:"
Cross-runtime parity with the Go-side observability wave (commits
d6d2fdf + 1a3a82a in golangLAKEHOUSE). The two layers J flagged:
the LIVE per-call view (Langfuse) and the LONGITUDINAL forensic view
(JSONL queryable via DuckDB). Hard correctness gate (FillValidator
phantom-rejection) was already in place; this is the observability
on top.
## Trace-id propagation
X-Lakehouse-Trace-Id header constant declared in
crates/gateway/src/v1/iterate.rs (matches Go's shared.TraceIDHeader
byte-for-byte). When set on an inbound /v1/iterate request, the
handler reuses it; the chat + validate self-loopback hops forward
the same header so chatd's trace emit nests under the parent rather
than minting a fresh top-level trace per call.
ChatTrace gains a parent_trace_id field. emit_chat_inner skips the
trace-create event when parent is set, only emits the
generation-create which attaches to the existing trace tree. Result:
an iterate session with N retries shows in Langfuse as ONE tree, not
N+1 disconnected traces.
emit_attempt_span (new) writes one Langfuse span per iteration
attempt with input={iteration, model, provider, prompt} and
output={verdict, raw, error}. WARNING level on non-accepted
verdicts. The returned span id is stamped on the corresponding
SessionRecord attempt for cross-log correlation.
## Coordinator session JSONL
crates/gateway/src/v1/session_log.rs — new writer matching Go's
internal/validator/session_log.go schema byte-for-byte:
- SessionRecord with schema=session.iterate.v1
- SessionAttemptRecord per retry
- SessionLogger.append: tokio Mutex serialized append-only
- Best-effort posture (slog.Warn on error, never blocks request)
iterate.rs builds + appends a row on EVERY code path:
- accepted: write_session_accepted with grounded_in_roster bool
derived from validate_workers WorkerLookup (matches Go's
handlers.rosterCheckFor("fill") semantics)
- max-iter-exhausted: write_session_failure
- infra-error: write_infra_error (so a missing /v1/iterate event
never silently disappears from the longitudinal log)
[gateway].session_log_path config field (empty = disabled).
Production: /var/lib/lakehouse/gateway/sessions.jsonl. Operators who
want a unified longitudinal stream can point both Rust and Go
loggers at the same path — write-append is safe at the row sizes we
produce.
## Cross-runtime parity probe
crates/gateway/src/bin/parity_session_log: tiny stdin/stdout helper
that round-trips a fixture through SessionRecord serde.
golangLAKEHOUSE/scripts/cutover/parity/session_log_parity.sh feeds
4 fixtures through both helpers and diffs the rows after stripping
timestamp + daemon (the two fields that legitimately differ between
producers).
Result: **4/4 byte-equal** including the unicode-prompt fixture
("Café résumé ⭐ 你好"). Schema parity holds. The non-trivial-equal
guard in the probe rejects the case where both sides fail
identically — protecting against a regression where one side
silently stops producing valid JSON.
## Verification
- cargo test -p gateway --lib: 90/90 PASS (3 new session_log tests
including concurrent-append safety)
- cargo check --workspace: clean
- session_log_parity.sh: 4/4 fixtures byte-equal
- Both runtimes can append to the same path; DuckDB sees one stream
- The Go-side validatord smoke remains 5/5 (unchanged)
## Architecture invariant
Don't propose to "wire trace-id propagation in Rust" or "add Rust
session log" — both are now shipped on the demo/post-pr11-polish
branch. The longitudinal log + Langfuse tree together cover the
multi-call observability concern J flagged 2026-05-02.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
2.2 KiB
Rust
72 lines
2.2 KiB
Rust
//! Cross-runtime parity helper for `SessionRecord` JSON shape.
|
|
//!
|
|
//! Reads a fixture JSON on stdin, builds a `SessionRecord`, emits
|
|
//! one JSONL row on stdout. Used by
|
|
//! `golangLAKEHOUSE/scripts/cutover/parity/session_log_parity.sh`
|
|
//! to verify the Rust gateway's session log shape stays byte-equal
|
|
//! to the Go-side validatord's `validator.SessionRecord` (commit
|
|
//! 1a3a82a in golangLAKEHOUSE).
|
|
|
|
use gateway::v1::session_log::{SessionAttemptRecord, SessionRecord, SESSION_RECORD_SCHEMA};
|
|
use serde::Deserialize;
|
|
use std::io::Read;
|
|
|
|
#[derive(Deserialize)]
|
|
struct FixtureInput {
|
|
session_id: String,
|
|
kind: String,
|
|
model: String,
|
|
provider: String,
|
|
prompt: String,
|
|
iterations: u32,
|
|
max_iterations: u32,
|
|
final_verdict: String,
|
|
attempts: Vec<SessionAttemptRecord>,
|
|
#[serde(default)]
|
|
artifact: Option<serde_json::Value>,
|
|
#[serde(default)]
|
|
grounded_in_roster: Option<bool>,
|
|
duration_ms: u64,
|
|
}
|
|
|
|
fn main() {
|
|
let mut buf = String::new();
|
|
if let Err(e) = std::io::stdin().read_to_string(&mut buf) {
|
|
eprintln!("read stdin: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
let input: FixtureInput = match serde_json::from_str(&buf) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("parse stdin: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
let rec = SessionRecord {
|
|
schema: SESSION_RECORD_SCHEMA.to_string(),
|
|
session_id: input.session_id,
|
|
// Pinned timestamp so both runtimes' rows compare byte-equal
|
|
// when the test wrapper normalizes on `daemon` only.
|
|
timestamp: "2026-01-01T00:00:00+00:00".to_string(),
|
|
daemon: "gateway".to_string(),
|
|
kind: input.kind,
|
|
model: input.model,
|
|
provider: input.provider,
|
|
prompt: input.prompt,
|
|
iterations: input.iterations,
|
|
max_iterations: input.max_iterations,
|
|
final_verdict: input.final_verdict,
|
|
attempts: input.attempts,
|
|
artifact: input.artifact,
|
|
grounded_in_roster: input.grounded_in_roster,
|
|
duration_ms: input.duration_ms,
|
|
};
|
|
match serde_json::to_string(&rec) {
|
|
Ok(s) => println!("{s}"),
|
|
Err(e) => {
|
|
eprintln!("marshal: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|