# Telemetry wire contract

Agent `waiting` is an explicit projection of a pending supervisor ask, read through the live
engine steer handle. Ask delivery and settlement (reply, timeout or cancellation) refresh the
projection; overlapping asks keep it waiting until the last one settles. Queued and terminal
statuses retain precedence. The producer deduplicates unchanged projections and never infers
waiting from tool arguments, output text or a quiet clock.

Telemetry is a vendor-neutral v2 envelope. `producerId` and `producerVersion` identify the emitting plugin; `version` identifies the wire schema. `seq` is monotonic only within `(producerId, sessionId)`, and `id` is deduplicated within that same scope.

The pi-persona producer writes semantic projections under `telemetry/v2/<workspace>/<producerId>/`. `<workspace>` always identifies the Pi process's actual cwd; joining another Exocom scope does not rewrite telemetry identity. It never persists task/prompt text, tool arguments/activity, model output, paths, secrets, or message bodies. Known pi-persona event payloads are allowlisted; an unreviewed producer event retains only its envelope/type (payload `{}`) at the producer and consumer boundary. Future producers must add a reviewed allowlist/schema before publishing data intended for a consumer.

JSONL compaction is atomic and hysteretic. It preserves the initial `instance.started` anchor and complete records; the configured byte limit is therefore a soft bound for an anchor plus one complete event. The `.previous` backup is recovered after an interrupted swap.

Producer shutdown flushes its terminal event before releasing its writer lease. It does not join the shared background retention sweep, so unrelated stale sessions cannot delay this shutdown or reload. `flushRetention()` remains the explicit await for maintenance completion; cleanup errors still reach the non-fatal diagnostic sink.

The pi-persona producer also applies a default 30-day age retention to its own valid v2 session groups; `retentionMs: 0` disables cleanup only, not writer leases. Cleanup is asynchronous after successful initialization and can be awaited with the producer's retention test seam. It strictly considers regular single-link logs plus recognized `.previous`/compaction scratch files beneath valid workspace IDs and this producer directory. Legacy data, foreign producers, malformed names, links, and unknown files are retained. Orphan backups/scratch and dead-marker-only groups are considered too; empty owned lease/producer/workspace directories are removed nonrecursively. The cutoff is exclusive and cleanup runs on activation, not on a wall-clock deletion schedule.

Retention and writers coordinate cooperatively on the same host using unique empty `0600` marker files in `<session>.jsonl.leases`. Writers claim before recovery/sequence reads and keep the claim through terminal publication and flush. A pruner claims before checking writers and revalidates the whole old session group before unlinking. Marker names are immutable and unique to avoid stale-lock ABA. PID liveness is conservative: only `ESRCH` proves death; current, live, permission-denied, and unknown states protect data. This protocol cannot protect against historic writers that predate leases, nor non-cooperating processes; it is not an OS-level lock. A pre-upgrade process has no marker: its log is protected only while normal writes/heartbeats keep its mtime newer than the cutoff. That incidental mtime grace is not active-writer protection; let old sessions settle before reloading/upgrading when possible.

Admission fails before recovery, sequence reads or emission when a live prune claim, unknown or unsafe lease marker, linked namespace/artifact, or filesystem error prevents a safe writer claim. The host warns and continues without telemetry for that activation. A failed attempt removes only its own marker and empty directories it created, nonrecursively; pre-existing data and leases remain intact. Cleanup and writer errors reach an always-enabled, non-fatal `onError` sink: a sanitized warning in interactive/RPC sessions, or stderr headless. A failing diagnostic renderer cannot interrupt the session. Retention deletes the sequence source: resuming an expired log starts a new retained stream at sequence 1, so it does not promise continuity with previously exported expired history.

The flow consumer is a compatibility reader for v1 logs, which sit under `persona/flow` on an install the storage-root consolidation has migrated and under `pi-persona/flow` on one that predates it; the consumer reads both roots, because no producer writes v1 any more and dropping either name would silently lose history. It re-implements the parser rather than importing this one, and that duplication is deliberate: a consumer that ran the producer's parser could not validate against a buggy or hostile producer, and a vendor-neutral contract cannot make every future producer depend on pi-persona. What keeps the two honest is a conformance corpus — one byte-identical block of wire cases (accept, reject, and the exact projected payload) carried by `test/unit/telemetry/parity.test.ts` here and `test/parity.test.ts` there. The flow suite enforces that identity, comparing its block against the one in a sibling pi-persona checkout (or `PI_PERSONA_REPO`) and naming the first line that differs; it lives there and not here because flow may depend on pi-persona and never the reverse, and it skips — never fails — when no such checkout resolves, so flow stays testable standalone. A divergence therefore surfaces as a named line rather than as a silently dropped event. `TELEMETRY_PRODUCER_VERSION` is asserted against `package.json` in the same test: a release bumps both or fails. Any future shared package must preserve v1 read compatibility, v2 producer/session scoping, and the corpus.
