Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

What Symposium records

Symposium records telemetry only after you opt in. The records stay on your machine; nothing described on this page is uploaded. cargo agents telemetry show displays the stored bytes, and cargo agents telemetry clear deletes event and aggregate-metric files. The never-record list summarizes the exclusions.

This page is the complete producer contract for consent version 1. If a field is not listed here, Symposium does not write it as telemetry.

Common fields

Every JSONL row has:

FieldExampleMeaning
v1Schema version for this row kind.
kindsession_startRow kind from the list below.
event_id9f2c41b6-...Identifier for this row; random when minted.
day2026-08-03UTC calendar day.
symposium0.4.0Symposium version that wrote the event.

Completed operational events (session_start and command) also have at, an RFC3339 UTC timestamp truncated to one second. Their day is the UTC calendar day containing at; a mismatch makes the row invalid. Resolution, configuration, and aggregate metric rows have only day.

Counters and durations are non-negative JSON integers that fit an unsigned 64-bit value. Symposium checks arithmetic and drops an overflowing batch or observation instead of wrapping the value.

Durations are converted to whole milliseconds by truncating any sub-millisecond remainder. A duration above the unsigned 64-bit millisecond range is recorded as the maximum value instead of wrapping.

event_id exists to deduplicate a future retry of the same event or identify one cumulative metric row. A metric row keeps the event_id minted when its dimension first appears that UTC day as the row is rewritten. It is not an installation, session, account, or project identifier.

Example JSONL for every row kind

These are independent row-shape examples, not one coherent operation or batch. The identifiers and coordinates are illustrative. An actual day may contain repeated low-volume events and one cumulative row per aggregate-metric dimension.

{"v":1,"kind":"session_start","event_id":"9f2c41b6-495e-4c88-a22b-c597f8102aed","day":"2026-08-03","at":"2026-08-03T09:14:02Z","symposium":"0.4.0","agent":"claude","os":"linux","arch":"x86_64","start":"fresh","session_id":"sess_31d8b1916028f65a0c0521dc1f4c86fb","retention_subject":"ret_74ddf26f80ad8b58de7f03e6c632e654","cohort_day":0}
{"v":1,"kind":"agent_configuration","event_id":"1a77f7c8-2c45-4de2-8cc6-b507ac3605f8","day":"2026-08-03","symposium":"0.4.0","agent":"claude","configured":true,"os":"linux","arch":"x86_64","agent_subject":"agt_9255770e1679cb789796a9f9e86325c5"}
{"v":1,"kind":"resolution_summary","event_id":"6c3ef7a1-f782-4b47-9a25-829dd64e1ba2","day":"2026-08-03","symposium":"0.4.0","trigger":"session_start","outcome":"ok","duration_ms":142,"public_packages":2,"unnamed_packages":1,"unnamed_package_reasons":{"private_registry":1,"git":0,"path":0,"workspace":0,"unknown_source":0,"invalid_coordinate":0},"plugins":1,"skills":1,"installed":2,"updated":0,"reaped":0,"session_id":"sess_31d8b1916028f65a0c0521dc1f4c86fb"}
{"v":1,"kind":"package_resolution","event_id":"c03e7390-52b1-4e11-b7e7-9573c84e555a","day":"2026-08-03","symposium":"0.4.0","package":{"ecosystem":"cargo","name":"example-runtime","version":"1.2.3"},"extension_match":"public","package_subject":"pkg_f6db813c87209816ae4896f3e60dd774"}
{"v":1,"kind":"extension_resolution","event_id":"fa21a1d7-64a7-4e3e-9f6a-4572cf8f1939","day":"2026-08-03","symposium":"0.4.0","target":{"type":"skill","source":"symposium-recommendations","name":"example-debugging"},"path":[{"type":"package","ecosystem":"cargo","name":"example-runtime","version":"1.2.3"},{"type":"extension","extension_type":"plugin","source":"symposium-recommendations","name":"example-tools"},{"type":"extension","extension_type":"skill","source":"symposium-recommendations","name":"example-debugging"}],"extension_subject":"ext_6e68a75b9701bbad86cf32cd876e994a"}
{"v":1,"kind":"hook_metrics","event_id":"b563dd02-0301-4e2c-aac4-2e0d5dfaa977","day":"2026-08-03","symposium":"0.4.0","agent":"claude","hook":"pre_tool_use","invocations":500,"outcomes":{"ok":498,"blocked":1,"plugin_error":1,"internal_error":0},"plugins_attempted":500,"plugins_completed":500,"duration_ms":{"bounds":[5,10,25,50,100,250,500,1000],"counts":[40,80,180,130,55,12,2,1,0]},"identified_sessions":1,"identified_sessions_non_ok":1,"session_counts_complete":true,"hook_subject":"hok_51f4f4143ce32704e96086899dc66a27"}
{"v":1,"kind":"plugin_hook_metrics","event_id":"70dcad2a-fd19-4b5d-97cb-a7c7b52a81f1","day":"2026-08-03","symposium":"0.4.0","agent":"claude","hook":"pre_tool_use","plugin_scope":"public","plugin":{"source":"symposium-recommendations","name":"example-tools"},"attempts":500,"executions":500,"outcomes":{"ok":499,"blocked":0,"error":1},"prepare_ms":{"bounds":[5,10,25,50,100,250,500,1000],"counts":[400,90,10,0,0,0,0,0,0]},"execute_ms":{"bounds":[5,10,25,50,100,250,500,1000],"counts":[10,50,200,180,50,8,2,0,0]},"identified_sessions":1,"identified_sessions_non_ok":1,"session_counts_complete":true,"plugin_subject":"plg_d42e3f1fbac5bb001e1258a1e70b0d77"}
{"v":1,"kind":"command","event_id":"5d18caa8-84f7-4aa3-846c-99ea810ccd85","day":"2026-08-03","at":"2026-08-03T10:02:11Z","symposium":"0.4.0","command":{"type":"builtin","name":"use"},"duration_ms":820,"outcome":"ok","command_subject":"cmd_adf0c14ddc35b97762b5daae6f4119ce"}
{"v":1,"kind":"storage_limit","event_id":"8430f7f3-7ec5-4ca5-9c65-8f6e83eaa3de","day":"2026-08-03","symposium":"0.4.0","dropped_operation":"manual_sync"}
{"v":1,"kind":"extension_invocation_metrics","event_id":"7b3bda33-1fd7-4657-9637-4057268370dc","day":"2026-08-03","symposium":"0.4.0","agent":"claude","target_scope":"public","target":{"type":"skill","source":"symposium-recommendations","name":"example-debugging"},"attempted":14,"completed":12,"failed":2,"session_counts_complete":true,"identified_sessions":4,"identified_sessions_completed":3,"extension_subject":"ext_6e68a75b9701bbad86cf32cd876e994a"}

Scoped identifiers

Key and rotation

When an enabled recorder first needs identity state, Symposium stores a random secret key in private <config-dir>/telemetry-state.toml (default ~/.symposium/telemetry-state.toml). The same state holds the current identifier-window anchor and, after the first observed session, the return-cohort anchor. Every recorder reads it under the telemetry lock, so identical domain, window, and dimension inputs produce the same subject across processes and restarts.

If private state is absent while daily files survive, Symposium initializes the latest-opened-day high-water mark and identifier-window anchor to the later of the current UTC day and the newest day named by a canonical events-YYYY-MM-DD.jsonl or metrics-YYYY-MM-DD.jsonl file. Other names are ignored. Malformed state and a syntactically valid unsupported state version are not treated as absence and are never replaced by this initialization path.

An identifier window includes its anchor day as day 0 and remains active through day 29. The first recording-capable observation on day 30 or later starts a new window anchored to that observation. This normal rollover changes the window input without replacing the key. Renewed consent or telemetry reset-identifiers replaces the key, sets the identifier-window anchor to the later of the current UTC day and the latest-opened-day high-water mark, and clears the return-cohort anchor. The next observed session starts a new cohort at D0. telemetry disable and telemetry clear preserve the key and whichever anchors exist.

Identifier-window age uses that same later day. An anchor later than the wall-clock day is valid after clock rollback and is not malformed for that reason alone.

This file is separate from the inspectable <config-dir>/telemetry/ data directory and has owner-only permissions where the platform supports them. The key is private state, not anonymized telemetry. It is not written into events, printed by telemetry commands, or derived from your machine. Someone who has the key can recompute candidate identifiers.

The implementation prevents accidental formatting or serialization of the key. It does not promise to scrub every in-memory copy: the security boundary is the private state file and keeping the key out of telemetry and diagnostics.

Derivation format

Version 1 uses the first 128 bits of HMAC-SHA-256. Each variable-length window or dimension value has an eight-byte unsigned big-endian byte length followed by its bytes:

frame(value) = u64_be(byte_length(value)) || value

HMAC(
    key,
    "telemetry:<domain>:v1\0"
    || frame(window)
    || frame(dimension field 1)
    || frame(dimension field 2)
    || ...
)

The window is the anchor’s ASCII YYYY-MM-DD representation from telemetry-state.toml. Dimension fields use the exact UTF-8 bytes of their stable labels and validated strings, without case folding or Unicode normalization. Length framing keeps field boundaries unambiguous even when a value contains a NUL byte. Domains with no dimension fields end after the framed window.

The domain strings, wire prefixes, and ordered dimension fields are frozen for consent version 1:

