Skip to content

feat(protocol): add runtime event envelope#2252

Open
cyq1017 wants to merge 2 commits into
Hmbown:mainfrom
cyq1017:codex/runtime-sse-envelope
Open

feat(protocol): add runtime event envelope#2252
cyq1017 wants to merge 2 commits into
Hmbown:mainfrom
cyq1017:codex/runtime-sse-envelope

Conversation

@cyq1017
Copy link
Copy Markdown
Contributor

@cyq1017 cyq1017 commented May 27, 2026

Summary

  • add a versioned RuntimeEventEnvelope protocol type for runtime event streams
  • emit /v1/threads/{id}/events SSE payloads through the shared envelope while preserving existing SSE event names
  • document the envelope contract for desktop clients, replay, and event-log consumers

Compatibility

  • keeps existing event, thread_id, turn_id, item_id, seq, timestamp, and payload fields in the JSON body
  • adds schema_version, kind, and created_at for a more stable typed-client envelope
  • serializes missing turn_id / item_id as null to preserve the previous thread-level event shape
  • separates API envelope schema versioning from the runtime store schema version

Validation

  • cargo fmt --all -- --check
  • cargo test -p codewhale-protocol
  • cargo test -p codewhale-tui runtime_api::tests
  • cargo check -p codewhale-tui -p codewhale-protocol
  • git diff --check

Greptile Summary

This PR introduces a versioned RuntimeEventEnvelope struct in the codewhale-protocol crate and wires it through the TUI's /v1/threads/{id}/events SSE endpoint, replacing an ad-hoc json!() literal with a typed, serializable struct.

  • Adds RuntimeEventEnvelope with schema_version, kind, created_at, and a #[serde(flatten)] catch-all extra: BTreeMap<String, Value> for forward-compatible unknown fields; existing fields (seq, event, thread_id, turn_id, item_id, timestamp, payload) are preserved with identical semantics.
  • Updates runtime_event_payload in runtime_api.rs to construct the envelope struct and serialize it (via .expect()), and expands integration tests to assert envelope field presence and shape on the SSE stream.
  • Documents the schema_version / kind / created_at contract in RUNTIME_API.md, explicitly noting that schema_version here tracks the API envelope version independently of the runtime store schema version.

Confidence Score: 5/5

Safe to merge. The change is purely additive: it introduces a new typed envelope struct, wires it into the existing SSE path, and adds comprehensive round-trip tests. Existing event names, seq, thread_id, and payload fields are all preserved unchanged.

The protocol struct is correctly typed with serde defaults, optional null serialization for turn_id/item_id, and a forward-compat BTreeMap for unknown fields — all verified by the new parity tests. The production code path always constructs the struct with well-defined values, so the serde serialization step is reliably infallible. No regressions to the existing SSE contract are introduced. All open concerns from prior review threads are pre-existing and not worsened by this revision.

No files require special attention. The two test assertions flagged in previous review threads (pinning assert_eq before a broader event-name predicate) are the only residual rough edges and do not affect the production path.

Important Files Changed

Filename Overview
crates/protocol/src/lib.rs Adds the runtime submodule with RuntimeEventEnvelope and its schema version constant; struct is well-typed with correct serde annotations including flatten-based forward-compat BTreeMap.
crates/protocol/tests/parity_protocol.rs Adds five focused round-trip tests covering schema_version default, null turn_id/item_id serialization, unknown field preservation, and full field parity; coverage is thorough.
crates/tui/src/runtime_api.rs Replaces the ad-hoc json!() literal in runtime_event_payload with a typed RuntimeEventEnvelope; tests assert envelope fields correctly, though two test functions retain a pinning assert_eq on thread.started before a broader event-name predicate (already flagged in previous review threads).
crates/tui/Cargo.toml Adds codewhale-protocol as a direct dependency; version is consistent with other protocol crates in the workspace.
docs/RUNTIME_API.md Updates the SSE payload example with the new envelope fields and adds a compatibility notes section explaining schema_version independence, the event/kind mirror, and the created_at alias semantics.
Cargo.lock Adds codewhale-protocol as a resolved dependency for codewhale-tui; no unexpected transitive dependency changes.

Sequence Diagram

sequenceDiagram
    participant Client
    participant TUI as codewhale-tui runtime_api
    participant Store as RuntimeEventRecord store
    participant Proto as codewhale-protocol

    Client->>TUI: "GET /v1/threads/id/events?since_seq=N"
    TUI->>Store: fetch events since seq N
    Store-->>TUI: Vec of RuntimeEventRecord

    loop each RuntimeEventRecord
        TUI->>Proto: runtime_event_payload(record)
        note over Proto: Constructs RuntimeEventEnvelope with schema_version and kind
        Proto-->>TUI: serde_json Value envelope
        TUI->>Client: SSE event line plus data line with envelope JSON
    end

    TUI->>Client: SSE done event
Loading

Reviews (4): Last reviewed commit: "test(protocol): tighten runtime envelope..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a formalized RuntimeEventEnvelope struct under the deepseek-protocol runtime module to standardize the schema of SSE event payloads. It adds fields such as schema_version, kind, and created_at, while supporting forward compatibility by preserving unknown fields. The TUI runtime API has been updated to use this new envelope, and comprehensive unit tests and documentation have been added. Feedback is provided regarding the serialization fallback path in runtime_event_payload, where using .expect(...) is recommended over an invalid fallback schema since serialization is infallible.

