feat(telemetry): adopt from the construction boundary — constructor seeds never re-export
A cursor-less adoption (process restart + resume, fork, seam-module reload) replayed the session's full log from seq 0, re-exporting history that already left the process — a resume re-billed its entire stored log on every restart, and a fork re-shipped the parent's prefix under the child's id, doubling query-time counts on OTLP backends with no native ingest dedupe. dsh-session now exposes the fact the constructor already validated but discarded: Session.firstLiveSeq, the constructor-seed length — the first seq appended in this process. header.seedLength cannot serve here: it is the durable fork-lineage boundary, and a resumed session's constructor seed is its full stored log while the header keeps the original fork value (llm-replay and session-query-sqlite depend on that meaning). Constructor seeds also never publish on the session/event firehose, so adoption replaying them was inconsistent with the system's own publication semantics. Adoption's cursor-less fallback starts at firstLiveSeq; seed events still feed the chunk projection, so mid-step continuations re-drop after a resume. Fork streams are no longer self-contained: records now carry session.seed_length (with the existing session.parent_id) so receivers stitch the child's stream onto the parent's. Accepted cost, consistent with at-most-once delivery and recorded in the revival Agent Note: a resume no longer backfills records a previous process failed to deliver — a deployment with that requirement needs the deferred outbox, not replay. Pinned red-first: seeded adoption exports nothing (assertion reversed from the prior seed-readback test, obsolete behavior changed with its test), resume-shaped seed rebuilds the projection without exporting, and fork records carry the stitch attributes.
This commit is contained in:
@@ -117,17 +117,23 @@ export class TelemetryCoordinator {
|
||||
|
||||
/**
|
||||
* Adopt a session: replay its log THROUGH the projection from the handoff
|
||||
* cursor (or from the start when no cursor survived), then rely on the
|
||||
* firehose for everything after. Events at or below the cursor still feed
|
||||
* the projection state (first-chunk tracking) without being re-handed, so
|
||||
* a resumed fiber drops mid-step chunk continuations exactly like the
|
||||
* fiber that saw the step begin.
|
||||
* cursor, then rely on the firehose for everything after. When no cursor
|
||||
* survived, replay starts at the session's construction boundary
|
||||
* (`firstLiveSeq`), not seq 0: constructor seeds never publish on the
|
||||
* firehose, and their content already left the process under another
|
||||
* identity — the same id in a previous process (resume) or the parent's
|
||||
* stream (fork, stitched by receivers via `session.seed_length`). Events
|
||||
* at or below the start still feed the projection state (first-chunk
|
||||
* tracking) without being re-handed, so a resumed fiber drops mid-step
|
||||
* chunk continuations exactly like the fiber that saw the step begin. The
|
||||
* cost, accepted with the seam's at-most-once stance: a resume no longer
|
||||
* backfills records a previous process failed to deliver.
|
||||
* @param session - the live session to adopt; a second adoption is a no-op.
|
||||
*/
|
||||
private adopt(session: Session): void {
|
||||
if (this.adopted.has(session)) return
|
||||
this.adopted.add(session)
|
||||
const cursor = handoffCursor.get(session) ?? -1
|
||||
const cursor = handoffCursor.get(session) ?? session.firstLiveSeq - 1
|
||||
// Containment is PER EVENT, matching the firehose: one rejected record
|
||||
// is withheld fail-closed while the rest of the historical replay
|
||||
// proceeds — wrapping the whole loop would let a single failure silently
|
||||
@@ -264,8 +270,11 @@ function identityOf(session: Session, event: SessionEvent): Record<string, strin
|
||||
'event.type': event.type,
|
||||
'event.seq': event.seq,
|
||||
}
|
||||
const { cwd, parentSession } = session.header
|
||||
const { cwd, parentSession, seedLength } = session.header
|
||||
if (cwd !== undefined) attributes['session.cwd'] = cwd
|
||||
if (parentSession !== undefined) attributes['session.parent_id'] = String(parentSession)
|
||||
// The durable fork boundary: a forked stream starts here, and its prefix
|
||||
// lives in the parent's stream — receivers stitch on (parent_id, seed_length).
|
||||
if (seedLength !== undefined) attributes['session.seed_length'] = seedLength
|
||||
return attributes
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user