IdentifierHMAC domainWire prefixWindow anchorOrdered dimension fields
session_idsession_idsess_identifier-windowAgent, vendor session id.
retention_subjectretention_subjectret_return-cohortNone.
agent_subjectagent_subjectagt_identifier-windowAgent.
package_subjectpackage_subjectpkg_identifier-windowPackage ecosystem, name, exact version.
extension_subjectextension_subjectext_identifier-windowTarget type, source, name, then the complete safe resolution path.
hook_subjecthook_subjecthok_identifier-windowAgent, hook surface.
plugin_subjectplugin_subjectplg_identifier-windowPublic source, plugin name.
command_subjectcommand_subjectcmd_identifier-windowCommand type, then its typed coordinate fields in event order.

Structured values such as an extension path use the same framing recursively. A sequence starts with its eight-byte unsigned big-endian item count. Each variant starts with its framed type label, followed by its fields in the order used by the corresponding event schema. Identity code owns this encoding; telemetry producers pass typed coordinates rather than concatenating strings.

Symposium derives each identifier for one narrow purpose:

IdentifierWhat it can linkRotation
session_idEvents carrying the same vendor session id for one agent30 days; omitted when unavailable
retention_subjectObserved session days in one D0-D30 cohortAfter cohort day 30
agent_subjectRepeated configuration observations for one agent30 days
package_subjectRepeated observations of one public package/version30 days
extension_subjectResolution and public invocation aggregates for one safe path30 days
hook_subjectDaily hook aggregates for one agent and hook surface30 days
plugin_subjectDaily hook aggregates for one eligible public plugin30 days
command_subjectRepeated use of one eligible command30 days

These values are pseudonymous, not anonymous: they deliberately permit limited linking inside the stated scope. retention_subject can link observed sessions across agents for D0-D30; this is the single exception needed for return measurement. No identifier links that cohort, a package subject, and a command subject, and there is no workspace id.

All lines in your local telemetry directory still come from your Symposium home. File order and same-day events can therefore suggest which observations happened together.

Version 1 public identity allowlists

Only the following stable labels can make package, plugin, skill, or plugin-command names eligible for recording under consent version 1:

EnumVersion 1 valuesUsed by
Package ecosystemcargopackage.ecosystem and package path nodes.
Public extension sourcesymposium-recommendations, crates-ioResolution/invocation target.source, extension path nodes, plugin.source, and plugin-command source.

crates-io applies only when core proves allowlisted crates.io provenance; symposium-recommendations identifies the built-in registry. user-plugins, configured registries, paths, workspaces, and arbitrary git sources remain unnamed. Raw registry names and URLs are never enum values. Adding an ecosystem or public-source label expands name eligibility and requires a new consent version.

Public plugin and skill names use a fixed version 1 grammar: 1 through 64 ASCII bytes, beginning with an ASCII letter or digit, followed by ASCII letters, digits, -, or _. Unlike public package names, extension names may begin with a digit because they are authored extension identifiers rather than crates.io package coordinates. Symposium preserves the spelling without case folding or Unicode normalization. An otherwise valid extension whose name does not fit this telemetry grammar remains usable but is treated as unnamed by telemetry.

Event kinds

session_start

This row records a completed registered Symposium session-start hook.

FieldValuesMeaning
atUTC secondWhen Symposium completed the session-start handling.
agentantigravity, claude, codex, copilot, kiroAgent that invoked the registered hook.
oslinux, macos, windows, otherOS class for the running Symposium build.
archx86_64, aarch64, otherArchitecture class for the running build.
startfresh, resumed, unknownAgent-supplied lifecycle classification when available.
session_idscoped id, optionalOmitted when the agent supplies no session id.
retention_subjectscoped idDeduplicates this D0-D30 observed-session cohort.
cohort_dayinteger 0 through 30UTC days since the cohort’s first observed session.

GitHub Copilot does not currently supply a session id. OpenCode and Goose do not currently call Symposium through a registered session-start hook, so they do not produce this event.

These rows, not hook_metrics rows whose hook is session_start, are authoritative for observed-session and return measurements. A stored D0 row admits its retention_subject cohort to analysis. D1, D7, or D30 is present when at least one later session-start row has that cohort_day, regardless of agent or vendor session id. A later row without a stored D0 for the same subject is ignored. Multiple rows on the same cohort day count once.

The producer captures at once when session-start handling completes. That timestamp determines day and the session observation from which the producer derives session_id, retention_subject, and cohort_day.

The aggregate hook rows measure only session-start hook reliability and latency.

agent_configuration

This row records whether a supported agent is configured for Symposium that day.

FieldValuesMeaning
agentantigravity, claude, codex, copilot, kiro, opencode, gooseAgent being checked.
configuredbooleanWhether the agent is listed in per-user Symposium configuration.
oslinux, macos, windows, otherOS class for the running Symposium build.
archx86_64, aarch64, otherArchitecture class for the running build.
agent_subjectscoped idDeduplicates one installation for this agent and window.

On the first non-telemetry recording-capable invocation each UTC day, Symposium attempts one whole batch containing all seven agents. configured means the agent is present in Symposium’s per-user configuration at that moment; Symposium does not inspect agent-owned configuration files.

A successful batch is the day’s snapshot, so later configuration changes appear on the next UTC day’s snapshot. If locking, storage, or I/O drops the batch, it remains outstanding and a later eligible invocation retries it.

configured: true does not claim that an agent session ran or that the agent-owned integration files remain intact.

resolution_summary

This row records the result of a full sync after session start, manual sync, use, or removal. Package, plugin, and skill counts are distinct coordinates within the sync, not duplicate declarations.

FieldValuesMeaning
triggersession_start, manual_sync, use, removeWhy full sync ran.
outcomeok, partial, errorClosed result classification.
duration_msintegerFull resolution/sync duration.
public_packagesintegerEligible public resolution-input packages.
unnamed_packagesintegerPrivate, local, unknown, invalid, or otherwise ineligible inputs.
unnamed_package_reasonsfixed countersMutually exclusive reasons that sum to unnamed_packages.
pluginsintegerPlugins in the final resolved set.
skillsintegerSkills in the final resolved set.
installedintegerArtifacts newly installed by this sync.
updatedintegerExisting artifacts changed by this sync.
reapedintegerObsolete artifacts removed by this sync.
session_idscoped id, optionalPresent only for a sync inside an identified agent session.

unnamed_package_reasons has exactly private_registry, git, path, workspace, unknown_source, and invalid_coordinate counters. Each unnamed package increments exactly one. Source provenance takes precedence; invalid_coordinate is used only for an allowlisted public source with a malformed name or a missing, wildcard, or invalid exact version. The counters sum to unnamed_packages.

When a resolution runs inside an identified agent session, the producer derives session_id from that agent and its raw vendor session identifier using the same bound recording context that supplies the row’s day. Producers never pass an already-derived session identifier into the row constructor.

Read-only extension lookup on ordinary hook calls does not produce this event.

package_resolution

This row records one eligible public package used as resolution input during a full sync.

FieldValuesMeaning
package.ecosystemcargoStable public ecosystem label.
package.namevalidated stringPublic package name using the fixed version 1 grammar.
package.versionvalidated exact versionExact resolved public version, never a range or *.
extension_matchpublic, unnamed_only, noneWhat kind of resolved extension, if any, the package contributed to.
package_subjectscoped idDeduplicates this exact coordinate for 30 days.

A package is named only when its package manager reports provenance matching a reviewed public-registry allowlist. Registry URLs themselves are not recorded.

The version 1 package-name grammar is 1 through 64 ASCII bytes. The first byte is an ASCII letter; the remaining bytes are ASCII letters, digits, -, or _. Symposium preserves the spelling without case folding or treating hyphens and underscores as equivalent. This stable telemetry grammar does not copy a registry’s changing reserved-name list.

An exact package version has three numeric semantic-version components and may include prerelease or build metadata. Missing versions, ranges, and wildcards are invalid coordinates.

extension_match describes what the package contributed:

  • public: at least one eligible public extension matched, including when unnamed content also matched.
  • unnamed_only: extension content matched, but none was eligible to name.
  • none: no resolved extension matched.

Events measure packages independently. Symposium does not record a workspace/resolution id, but the summary and contiguous relationship batch can still make the public package set for one sync recoverable, especially when only one sync occurred that day. Review the whole file before sharing it.

extension_resolution

This row records one public plugin or skill and one safe path that selected it.

FieldValuesMeaning
target.typeplugin, skillResolved extension type.
target.sourcesymposium-recommendations, crates-ioStable label, never a configured URL or local name.
target.namepublic extension nameName defined by eligible public content.
pathbounded typed nodesActual safe package/predicate/extension chain.
extension_subjectscoped idDeduplicates this safe target/path for 30 days.

Path nodes are limited to:

Node typeRecorded content
packageEligible public ecosystem, name, and exact version.
extensionEligible public plugin/skill source and name.
allAll safe children that contributed to success.
anyThe branch that actually made the expression succeed.
notMarker only; the child is not recorded.
opaqueFixed reason: private_source, non_package_predicate, or limit.

The exact version 1 node shapes are shown below. These examples use public placeholder coordinates; the same package and extension validation rules described above apply inside path nodes.

{"type":"package","ecosystem":"cargo","name":"example-runtime","version":"1.2.3"}
{"type":"extension","extension_type":"skill","source":"symposium-recommendations","name":"example-debugging"}
{"type":"all","children":[{"type":"package","ecosystem":"cargo","name":"example-runtime","version":"1.2.3"},{"type":"any","child":{"type":"extension","extension_type":"skill","source":"symposium-recommendations","name":"example-debugging"}},{"type":"not"},{"type":"opaque","reason":"limit"}]}
{"type":"any","child":{"type":"not"}}
{"type":"not"}
{"type":"opaque","reason":"non_package_predicate"}

all.children contains one or more nodes. any.child contains the one branch that made the expression succeed. Each object is strict: missing fields, additional fields, unknown node types, and unknown opaque reasons are invalid.

