term_msg: carry the source event's time on every row
A terminal row published on `$SWARM.term.<hive>.<agent>` goes out bare, with no envelope around it and no server-side stamp, so a subscriber had nothing to place the row in time with beyond its own receipt clock — wrong by the queue's latency and meaningless for anything read later than live. `TermMsg` gains `ts`, ISO 8601 UTC. `classify` takes the event's own unix-seconds stamp and applies it to every row that event expands into, so a row replayed out of sqlite says when it happened rather than when it was read, and a row that sat in a lagging subscriber's buffer does not lie about its time. The oversize degrade keeps it; only the body is ever spent. `TermEnvelope` stops duplicating `ts` and keeps `seq`: the dedup counter is a real transport concern, the event's time is not, now that it rides on the row. Nothing in the frontend read `envelope.ts` — only the type declared it. Refs #4321
This commit is contained in:
parent
349f7096ba
commit
053340128b
5 changed files with 206 additions and 38 deletions
|
|
@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||
use tokio_stream::{Stream, StreamExt, wrappers::BroadcastStream};
|
||||
|
||||
use super::AppState;
|
||||
use crate::term_msg::{ClassifyCtx, Level, TermMsg, classify};
|
||||
use crate::term_msg::{ClassifyCtx, Level, TermMsg, classify, iso8601_utc};
|
||||
|
||||
/// One classified envelope on the wire: transport-level metadata (a sibling
|
||||
/// of the terminal-row payload, not part of it) plus the zero-or-more rows
|
||||
|
|
@ -20,14 +20,15 @@ use crate::term_msg::{ClassifyCtx, Level, TermMsg, classify};
|
|||
///
|
||||
/// `seq` is the live per-event dedup counter (`BusEvent::seq`) — `Some` on
|
||||
/// the SSE path, `None` on history replay (a stored row has no live seq).
|
||||
/// Same category of plumbing as `ts`: the client already used it to drop
|
||||
/// buffered live traffic it's about to see again in the initial history
|
||||
/// page, and that need didn't go away just because rows lost their `kind`
|
||||
/// tag — dropping it here would silently reintroduce duplicate rows across
|
||||
/// the live/history boundary.
|
||||
/// The client uses it to drop buffered live traffic it's about to see again
|
||||
/// in the initial history page, and that need didn't go away just because
|
||||
/// rows lost their `kind` tag — dropping it here would silently reintroduce
|
||||
/// duplicate rows across the live/history boundary. It is the one piece of
|
||||
/// transport plumbing left: the event's time now rides on each row's own
|
||||
/// `ts` (`crate::term_msg`), which is where a consumer with no envelope
|
||||
/// around it — the swarm queue's — can also read it.
|
||||
#[derive(Serialize)]
|
||||
pub(super) struct TermEnvelope {
|
||||
ts: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
seq: Option<u64>,
|
||||
msgs: Vec<TermMsg>,
|
||||
|
|
@ -87,15 +88,11 @@ pub(super) async fn events_history(
|
|||
let events: Vec<TermEnvelope> = events
|
||||
.into_iter()
|
||||
.filter_map(|se| {
|
||||
let msgs = classify(&se.event, &mut ctx);
|
||||
let msgs = classify(&se.event, se.ts, &mut ctx);
|
||||
if msgs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(TermEnvelope {
|
||||
ts: se.ts,
|
||||
seq: None,
|
||||
msgs,
|
||||
})
|
||||
Some(TermEnvelope { seq: None, msgs })
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -117,10 +114,15 @@ pub(super) async fn events_stream(
|
|||
// stream rather than emitted to the bus — a bus emit would spam every
|
||||
// already-connected client with a spurious note each time anyone opens
|
||||
// the stream.
|
||||
// Synthesised here rather than classified from a bus event, so this is
|
||||
// the one row that stamps itself: its "source event" is the subscribe
|
||||
// that just happened.
|
||||
let hello_envelope = TermEnvelope {
|
||||
ts: chrono::Utc::now().timestamp(),
|
||||
seq: None,
|
||||
msgs: vec![TermMsg::new(Level::Debug, "live stream attached")],
|
||||
msgs: vec![
|
||||
TermMsg::new(Level::Debug, "live stream attached")
|
||||
.at(iso8601_utc(chrono::Utc::now().timestamp())),
|
||||
],
|
||||
};
|
||||
let hello = Event::default().data(serde_json::to_string(&hello_envelope).unwrap_or_default());
|
||||
// One `ClassifyCtx` per connection, moved into the closure — tool_use→
|
||||
|
|
@ -130,12 +132,11 @@ pub(super) async fn events_stream(
|
|||
let mut ctx = ClassifyCtx::default();
|
||||
let live = BroadcastStream::new(rx).filter_map(move |res| {
|
||||
let ev = res.ok()?;
|
||||
let msgs = classify(&ev.event, &mut ctx);
|
||||
let msgs = classify(&ev.event, ev.ts, &mut ctx);
|
||||
if msgs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let envelope = TermEnvelope {
|
||||
ts: ev.ts,
|
||||
seq: Some(ev.seq),
|
||||
msgs,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue