Skip to content

Build the telemetry recording foundation - #286

Open
gmemuriuki wants to merge 30 commits into
symposium-dev:mainfrom
gmemuriuki:telemetry/recording-foundation
Open

gmemuriuki wants to merge 30 commits into
symposium-dev:mainfrom
gmemuriuki:telemetry/recording-foundation

Conversation

@gmemuriuki

Copy link
Copy Markdown
Contributor

What does this PR do?

This builds the foundation for the accepted telemetry RFD (#273).

It adds strict, versioned schemas for the first telemetry rows, typed classification of stored lines, scoped HMAC identifiers, and private in-memory identity state. Cohort, window-rollover, and reset transitions are defined without adding persistence or production event emission.

It also defines the initial package-resolution vocabulary and clarifies that a future uploader will read validated records from closed days.

Verification

  • 140 telemetry tests pass.
  • Clippy passes with warnings denied.
  • The mdBook builds with two existing missing-example warnings.
Disclosure questions

AI disclosure.

  • The AI tool authored large parts of the code.

Questions for reviewers.

  • Does this provide the right boundary for the storage work that follows?

gmemuriuki and others added 29 commits September 11, 2026 20:44
Co-authored-by: Codex <codex@openai.com>
Make room for new telemetry pieces without changing behavior.
Use UUIDs so malformed identifiers are rejected when data is read.
Validate stored dates and versions and keep timestamps to whole seconds.
Add shared row metadata and reject schema drift in version 1.
Classify recognized but invalid rows separately from malformed input.
Give each scoped telemetry identifier a fixed prefix and canonical
128-bit hexadecimal representation. Parse the same representation
strictly and use it for JSON serialization.

Keep identifier domains separate at the type level, and check the
published examples so code and the telemetry contract cannot drift.
Separate identifier windows from domain-scoped dimensions so callers
cannot exchange them or mix identifiers from different domains.

Length-prefix each input field and pin the HMAC domain constants in the
recording contract. This keeps component boundaries unambiguous and
makes accidental identifier rotation visible in tests.
Create each 256-bit identity key from the operating system random
source and return failures to the caller.

Keep generation independently testable so the complete buffer and error
path are covered without relying on probabilistic assertions.
Keep identity material in strict, versioned TOML before adding file
persistence.

Share lowercase hexadecimal encoding with scoped identifiers so keys and
identifiers follow the same rules. Reject unknown fields, future versions,
malformed keys, and noncanonical anchor dates.

Pin storage-limit JSON field order in its contract test.
Generate fresh state from one UTC day while leaving the return-cohort
anchor absent until a session is observed. Use a deterministic constructor
to pin the exact initial TOML in tests.

Keep identity resets monotonic with the latest-opened-day high-water mark
and document that rule consistently across the RFD.
Represent cohort days as a bounded D0-D30 value and advance the
private return-cohort anchor as sessions are observed.

Persist state before recording D0 so partial failures keep identifiers
stable and undercount returns.
Roll identifier windows over on the first observation at day 30 or
later, while keeping the identity key stable.

Select identifier and return-cohort anchors before mutating private
state, so storage can persist one coherent session transition.
Rotate the identity key, advance the window anchor, and clear the
return cohort as one state replacement.

Generate the new key before mutation so failures leave the previous
state intact. Document reset's clock-clamping rules and future state
obligations.
Make room for focused state submodules while preserving the existing
implementation unchanged.

The telemetry parent continues to load state through the same private
module boundary.
Keep the serialized private-state model in the module root and move
identifier reset, window, and cohort transitions into lifecycle.rs.

Leave each responsibility's tests beside its implementation and record
the boundary in the module structure guide.
Prepare the schema for event-family modules without changing its
behavior.

Adjust the contract fixture path for the file's new directory depth.
Use closed enums for hook agents, platforms, and session starts.

Pin serialized names to the version-one contract and reject unknown values.
Define the agent, platform, and start values recorded for a session.

Read session-start rows through the versioned classifier and test them
against the JSONL contract. Document that platform values describe the
binary's compilation target rather than its physical host.
Keep the seven-agent configuration list separate from the smaller hook
agent list.

Convert every project agent explicitly so future agent support requires
a deliberate telemetry update. Pin each version-one serialized name,
including the opencode spelling.
Add the version-one row for one entry in the daily agent
configuration snapshot.

Keep fixed fields inside the constructor and test every value without
wiring storage or production recording yet.
Recognize version-one agent configuration rows through the shared
envelope classifier.

Keep future versions separate from invalid rows and pin serialization to
the JSONL contract example.
Link hook agents to the broader supported-agent vocabulary so rows that
refer to the same agent cannot drift apart.

Group per-agent configuration values at construction sites, making the
configured flag explicit and preparing for the daily batch builder.
Add the fixed triggers, outcomes, and unnamed-package reasons used by
resolution summaries.

Keep reason counters private, increment them through checked operations,
and align dropped resolution batches with storage-limit reporting.
Build resolution summary rows from their reason counters so the stored
unnamed-package total cannot drift from its breakdown.

Apply the same validation when reading JSON, and reject overflow or
missing fields before invalid data reaches analysis.
Teach the shared JSONL reader to recognize version one resolution
summaries and serialize them through TelemetryRow.

Keep future versions distinct from invalid supported rows, including
summaries with unknown fields or inconsistent package counts.
Add the closed version one package ecosystem and extension match
values before introducing the package resolution row.

Pin their JSON names and reject unknown values so later schema changes
remain explicit.
@gmemuriuki
gmemuriuki force-pushed the telemetry/recording-foundation branch from 9bca222 to 7b84afb Compare September 11, 2026 18:36
Comment thread src/telemetry/schema/agent.rs
A session start's day is derived from its completion timestamp. Reject
stored rows where those values disagree instead of admitting inconsistent
data to typed readers.

Keep file membership checks with the archive reader, which has the daily
file context needed for that separate invariant.
@Fluzko

Fluzko commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

This looks pretty nice, I've made my agents play around with this solution & the #287 one.
A few minor concerns that might be worth looking:

The identity API here gets reworked in #287, so thing like IdentityDeriver, ScopedDimension and IdentityWindow are gone there, and get replaced by other entities. They're only ever exercised by #286's own tests in the meantime. Not asking to change it, but the diff would look nicer and easier to review both PRs in one shot.

ExtensionResolutionV1::new takes a path, but there's no way to build one from outside When you'll write the producer you'll face this on day 1.

telemetry-state.toml it's locked to version 1 and rejects unknown fields, but the contract says v1 also carries the high-water mark, marker metadata and the session sets, and those aren't in the struct yet. When storage adds them it'll have to either call it v2 and migrate a format nobody ever had on disk, or make them optional. Fine while nothing has shipped, I'd just avoid cutting a release with this file format before storage 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.

3 participants