Shell commands, paths, environment variables, custom predicate names or arguments, and private package or extension names never enter a path. An opaque marker can represent their position.

The path array is non-empty, and each top-level node begins at depth 1. Depth counts nested evidence nodes through a terminal package, extension, not, or opaque node. A subtree that would exceed depth 8 becomes opaque: limit. The complete path is limited to 16 evidence leaves. Its 4 KiB bound is the byte length of the compact UTF-8 JSON encoding of the complete path array, excluding the surrounding event row. Evidence depth does not count filesystem components; filesystem paths are never recorded.

This event says an extension resolved. It does not say that an agent read or used the extension; a matching extension_invocation_metrics aggregate separately reports observed agent activation. Version 1 can produce that aggregate only for Claude.

hook_metrics

This cumulative row combines completed hook observations for one UTC day, agent, hook surface, and active identifier epoch. An epoch normally lasts 30 days but ends early after identifier reset or renewed consent. A reset can therefore leave two rows for the same agent, hook, and day with different hook_subject values. Symposium updates the current row instead of appending one telemetry row per invocation.

FieldValuesMeaning
agentantigravity, claude, codex, copilot, kiroInvoking agent.
hookpre_tool_use, post_tool_use, user_prompt_submit, session_start, stopSymposium hook surface.
invocationsintegerCompleted hook observations merged into the row.
outcomeshook outcome countersExact counters named ok, blocked, plugin_error, and internal_error.
plugins_attemptedintegerPlugin hooks whose preparation began.
plugins_completedintegerPlugin hooks with an observed terminal result.
duration_mslatency histogramParsed-input-to-response-ready latency; telemetry update time is excluded.
identified_sessionsinteger, optionalDistinct identified sessions represented; present only when session_counts_complete=true.
identified_sessions_non_okinteger, optionalThose sessions with at least one non-ok observation; present only when counts are complete.
session_counts_completebooleanWhether the two distinct identified-session counts are complete for every observation.
hook_subjectscoped idLinks this agent/hook dimension inside its 30-day identifier window.

The row is written only after its first completed observation, so invocations is greater than zero. outcomes and the duration histogram each sum to invocations. Exactly one outcome counter advances for each completed observation. Precedence is internal_error, then blocked, then plugin_error, then ok.

Within each observation, and therefore across the cumulative row, plugins_completed cannot exceed plugins_attempted.

Session counts remain complete only when every contributing observation supplies a session id and neither set exceeds 256 distinct ids for the row. When complete, identified_sessions is at least 1 and cannot exceed invocations. identified_sessions_non_ok is at least 1 when outcomes.ok < invocations, and cannot exceed either identified_sessions or invocations - outcomes.ok. The remaining all-ok sessions cannot exceed outcomes.ok. On the first missing id or overflow, Symposium discards both sets, writes session_counts_complete: false, and omits both counts for the rest of that day. Raw and keyed session ids are never written into the aggregate file.

plugin_hook_metrics

This cumulative row combines plugin-hook observations for one UTC day, agent, hook surface, active identifier epoch, and bounded plugin bucket. A reset can leave otherwise identical unnamed or overflow rows distinguished only by event_id; public rows also receive a new plugin_subject.

FieldValuesMeaning
agentantigravity, claude, codex, copilot, kiroInvoking agent.
hookpre_tool_use, post_tool_use, user_prompt_submit, session_start, stopSymposium hook surface.
plugin_scopepublic, unnamed, overflowWhether the bucket names an eligible public plugin.
plugin.sourcesymposium-recommendations, crates-io, conditionalPresent only when plugin_scope=public.
plugin.namepublic extension name, conditionalPresent only when plugin_scope=public.
attemptsintegerPlugin-hook attempts that reached an observed terminal result.
executionsintegerThose completed attempts that reached child execution.
outcomesplugin outcome countersExact counters named ok, blocked, and error.
prepare_mslatency histogramPreparation time for every attempt.
execute_mslatency histogramChild execution time for attempts counted by executions.
identified_sessionsinteger, optionalDistinct identified sessions represented; present only when session_counts_complete=true.
identified_sessions_non_okinteger, optionalThose sessions with at least one non-ok attempt; present only when counts are complete.
session_counts_completebooleanWhether the two distinct identified-session counts are complete for every attempt.
plugin_subjectscoped id, conditionalPresent only with an eligible public plugin.

Counting rules

attempts counts terminal results. The row exists only after its first terminal result, so attempts is greater than zero. executions counts the subset that started child execution. A preparation failure is terminal error with no execution. Every attempt that does not execute has an error outcome, so attempts - executions cannot exceed outcomes.error. outcomes and prepare_ms each sum to attempts; execute_ms sums to executions. Across every epoch and bucket for one agent, hook, and day, plugin attempts sum to the corresponding top-level plugins_completed.

blocked records exit code 2 or a block decision in successfully parsed output. error records input serialization, preparation, spawn, wait, or output-parsing failure; termination by signal; or any other non-zero exit. When both signals apply, error takes precedence over blocked. ok is every other completed attempt. The same 256-id all-or-nothing session rule used by hook_metrics applies, with attempts in place of invocations and outcomes.ok as the ok observation count.

Plugin identity and row limits

Eligible public plugins use {source, name} coordinates from the reviewed allowlist. Private, local, invalid, or otherwise ineligible plugins merge into one unnamed row per agent, hook, epoch, and day and expose no identity.

At most 128 named public-plugin rows may appear across all agents, hooks, and identifier epochs in one UTC day. Attempts for later rows merge into overflow rows per agent, hook, and epoch and expose no identity. This daily allowance survives identifier reset and telemetry disable. UTC-day rollover starts a new allowance; telemetry clear resets it because clear deletes the snapshot rows it accounts for.

Named rows are first-observed, not sampled, so earlier-in-day public plugins are overrepresented when the limit is reached. Analysis must report overflow and must not treat the named subset as random.

Rules shared by hook aggregates

A latency histogram is exactly:

{"bounds":[5,10,25,50,100,250,500,1000],"counts":[0,0,0,0,0,0,0,0,0]}

The nine non-overlapping millisecond buckets mean <=5, (5,10], (10,25], (25,50], (50,100], (100,250], (250,500], (500,1000], and >1000. Bounds and count-array length cannot vary. Fixed histograms can be merged correctly across installations; locally calculated percentiles cannot.

Neither aggregate kind has at, an invocation id, a raw or scoped session id, or an individual invocation’s time or outcome. No hook input, output, stdout, stderr, error text, or numeric exit detail is recorded. Symposium adds no timeout as part of telemetry.

A host termination, lock conflict, storage limit, or I/O failure can omit an observation, so aggregate counts are lower bounds. No durable dropped-update counter can cover every loss mode because lock contention, termination, and I/O failure can also prevent writing that counter. Counting only cap rejections would imply false completeness.

These rows do reveal exact daily counts for each hook surface. In particular, pre_tool_use, post_tool_use, and user_prompt_submit counts are proxies for daily tool and prompt activity even though no individual activity record exists.

extension_invocation_metrics

This cumulative row combines skill-invocation observations for one UTC day, supported agent, active identifier epoch, and bounded skill bucket. Version 1 supports only Claude Code and accepts only its fixture-tested Skill signal. Symposium does not append one row per invocation.

FieldValuesMeaning
agentclaudeThe only version 1 agent with a supported structured skill-invocation signal.
target_scopepublic, unnamed, overflowWhether the bucket names an eligible public skill.
target.typeskill, conditionalPresent only when target_scope=public.
target.sourcesymposium-recommendations, crates-io, conditionalReviewed public source; present only when target_scope=public.
target.namepublic extension name, conditionalPublic skill name; present only when target_scope=public.
unnamed_reasonineligible, not_indexed, attribution_unavailable, ambiguous, invalid_signal, conditionalPresent only when target_scope=unnamed.
attemptedintegerValid Claude PreToolUse:Skill observations merged into the row.
completedintegerSuccessful Claude PostToolUse:Skill observations merged into the row.
failedintegerTerminal Claude PostToolUseFailure:Skill observations merged into the row.
session_counts_completebooleanWhether both distinct identified-session counts are complete for all attempted and completed observations.
identified_sessionsinteger, optionalDistinct sessions with an attempted observation; present only when session counts are complete.
identified_sessions_completedinteger, optionalDistinct sessions with a completed observation; present only when session counts are complete.
extension_subjectscoped id, conditionalPresent only for a public bucket; matches the selected safe resolution path for 30 days.

Count semantics

attempted, completed, and failed are independent lower bounds. Each hook phase updates the snapshot separately. If one update is lost, completed plus failed need not equal attempted and may exceed it. The two session counts have the same independence. A stored row has at least one non-zero phase counter.

failed advances only when the supported agent emits its targeted failure signal. For version 1, that is Claude’s PostToolUseFailure:Skill event. Symposium does not infer failure from denial, termination, or a missing terminal observation. Completion means that the agent activated the skill; it does not show whether the agent followed its instructions or improved the task result.

Attribution

Public attribution comes only from the generated installation index and a matching Symposium marker fingerprint. Unnamed reasons have these meanings:

  • ineligible: the installed skill was private, local, invalid, or otherwise unsafe to name.
  • not_indexed: the index was valid and readable but contained no matching agent-facing identifier.
  • attribution_unavailable: the index was missing, corrupt, or stale.
  • ambiguous: more than one entry matched.
  • invalid_signal: the Claude payload did not match the validated schema.

No reason exposes the raw identifier.

Row and session limits

At most 128 public-skill rows may be named across identifier epochs in one UTC day. Later public skills merge into one overflow row per agent and epoch. Each fixed unnamed reason produces at most one row per agent and epoch. This daily allowance survives identifier reset and telemetry disable. UTC-day rollover starts a new allowance; telemetry clear resets it because clear deletes the snapshot rows it accounts for. The public subset is first-observed, not sampled; analysis must report overflow and include unnamed counts when interpreting the public denominator.

The same 256-id all-or-nothing rule applies to the attempted and completed session sets. Only attempted and completed observations contribute to those sets. A failed observation does not contribute an id or make counts incomplete because its id is missing, but it still reconciles private contribution counts with the stored row. When counts are complete, identified_sessions is at most attempted, identified_sessions_completed is at most completed, and each count is at most 256. A positive attempted or completed counter has at least one session in its matching set. On the first missing id for an attempted or completed observation, overflow, or state/snapshot mismatch, Symposium discards both sets, writes session_counts_complete: false, and omits both counts for the rest of that row and day.

This aggregate has no at, duration, raw or scoped session id, invocation id, tool name, tool input/output, prompt, transcript, or individual outcome row. Unsupported agents emit no row, which means unknown rather than zero.

command

This row records one completed eligible top-level user command.

FieldValuesMeaning
atUTC secondCompletion time.
commandtyped coordinateFixed built-in, or eligible public plugin command coordinate.
duration_msintegerTop-level command duration.
outcomeok, errorClosed result.
command_subjectscoped idDeduplicates this command for 30 days.

Arguments are never recorded. Internal hook, all telemetry commands, and ineligible external/plugin commands do not produce command events.

Built-in command operations are init, sync, search, use, remove, status, plugin_sync, plugin_list, plugin_show, plugin_validate, self_update, and crate_info. remove is the telemetry name for cargo agents use --remove; it is not a separate CLI subcommand.

{"type":"builtin","name":"use"}

An eligible plugin command contains only its reviewed public-source label, public plugin name, and declared command name:

{"type":"plugin","source":"symposium-recommendations","plugin":"example-tools","name":"example-check"}

Public plugin-command names use a fixed version 1 grammar: 1 through 64 ASCII bytes, beginning with an ASCII letter or digit, followed by ASCII letters, digits, -, or _. Symposium preserves the spelling without case folding or Unicode normalization. A plugin command outside this telemetry grammar remains usable but does not produce a command event.

storage_limit

This row means that the next complete low-volume event batch did not fit in the shared daily 8 MiB allowance. In addition to the common fields, dropped_operation is session_start, manual_sync, use, remove, init, configuration, or command. It identifies the top-level operation whose batch was rejected.

The row appears at most once per UTC day, and the marker itself counts toward 8 MiB. An aggregate-metric update that would exceed its separate 512 KiB maximum or the remaining shared allowance is dropped without this marker and does not stop low-volume recording. The marker does not report lock-contention, aggregate-update, or crash losses.

What is never recorded

  • Individual prompt/tool activity records, per-invocation hook records, or individual skill-invocation records.
  • Prompt text or any substring of it.
  • Tool names, arguments, responses, or results.
  • Shell commands, file contents, patches, or URLs from user/agent data.
  • Error messages, debug strings, hook stdout/stderr, numeric exit details, or raw agent/package-manager payloads.
  • Raw agent-facing or private skill identifiers and ephemeral invocation identifiers.
  • File paths, workspace roots, project names, repository ids, or dependency-set snapshots.
  • Environment names/values, hostname, username, home directory, IP address, model name, or account/vendor identifiers.
  • Names or versions from private registries, git/path/workspace sources, or sources whose public identity is not allowlisted.
  • Raw configured registry names/URLs or arbitrary git URLs.
  • Raw vendor session ids, a global installation id, a workspace id, or one identifier shared across measurement purposes.
  • Wall-clock timestamps more precise than one second.

Storage and expiry

Data files and recording

Low-volume events are appended as JSON lines in events-YYYY-MM-DD.jsonl under the inspectable <config-dir>/telemetry/ data directory (default ~/.symposium/telemetry/). Current cumulative hook, plugin-hook, and extension-invocation aggregates are JSON lines in metrics-YYYY-MM-DD.jsonl. Symposium rewrites this bounded snapshot atomically after a merge.

The lock in the telemetry directory also guards sibling private state. A recorder makes one non-waiting lock attempt. It may drop a complete buffered event batch or aggregate observation rather than delay your hook or command. Recording failures never change the user operation’s result.

Under that lock, a recording operation captures its completion timestamp once, uses its UTC day, and rejects it when it precedes the latest-opened-day high-water mark. It calculates the identifier-window transition, applies it with any high-water advancement to one in-memory state, atomically replaces private state once, and only then binds the timestamp, day, and identifier window for row construction. The replacement also occurs when the selected window remains current, so every operation has the same persist-before-bind boundary. Every row in one operation reuses that bound recording context. In particular, a full resolution uses one context for its summary, package, and extension rows.

Session recording also calculates the return-cohort transition before mutating either anchor. The identifier-window transition, return-cohort transition, and high-water advancement are persisted together. The session_start row is then derived from the same bound session context. If its append fails after a new cohort is stored, later rows for the cohort remain ineligible for Q1 unless a D0 row was stored. This failure mode undercounts returns rather than creating unstable identity.

Private state keeps the latest opened UTC day as a high-water mark. Observing a later day permanently closes earlier daily files. An observation dated before the high-water mark is dropped rather than modifying a closed day. Raw inspection still preserves every stored line. Typed reading of a closed day returns only recognized rows that pass their versioned schema and file/day invariants, and reports malformed, invalid, and unknown-version lines separately. It rejects an oversized or incompletely read day as a whole rather than returning a partial validated result.

Daily limits and retention

The event file, aggregate-metric snapshot, and reserved maximum-size storage_limit line share an 8 MiB daily allowance. This allowance is a safety ceiling, not expected volume or preallocation. It bounds damage from a producer bug or unexpectedly large resolution batch.

Aggregate metrics may use at most 512 KiB, so high-volume hook and skill activity cannot consume the allowance reserved for resolution and configuration events.

A file is eligible for deletion only when current_utc_day - file_utc_day > 30. A D0 file remains throughout D30 and is first eligible on D31. Together with D31 expiry, the daily limit bounds ordinary retained telemetry near 248 MiB, excluding temporary files and private state. Cleanup is lazy, so an old file remains until a recording-capable invocation or telemetry command runs.

Private state

The sibling private <config-dir>/telemetry-state.toml holds the identity key, current identifier-window anchor, optional return-cohort anchor, the latest opened UTC day, cleanup and marker metadata, bounded keyed session sets, snapshot contribution counts used to calculate complete distinct-session counts, the stable row event_id in each aggregate entry that cannot reconstruct its identity from the row, and separate per-day public-row admission counts for plugin-hook and extension-invocation metrics. The row identifier is not secret; the corresponding metric row contains the same value.

Symposium creates and replaces it atomically with owner-only permissions where supported. Replacement uses a same-directory temporary file beside config.toml; abandoned state temporaries are ignored and cleaned lazily under the telemetry lock.

A recorder reads at most 16 MiB of private state. Invalid UTF-8, invalid TOML, TOML that violates the state schema, and an oversized file are malformed state; diagnostics retain at most the error location and never the source line or parser error that could contain the identity key. A syntactically valid unsupported state version stops recording but is reported separately from malformed state, so an older binary does not treat newer state as safe to replace.

The session sets are not printed or copied into metric rows. Symposium discards each plugin-hook or extension-invocation entry, including its row identifier, session sets, and contribution counts, at UTC-day rollover and removes it when telemetry clear or telemetry reset-identifiers runs. clear also deletes the row the entry identifies; reset removes entries whose keys belong to the previous identifier epoch. State is replaced before the corresponding metric snapshot. If a later snapshot write fails, a contribution-count mismatch on the next update discards the sets and permanently marks the row’s session counts incomplete for that day.

Loading private state with an existing metric snapshot reconciles the two before selecting an aggregate. Symposium rebuilds each daily public-row allowance from every surviving public row for that family and day, including rows from earlier identifier epochs, and saturates the count at 128.

When selecting a public aggregate with no private entry, Symposium adopts a surviving row’s event_id without consuming another allowance slot only when the row’s day, agent, public coordinate, and, for plugin hooks, hook surface match the observation and the recorded subject equals the subject derived from that observation. Adoption runs before overflow selection. A surviving row that receives no later observation needs no private entry. An unnamed or overflow row cannot establish this relationship, so a missing private entry starts a separate row with a new event_id.

telemetry clear deletes event and aggregate-metric files and rewrites private state to remove pending sets while preserving the identity key, current anchors, and latest-opened-day high-water mark. telemetry reset-identifiers rotates future identifiers, sets the identifier-window anchor to the later of the current UTC day and that high-water mark, and clears the return-cohort anchor without moving the high-water mark backward. The next observed session starts a new retention cohort at D0. telemetry disable stops recording; existing files remain unless the user accepts its interactive clear offer or runs telemetry clear later.

Installation index

The generated <agent-skills-parent>/.symposium/index-v1.json file is installation state, not telemetry. It may contain local agent-facing identifiers, installed-directory fingerprints, eligibility, and safe public attribution needed to identify the skill Symposium installed. It is gitignored, atomically replaced after sync, excluded from telemetry show and telemetry clear, and not uploaded by this RFD.

Limits of interpretation

Public names, versions, and exact counts can be identifying when they are unusual. Exact daily hook counts disclose approximate prompt/tool activity by surface. Extension-invocation rows disclose exact skill-attempt, completion, and failure counts.

Lock contention, process termination, full files, and I/O failures can make the log incomplete. The records are therefore pseudonymous, locally associated, and best-effort. They must not be described as anonymous or as proof that an unrecorded action did not happen.