fix(telemetry): emit the shutdown marker at the session's own disposal edge
Review finding (Codex P1), pinned red-first: the marker was tied to telemetry-plugin lifetime, but receivers key crash detection on its absence per session. A normally closed session in a long-running host retired silently (classified as a crash once stale), while a telemetry reload marked every still-live session as cleanly ended. The session/disposed handler now emits the marker at the session's own termination edge before retiring it; the dispose-time sweep only marks sessions still alive at application teardown (their own edge would fire unobserved). READMEs restate the marker semantics: telemetry stopped observing cleanly — a marker followed by more session events is a telemetry reload, not a session restart.
This commit is contained in:
@@ -35,19 +35,20 @@ const handoffCursor = new WeakMap<Session, number>()
|
||||
* Registers the persistence-coordinator listener set plus the `agent/error`
|
||||
* relay, all through `ctx.effect()`/`ctx.on()` on the composing fiber, and
|
||||
* sweeps already-live sessions (a hot reload does not replay
|
||||
* `session/created`). A `session/disposed` retires the session from the
|
||||
* adopted set — a long-lived backend must not retain closed sessions (and
|
||||
* their frozen event logs) or stamp dispose-time markers for sessions that
|
||||
* already ended. Disposal emits each still-adopted session's `shutdown`
|
||||
* operational record and then awaits the backend's `shutdown()`; a failure
|
||||
* there warns instead of throwing — best-effort reporting must not fail
|
||||
* application teardown.
|
||||
* `session/created`). A `session/disposed` emits the session's `shutdown`
|
||||
* operational record — the marker rides the session's own termination edge,
|
||||
* where receivers key crash detection — and retires it from the adopted set,
|
||||
* so a long-lived backend neither retains closed sessions (and their frozen
|
||||
* event logs) nor re-marks them at unload. Disposal marks the sessions still
|
||||
* alive at teardown (their own edge would fire unobserved) and then awaits
|
||||
* the backend's `shutdown()`; a failure there warns instead of throwing —
|
||||
* best-effort reporting must not fail application teardown.
|
||||
*/
|
||||
export class TelemetryCoordinator {
|
||||
/**
|
||||
* Sessions adopted by THIS fiber and still live, for dispose-time
|
||||
* `shutdown` records and double-adoption protection; `session/disposed`
|
||||
* retires entries.
|
||||
* Sessions adopted by THIS fiber and still live, for double-adoption
|
||||
* protection and the teardown sweep of unmarked sessions;
|
||||
* `session/disposed` marks and retires entries.
|
||||
*/
|
||||
private readonly adopted = new Set<Session>()
|
||||
/** Per session, the `turn:step` keys whose first chunk already shipped; rebuilt from the log on re-adoption. */
|
||||
@@ -64,10 +65,17 @@ export class TelemetryCoordinator {
|
||||
ctx.on('session/created', (session) => {
|
||||
this.adopt(session)
|
||||
})
|
||||
// Retirement is observe-only: the projection/cursor WeakMaps die with the
|
||||
// Session object; only the strong adopted set needs the explicit release.
|
||||
// The session's own termination edge: emit the shutdown marker HERE —
|
||||
// receivers classify a session with activity and no marker as crashed,
|
||||
// so a normally closed session in a long-running host must get its
|
||||
// marker at disposal, not never. Then retire: the projection/cursor
|
||||
// WeakMaps die with the Session object; only the strong adopted set
|
||||
// needs the explicit release.
|
||||
ctx.on('session/disposed', (session) => {
|
||||
this.adopted.delete(session)
|
||||
this.contain(() => {
|
||||
if (!this.adopted.delete(session)) return
|
||||
this.handOff(shutdownRecord(session))
|
||||
})
|
||||
})
|
||||
ctx.on('session/event', (session, event) => {
|
||||
this.contain(() => {
|
||||
@@ -87,6 +95,10 @@ export class TelemetryCoordinator {
|
||||
})
|
||||
})
|
||||
ctx.effect(() => async () => {
|
||||
// Sessions still adopted here are alive through a whole-application
|
||||
// teardown (their own disposal edge will fire after telemetry is gone,
|
||||
// unobserved) — mark them now so the receiver sees a clean stop of
|
||||
// observation rather than a crash-shaped silence.
|
||||
for (const session of this.adopted) {
|
||||
this.contain(() => {
|
||||
this.handOff(shutdownRecord(session))
|
||||
@@ -214,7 +226,10 @@ export class TelemetryCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/** Build the per-session clean-exit marker emitted at dispose, before the backend's `shutdown()`. */
|
||||
/**
|
||||
* Build the per-session clean-exit marker: emitted at the session's own
|
||||
* disposal edge, or at coordinator dispose for sessions still alive then.
|
||||
*/
|
||||
function shutdownRecord(session: Session): TelemetryRecord {
|
||||
return {
|
||||
channel: 'ops',
|
||||
|
||||
Reference in New Issue
Block a user