Comment thread crates/tui/src/runtime_api.rs Outdated
Comment on lines +1473 to +1493
let event_name = event.event.clone();
let timestamp = event.timestamp.to_rfc3339();
let schema_version = RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION;
let seq = event.seq;
let thread_id = event.thread_id.clone();
let envelope = RuntimeEventEnvelope {
schema_version,
seq,
event: event_name.clone(),
kind: event_name,
thread_id: event.thread_id,
turn_id: event.turn_id,
item_id: event.item_id,
timestamp: timestamp.clone(),
created_at: Some(timestamp),
payload: event.payload,
extra: Default::default(),
};
serde_json::to_value(envelope).unwrap_or_else(
|_| json!({ "schema_version": schema_version, "seq": seq, "thread_id": thread_id }),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fallback path in unwrap_or_else produces a JSON object that violates the RuntimeEventEnvelope schema because it lacks required fields like event, kind, timestamp, and payload. Since RuntimeEventEnvelope only contains standard serializable types and serde_json::Value, its serialization is infallible. Using .expect(...) is safer, cleaner, and avoids an unnecessary clone of thread_id.

    let event_name = event.event.clone();
    let timestamp = event.timestamp.to_rfc3339();
    let envelope = RuntimeEventEnvelope {
        schema_version: RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION,
        seq: event.seq,
        event: event_name.clone(),
        kind: event_name,
        thread_id: event.thread_id,
        turn_id: event.turn_id,
        item_id: event.item_id,
        timestamp: timestamp.clone(),
        created_at: Some(timestamp),
        payload: event.payload,
        extra: Default::default(),
    };
    serde_json::to_value(envelope).expect("RuntimeEventEnvelope serialization is infallible")

Comment thread crates/tui/src/runtime_api.rs
Comment thread crates/tui/src/runtime_api.rs Outdated
Comment on lines +1491 to +1493
serde_json::to_value(envelope).unwrap_or_else(
|_| json!({ "schema_version": schema_version, "seq": seq, "thread_id": thread_id }),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent degradation on serialization failure drops all event data

serde_json::to_value on this struct is infallible in practice (all fields are String, u64, Option<String>, Value, BTreeMap<String, Value>), but if it ever fires, the fallback silently strips event, kind, timestamp, payload, and all other fields from the response. Clients would receive a minimal three-field object with no indication that anything went wrong. At minimum this deserves a tracing::error! or debug_assert! so failures are observable.

Suggested change
serde_json::to_value(envelope).unwrap_or_else(
|_| json!({ "schema_version": schema_version, "seq": seq, "thread_id": thread_id }),
)
serde_json::to_value(envelope).unwrap_or_else(|err| {
tracing::error!(seq, %thread_id, %err, "failed to serialize RuntimeEventEnvelope; falling back to minimal payload");
json!({ "schema_version": schema_version, "seq": seq, "thread_id": thread_id })
})

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread crates/protocol/src/lib.rs
@cyq1017 cyq1017 force-pushed the codex/runtime-sse-envelope branch from 71e1940 to baa47a3 Compare May 27, 2026 04:34
@cyq1017 cyq1017 force-pushed the codex/runtime-sse-envelope branch from baa47a3 to d102cbd Compare May 27, 2026 04:42
@cyq1017 cyq1017 marked this pull request as ready for review May 27, 2026 04:54
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented May 27, 2026

Independent review (Devin):

Schema design — sound. schema_version = 1 with #[serde(default)] handles old payloads gracefully; the flattened BTreeMap<String,Value> for extra is the right forward-compat pattern. RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION as a named constant avoids magic numbers.

serde tags — no breaking change. Struct serialization is flat, so the wire shape is identical to the previous json!{} block plus three new keys (schema_version, kind, created_at). Existing SSE consumers that ignore unknown fields are unaffected; the SSE event name is set by the outer SseEvent, not the payload, so that contract is untouched.

Two real issues to address before merge:

  1. Duplicate thread_id assertionassert_eq!(payload_b["thread_id"], thread_id) appears on both line 2773 (new block) and line 2782 (pre-existing). One is dead. Remove the one at 2782.

  2. Roundtrip test leaves fields uncheckedruntime_event_envelope_roundtrip verifies seq and event==kind after re-encoding, but does not assert schema_version, thread_id, turn_id, item_id, or payload values. A serde rename typo on any of those would pass silently. Add full-field assertions (the greptile suggestion is correct).

Minor: created_at being an explicit duplicate of timestamp is self-documented in RUNTIME_API.md, but there is no deprecation note on timestamp — clarify which is canonical long-term to avoid clients depending on both.

v0.8.48 overlap: PR #2256 consolidates workspace crates (14→11) and touches crates/tui/Cargo.toml, but does not touch crates/protocol/. Merge conflict risk is limited to the [dependencies] block in crates/tui/Cargo.toml — resolve manually after #2256 lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants