diff --git a/.agents/skills/agent-core-dev/telemetry.md b/.agents/skills/agent-core-dev/telemetry.md
index 0d04dc5da4e..e1bf621e21f 100644
--- a/.agents/skills/agent-core-dev/telemetry.md
+++ b/.agents/skills/agent-core-dev/telemetry.md
@@ -7,7 +7,8 @@ Telemetry is a **layer-1 root** domain (alongside `log`): the facade lives at `A
## Where things live
- `src/app/telemetry/telemetry.ts`: contract — `ITelemetryService` (facade), `ITelemetryAppender` (destination), `TelemetryProperties`, `nullTelemetryAppender`, and `TelemetryServiceOptions`.
-- `src/app/telemetry/events.ts`: event registry — `telemetryEventDefinitions` pairs every business event's property type with review metadata (owner / purpose / per-property comment); the single source of truth for `track2`. Agent-scope events register with `defineAgentTelemetryEvent
` and compose the ambient `AgentTelemetryEventContext` (`agent_id`) into their wire schema; all other events register with `defineTelemetryEvent
`.
+- `src/app/telemetry/events.ts`: event registry — `telemetryEventDefinitions` pairs every business event's property type with review metadata (owner / domain / purpose / per-property comment); the single source of truth for `track2`. Agent-scope events register with `defineAgentTelemetryEvent
` and compose the ambient `AgentTelemetryEventContext` (`agent_id`) into their wire schema; all other events register with `defineTelemetryEvent
`.
+- `src/app/telemetry/coverage.ts`: domain coverage map — `telemetryDomainExemptions` (domains that intentionally emit nothing, with reasons) and `telemetryDomainKnownGaps` (domains with zero coverage whose events are planned). `events.test.ts` walks `src/` and requires every domain to own an event, be exempted, or be listed as a known gap; adding a domain without a telemetry decision fails the test.
- `src/app/telemetry/telemetryService.ts`: `TelemetryService` impl + `registerScopedService(LifecycleScope.App, …)`.
- `src/app/telemetry/agentTelemetryContext.ts` + `agentTelemetryContextService.ts`: `IAgentTelemetryContextService` — Agent-scoped mutable request context (`mode` / `provider_type` / `protocol` / `turn_id` / `trace_id`) snapshot into turn telemetry at launch. Agent identity (`agent_id`) is not part of it — identity is bound by the Agent-scoped `ITelemetryService` view.
- `src/app/telemetry/consoleAppender.ts`: `ConsoleAppender` — echoes events to a log function (dev / debug).
@@ -27,7 +28,7 @@ constructor(@ITelemetryService private readonly telemetry: ITelemetryService) {}
this.telemetry.track2('cron_fired', { task_id: taskId, coalesced_count: 0, stale: false, buffered: false, recurring: true });
```
-`track2` is checked against the registry in `events.ts` at compile time: the event name must be a key of `telemetryEventDefinitions`, and the properties must match the registered interface exactly (extra or missing keys are compile errors). **New events must be registered first** — add a properties interface, then register it with `defineAgentTelemetryEvent
({ owner, comment, properties })` when every emission path goes through an Agent-scoped `ITelemetryService` view, or `defineTelemetryEvent
` otherwise (including events with any non-Agent emission path, e.g. `image_compress` from the kap-server prompt routes), documenting every property. For agent-scope events the registered interface is the business payload only: ambient `agent_id` is declared once in `AgentTelemetryEventContext` and composed into the wire schema, so it must not appear in the payload or at call sites. Naming: snake_case for events and properties, unit suffixes (`_ms` / `_count` / `_bytes`), no user content or file paths; `test/app/telemetry/events.test.ts` enforces the conventions. The low-level `track` remains for appender plumbing and tests only.
+`track2` is checked against the registry in `events.ts` at compile time: the event name must be a key of `telemetryEventDefinitions`, and the properties must match the registered interface exactly (extra or missing keys are compile errors). **New events must be registered first** — add a properties interface, then register it with `defineAgentTelemetryEvent
({ owner, domain, comment, properties })` when every emission path goes through an Agent-scoped `ITelemetryService` view, or `defineTelemetryEvent
` otherwise (including events with any non-Agent emission path, e.g. `image_compress` from the kap-server prompt routes), documenting every property. `domain` is the owning `src/` directory path (`agent/loop`, `wire`) or the pseudo-domain `host` for events the host app emits; `coverage.ts` plus `events.test.ts` require every source domain to own an event, be exempted, or be listed as a known gap. For agent-scope events the registered interface is the business payload only: ambient `agent_id` is declared once in `AgentTelemetryEventContext` and composed into the wire schema, so it must not appear in the payload or at call sites. Naming: snake_case for events and properties, unit suffixes (`_ms` / `_count` / `_bytes`), no user content or file paths; `test/app/telemetry/events.test.ts` enforces the conventions. The low-level `track` remains for appender plumbing and tests only.
`TelemetryService.track` merges the bound context into the properties and fans the event out to every registered appender. A single throwing appender is isolated via `onUnexpectedError` and never blocks the rest.
@@ -79,7 +80,7 @@ telemetry.addAppender(new CloudAppender({ // production
`addAppender` returns an `IDisposable` that removes the appender when disposed. `setAppender(appender)` resets to a single appender (mainly for tests). `removeAppender(appender)` drops one.
-> There is no production bootstrap wired yet — `TelemetryService` defaults to `[nullTelemetryAppender]`, so `track(...)` is a no-op until `addAppender` is called at startup.
+> Production bootstrap exists in three places: kap-server (`packages/kap-server/src/services/telemetry.ts`), the v2 print runner (`apps/kimi-code/src/cli/v2/run-v2-print.ts`), and the node-sdk v2 client (`installEngineTelemetry` in `packages/node-sdk/src/sdk-rpc-client-v2.ts`) — each attaches a `CloudAppender` (or the host client) at startup. Without `addAppender`, `TelemetryService` defaults to `[nullTelemetryAppender]` and `track(...)` is a no-op.
## Lifecycle
diff --git a/packages/agent-core-v2/AGENTS.md b/packages/agent-core-v2/AGENTS.md
index eb8da514b25..076e13ad441 100644
--- a/packages/agent-core-v2/AGENTS.md
+++ b/packages/agent-core-v2/AGENTS.md
@@ -48,6 +48,7 @@ Business events go through `ITelemetryService.track2` — never the low-level `t
- **Naming**: event names and property keys are snake_case (`tool_call`, `duration_ms`). Durations, counts, and sizes carry a unit suffix (`_ms` / `_count` / `_bytes`). Use specific names (`error_type`, not `error`).
- **Privacy**: never register user content, prompts, or file paths as properties. `CloudAppender` redacts URLs, emails, tokens, and absolute paths from string values before events leave the process, but that is a safety net, not a license.
- **Stability**: registered event names and property keys are wire data consumed by dashboards — treat renames as breaking changes.
+- **Coverage**: every event declares its owning `domain` — a `src/` directory path (`agent/loop`, `wire`) or the pseudo-domain `host` for events the host app emits. `src/app/telemetry/coverage.ts` records the domains that intentionally emit nothing (`telemetryDomainExemptions`, with reasons) and the domains whose gaps are known and tracked (`telemetryDomainKnownGaps`, with the planned events). `events.test.ts` walks `src/` and fails unless every domain owns an event, is exempted, or is a known gap — adding a domain without a telemetry decision breaks the test, so make the decision explicit.
- The registry is the single source of truth; `test/app/telemetry/events.test.ts` enforces the naming conventions.
## Persistence
diff --git a/packages/agent-core-v2/src/app/telemetry/coverage.ts b/packages/agent-core-v2/src/app/telemetry/coverage.ts
new file mode 100644
index 00000000000..e859b439c4b
--- /dev/null
+++ b/packages/agent-core-v2/src/app/telemetry/coverage.ts
@@ -0,0 +1,146 @@
+export const telemetryCoverageTierRoots = ['app', 'session', 'agent', 'workspace', 'features'] as const;
+
+export const telemetryPseudoDomains = ['host'] as const;
+
+export const telemetryDomainExemptions: Readonly> = {
+ 'app/agentIdentity': 'Config-derived snapshot built once; no IO, state machine, or failure branches.',
+ 'app/agentProfileCatalog':
+ 'Type definitions, pure functions, and static built-in profile loading; no IO or user decisions.',
+ 'app/authLegacy':
+ 'Read-only aggregate facade over config and oauth status; authentication events belong to app/auth.',
+ 'app/bashParser': 'Stateless parse adapter over tree-sitter-bash; no IO or lifecycle.',
+ 'app/bootstrap': 'Pure value carriers (path derivation, env reads) and scope creation functions.',
+ 'app/edit': 'Thin file-edit adapter; failures return to the Edit tool and are covered by tool_call.',
+ 'app/event': 'Pure pub/sub plumbing with no business semantics.',
+ 'app/feature': 'Thin DI unit management shell; no user-perceivable behavior.',
+ 'app/flag': 'In-memory flag resolution (env > config > default); no IO or lifecycle.',
+ 'app/gateway': 'Thin delegate to prompt/loop services; turn events are emitted by the owning domains.',
+ 'app/hostFolderBrowser': 'Stateless readdir proxy; errors map to typed RPC errors.',
+ 'app/mcpRegistry': 'Read-only aggregation of config and plugin queries; no writes or failure branches.',
+ 'app/projectLocalConfig': 'Interface declaration only; implementation lives in persistence backends.',
+ 'app/remoteControl': 'Flag definition registration only; no runtime logic.',
+ 'app/sessionManager':
+ 'Resume failures funnel to session_load_failed via sessionLookup; lifecycle events are owned by workspace/sessionLifecycle.',
+ 'app/sessionLegacy': 'Read-only aggregate proxy; resume failures are covered by session_load_failed.',
+ 'app/state': 'StateRegistry subclass with no business logic.',
+ 'app/task': 'Generic task-handle primitive; background task events live in agent/task.',
+ 'app/telemetry': 'Telemetry infrastructure itself; it cannot instrument itself.',
+ 'app/workspaceAliases': 'Stateless resolution proxy over IWorkspaceService and the session index.',
+ 'app/workspaceSessions': 'Read-only aggregate facade over workspace aliases and the session index.',
+ 'session/approval': 'Thin delegate to features/interaction; resolution events are emitted by agent/toolApproval.',
+ 'session/mcp': 'Type seeds and a merged view; connection events are emitted by workspace/workspaceMcp.',
+ 'session/question':
+ 'Thin delegate; resolution events are emitted by the ask-user-question tool (agent/tools).',
+ 'session/sessionActivity': 'Pure in-memory fold of already-instrumented turn and activity events.',
+ 'session/sessionAgentProfileCatalog':
+ 'In-memory registry merge projection; diagnostics go through log and inspect surfaces.',
+ 'session/sessionContext': 'Pure data carrier (sessionId/workspaceId/cwd) plus seed factory.',
+ 'session/sessionInstructions': 'Interface declaration and DI seed helper only.',
+ 'session/sessionLog':
+ 'Thin adapter over FileLogWriter; it is the logging substrate telemetry itself relies on.',
+ 'session/sessionToolPolicy': 'Simple disabled-tools preference persistence; changes broadcast via onDidChange.',
+ 'session/sessionToolPolicyGate': 'No-op implementation with no behavior to observe.',
+ 'session/state': 'StateRegistry subclass with no business logic.',
+ 'session/tokenCounting':
+ 'Pure in-memory token estimation bookkeeping; size signals are covered by loop and compaction events.',
+ 'session/usage':
+ 'Pure in-memory usage accumulator; consumption signals are covered by llmRequester and loop events.',
+ 'session/workspaceInfo': 'Interface declaration and scope seed factory only.',
+ 'agent/activityView':
+ 'Read-only projection of loop, task, compaction, and approval events that are instrumented at the source.',
+ 'agent/agentContext': 'In-memory agent model lease registry; anomalies route to onUnexpectedError.',
+ 'agent/command': 'Thin dispatcher over command contributions; real work is instrumented by owning domains.',
+ 'agent/contextMemory':
+ 'In-memory history mutation API; lifecycle operations are instrumented by owner domains (undo, compaction, loop).',
+ 'agent/interruptionReminder':
+ 'Fixed-text reminder injection on user_cancelled; the interrupt itself is covered by turn_interrupted.',
+ 'agent/modeMutex':
+ 'Mode mutual-exclusion wiring over the event bus; mode transitions are owned by features/plan, features/swarm, and features/tower.',
+ 'agent/permissionPolicy': 'Stateless policy evaluation chain; decisions are recorded by permissionGate.',
+ 'agent/permissionRules': 'Thin state wrapper; approval persistence is recorded by permission_approval_result.',
+ 'agent/plugin': 'Reminder reconcile and render logic; plugin install/enable events belong to app/plugin.',
+ 'agent/replayBuilder': 'Pure type definitions.',
+ 'agent/scopeContext': 'Stateless plumbing (scope key and frozen context factory).',
+ 'agent/state': 'StateRegistry subclass for replayable key bookkeeping.',
+ 'agent/tokenCounting': 'Contracts and wire event definitions only; counting logic lives in session/tokenCounting.',
+ 'agent/toolActivation': 'Tool registration bookkeeping; policy-blocked calls surface via tool_call.',
+ 'agent/toolPolicy': 'Pure policy evaluation; guard interceptions are recorded via tool_call.',
+ 'agent/toolRegistry': 'In-memory Map registry with no IO or failure degradation.',
+ 'workspace/state': 'StateRegistry subclass with no business logic.',
+ 'workspace/workspaceContext': 'Pure data interface plus seed factory.',
+ 'workspace/workspaceDirs': 'Thin state holder over project-local config; failures propagate to session creation.',
+ 'workspace/workspaceGit': 'Pass-through proxy to IGitService; git subprocess telemetry belongs to app/git.',
+ 'workspace/workspaceInstructions':
+ 'AGENTS.md snapshot loader with fs watch; reload failures degrade to prior content with log.warn.',
+ 'workspace/workspaceMcpConfig':
+ 'Config aggregation with fingerprint diff; connection outcomes are covered by workspaceMcp events.',
+ 'features/dateChange': 'Date disclosure computation and reminder injection; no IO or decisions.',
+ 'features/debugEvents': 'Read-only introspection for the /api/v1/debug surface.',
+ 'features/tokenCounting': 'Pure feature assembly; counting logic lives in session/tokenCounting.',
+ 'features/usage': 'Pure feature assembly; usage logic lives in session/usage.',
+ _base: 'DI kernel and base utilities below the telemetry layer; activation failures surface as sticky Failed units.',
+ debug: 'Read-only introspection views for the /api/v1/debug surface.',
+ kosong:
+ 'LLM HTTP errors translate and bubble to api_error in agent/llmRequester; instrumenting here would double-count.',
+ os: 'Host capability implementations; failures bubble to caller domains (mcp_failed, tool_call, fs fallbacks).',
+ runtime:
+ 'Runtime registry and host shells; failures throw typed RuntimeError to callers and state changes publish via onDidChange.',
+ state: 'State definitions and the event dispatcher pipeline; restore failures are covered by session_load_failed.',
+ tool: 'Stateless tool utilities (path access, args validation, output accumulation); rejections surface via tool_call.',
+};
+
+export const telemetryDomainKnownGaps: Readonly> = {
+ 'app/auth':
+ 'Login funnel: oauth_login_finished (provider, status, duration_ms), oauth_models_refresh_finished, auth_ensure_ready_failed.',
+ 'app/capability': 'Install funnel: capability_install_started / capability_install_ended (outcome, duration_ms).',
+ 'app/config': 'Config health: config_load_failed, config_persist_blocked, config_migration_applied.',
+ 'app/file': 'Upload health: file_saved (outcome, size_bytes), file_blob_missing (index/blob divergence).',
+ 'app/git': 'Subprocess health: git_spawn_failed, git_command_timeout, git_command_duration.',
+ 'app/kosongConfig': 'Provider config: config_persist_failed, provider_models_refreshed, provider_catalog_import.',
+ 'app/mcpConfig': 'Credential store: mcp_oauth_store_read_failed (silent credential loss).',
+ 'app/mcpManagement': 'Server management: mcp_server_test, mcp_auth_flow_completed, mcp_server_config_mutated.',
+ 'app/plugin': 'Plugin lifecycle: plugin_install, plugin_reload, plugin_update_check.',
+ 'app/sessionExport': 'Export health: session_export (success, duration_ms, entries_count).',
+ 'app/sessionIndex':
+ 'Read model health: session_index_degraded, session_index_projected, session_index_mirror_give_up.',
+ 'app/web': 'Managed fetch fallback: web_fetch_fallback (silent local degradation).',
+ 'app/workspace':
+ 'Workspace lifecycle: workspace_created, workspace_deleted, workspace_catalog_rebuilt, workspace_root_invalid.',
+ 'session/agentLifecycle': 'Creation failure: agent_create_failed (stage, error_type).',
+ 'session/sessionMetadata': 'Metadata health: session_meta_load_failed, session_meta_migrated.',
+ 'session/sessionTitle':
+ 'Title generation: session_title_generated, session_title_generation_failed (experiment evaluation).',
+ 'session/terminal': 'Terminal lifecycle: terminal_spawn_failed, terminal_exited.',
+ 'session/workspaceContext': 'Security boundary: workspace_path_denied (path escape attempts).',
+ 'agent/blob': 'Media storage: blob_read_failed (silent media loss), blob_offloaded.',
+ 'agent/mcp': 'MCP tool calls: mcp_tool_reconnect, mcp_tool_name_collision.',
+ 'agent/pluginCommand': 'Adoption: plugin_command (plugin_id, command_name).',
+ 'agent/runtime': 'Runtime lifecycle: agent_runtime_failed (phase), agent_runtime_restored (duration_ms).',
+ 'agent/runtimeBinding': 'Binding decisions: agent_runtime_binding_changed, agent_runtime_binding_rejected.',
+ 'agent/shellCommand':
+ 'Execution bypasses tool_call: shell_command_finished (duration_ms, is_error, backgrounded).',
+ 'agent/stepRetry': 'Retry behavior: turn_step_retrying, turn_step_retry_exhausted.',
+ 'agent/toolResultTruncation':
+ 'Truncation: tool_result_truncated (size distribution), tool_result_spill_save_failed.',
+ 'agent/toolSelect': 'Dynamic tool loading: tool_select_load (to_load_count, unknown_count).',
+ 'agent/userTool': 'Adoption: user_tool_registered.',
+ 'workspace/workspaceAgentProfileLoader': 'Profile loading: agent_profile_load_failed (source, fatal).',
+ 'workspace/workspaceInstance': 'Materialization: workspace_materialized (duration_ms), workspace_materialize_failed.',
+ 'workspace/workspaceTrust':
+ 'Trust decisions: workspace_trust_changed, workspace_trust_read_failed (fail-closed silently).',
+ 'features/btw': 'Adoption: btw_started.',
+ 'features/externalHooks':
+ 'Hook execution: external_hook_executed (outcome), external_hook_blocked (security decisions).',
+ 'features/interaction': 'Orphan interactions: interaction_cancelled (kind, reason, pending_duration_ms).',
+ 'features/reminder': 'Injection health: reminder_provider_failed (silent context-injection failure).',
+ 'features/sessionInit': '/init run: session_init (outcome, duration_ms).',
+ 'features/staleGuard': 'Guard hits: stale_guard_blocked (reason).',
+ 'features/swarm':
+ 'Batch runs: agent_swarm_batch_finished (outcome distribution), agent_swarm_rate_limit_mode_entered.',
+ 'features/todo': 'Reminder strategy: todo_list_reminder_shown.',
+ 'features/tower':
+ 'Tower governance: tower_mode_entered, tower_spawn_denied, tower_rate_limit_paused, tower_worktree_escape_denied, tower_worktree_setup_warning.',
+ mcpCore: 'Runtime connection health: mcp_server_dropped, mcp_oauth_refresh_failed.',
+ persistence: 'Store health: query_store_rebuilt (silent corruption recovery).',
+ program: 'Generation failures: program_generation_failed (stage).',
+};
diff --git a/packages/agent-core-v2/src/app/telemetry/events.ts b/packages/agent-core-v2/src/app/telemetry/events.ts
index 4cc18048738..ac1dc186e65 100644
--- a/packages/agent-core-v2/src/app/telemetry/events.ts
+++ b/packages/agent-core-v2/src/app/telemetry/events.ts
@@ -2,6 +2,7 @@ import type { TelemetryPrimitive } from './telemetry';
export interface TelemetryEventMeta {
readonly owner: string;
+ readonly domain: string;
readonly comment: string;
readonly properties: Readonly>;
}
@@ -477,6 +478,7 @@ export interface ExitEvent {
export const telemetryEventDefinitions = {
turn_started: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/loop',
comment: 'A turn starts running.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session',
@@ -488,6 +490,7 @@ export const telemetryEventDefinitions = {
}),
turn_interrupted: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/loop',
comment: 'A running turn is interrupted.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session',
@@ -503,6 +506,7 @@ export const telemetryEventDefinitions = {
}),
turn_ended: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/loop',
comment: 'A turn ends, unconditionally.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session',
@@ -518,6 +522,7 @@ export const telemetryEventDefinitions = {
}),
prompt_cache_probe: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/usage',
comment:
'An agent whose first request is expected to hit the prompt cache reports that request\'s cache usage.',
properties: {
@@ -533,6 +538,7 @@ export const telemetryEventDefinitions = {
}),
tool_call: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/toolExecutor',
comment: 'A tool call finishes execution.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session',
@@ -548,6 +554,7 @@ export const telemetryEventDefinitions = {
}),
api_error: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/llmRequester',
comment: 'An LLM API request fails.',
properties: {
error_type: 'Classified error category',
@@ -568,6 +575,7 @@ export const telemetryEventDefinitions = {
}),
skill_invoked: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/skill',
comment: 'A skill is invoked.',
properties: {
skill_name: 'Skill name',
@@ -576,11 +584,13 @@ export const telemetryEventDefinitions = {
}),
flow_invoked: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/skill',
comment: 'A flow-type skill is invoked.',
properties: { flow_name: 'Flow name' },
}),
input_steer: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/prompt',
comment: 'The user steers input while a turn is running.',
properties: {
parts: 'Number of input parts',
@@ -588,6 +598,7 @@ export const telemetryEventDefinitions = {
}),
cancel: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/loop',
comment: 'The user cancels ongoing work.',
properties: {
from: 'What was running when cancelled',
@@ -597,6 +608,7 @@ export const telemetryEventDefinitions = {
}),
conversation_undo: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/undo',
comment: 'The user undoes conversation entries.',
properties: {
count: 'Number of entries undone',
@@ -604,16 +616,19 @@ export const telemetryEventDefinitions = {
}),
yolo_toggle: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/permissionMode',
comment: 'Yolo permission mode is toggled.',
properties: { enabled: 'Whether yolo mode is now enabled' },
}),
afk_toggle: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/permissionMode',
comment: 'AFK (auto) permission mode is toggled.',
properties: { enabled: 'Whether auto mode is now enabled' },
}),
permission_policy_decision: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/permissionGate',
comment: 'A permission policy evaluates a tool call.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session',
@@ -626,6 +641,7 @@ export const telemetryEventDefinitions = {
}),
permission_approval_result: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/toolApproval',
comment: 'A permission approval prompt resolves.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session',
@@ -644,6 +660,7 @@ export const telemetryEventDefinitions = {
}),
plan_submitted: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/plan',
comment: 'A plan is submitted for review.',
properties: {
has_options: 'Whether the plan offered selectable options',
@@ -651,6 +668,7 @@ export const telemetryEventDefinitions = {
}),
plan_resolved: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/plan',
comment: 'A submitted plan is resolved.',
properties: {
outcome: 'How the plan was resolved',
@@ -660,6 +678,7 @@ export const telemetryEventDefinitions = {
}),
plan_enter_resolved: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/plan',
comment: 'A request to enter plan mode is resolved.',
properties: {
outcome: 'How the request was resolved',
@@ -667,6 +686,7 @@ export const telemetryEventDefinitions = {
}),
compaction_finished: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/fullCompaction',
comment: 'Context compaction completes.',
properties: {
turn_id: 'Per-agent turn index when compaction ran inside a turn; omitted for manual compaction between turns',
@@ -689,6 +709,7 @@ export const telemetryEventDefinitions = {
}),
compaction_failed: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/fullCompaction',
comment: 'Context compaction fails.',
properties: {
turn_id: 'Per-agent turn index when compaction ran inside a turn; omitted for manual compaction between turns',
@@ -705,6 +726,7 @@ export const telemetryEventDefinitions = {
}),
context_projection_repaired: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/contextProjector',
comment: 'The context projector repairs the outgoing request to keep it wire-valid.',
properties: {
reordered: 'Tool results moved back next to their call',
@@ -720,6 +742,7 @@ export const telemetryEventDefinitions = {
}),
background_task_created: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/task',
comment: 'A background task is created.',
properties: {
task_id: 'Background task id; joins background_task_created with background_task_completed',
@@ -728,6 +751,7 @@ export const telemetryEventDefinitions = {
}),
background_task_completed: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/task',
comment: 'A background task reaches a terminal state.',
properties: {
task_id: 'Background task id; joins background_task_created with background_task_completed',
@@ -738,6 +762,7 @@ export const telemetryEventDefinitions = {
}),
wait_for_completed: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/tools',
comment: 'A WaitFor tool call returns.',
properties: {
outcome:
@@ -750,11 +775,13 @@ export const telemetryEventDefinitions = {
}),
model_switch: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/profile',
comment: 'The active model is bound or switched.',
properties: { model: 'Model alias' },
}),
thinking_toggle: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/profile',
comment: 'Thinking effort is toggled.',
properties: {
enabled: 'Whether thinking is now enabled',
@@ -764,6 +791,7 @@ export const telemetryEventDefinitions = {
}),
question_dismissed: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/tools',
comment: 'A user question prompt is dismissed.',
properties: {
trace_id:
@@ -772,6 +800,7 @@ export const telemetryEventDefinitions = {
}),
question_answered: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/tools',
comment: 'A user question prompt is answered.',
properties: {
answered: 'Number of questions answered',
@@ -782,6 +811,7 @@ export const telemetryEventDefinitions = {
}),
goal_created: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/goal',
comment: 'A goal is created.',
properties: {
actor: 'Who created the goal',
@@ -790,6 +820,7 @@ export const telemetryEventDefinitions = {
}),
goal_budget_set: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/goal',
comment: 'A goal budget is set.',
properties: {
actor: 'Who set the budget',
@@ -800,16 +831,19 @@ export const telemetryEventDefinitions = {
}),
goal_continued: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/goal',
comment: 'A goal continues into another turn.',
properties: { turns_used: 'Turns consumed so far' },
}),
goal_cleared: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/goal',
comment: 'A goal is cleared.',
properties: { actor: 'Who cleared the goal' },
}),
goal_status_changed: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/goal',
comment: 'A goal changes status.',
properties: {
actor: 'Who changed the status',
@@ -824,6 +858,7 @@ export const telemetryEventDefinitions = {
}),
tool_call_dedup_detected: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/toolDedupe',
comment: 'A duplicate tool call is detected.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session; omitted when no turn is active',
@@ -838,6 +873,7 @@ export const telemetryEventDefinitions = {
}),
tool_call_repeat: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/toolDedupe',
comment: 'A repeated tool call streak is detected.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session; omitted when no turn is active',
@@ -850,6 +886,7 @@ export const telemetryEventDefinitions = {
}),
tool_call_turn_repeat: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/toolDedupe',
comment: 'A tool call reappears within the same turn.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session; omitted when no turn is active',
@@ -864,6 +901,7 @@ export const telemetryEventDefinitions = {
}),
agents_md_reminder_shown: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/agentsMdReminder',
comment: 'An AGENTS.md discovery reminder is appended to a tool result.',
properties: {
turn_id: 'Per-agent turn index (main or subagent); pair with agent_id to locate a turn within a session',
@@ -875,6 +913,7 @@ export const telemetryEventDefinitions = {
}),
grep_tool_rg_fallback: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/tools',
comment: 'The grep tool falls back when resolving ripgrep.',
properties: {
source: 'Where ripgrep was resolved from',
@@ -883,6 +922,7 @@ export const telemetryEventDefinitions = {
}),
glob_tool_rg_fallback: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/tools',
comment: 'The glob tool falls back when resolving ripgrep.',
properties: {
source: 'Where ripgrep was resolved from',
@@ -891,16 +931,19 @@ export const telemetryEventDefinitions = {
}),
fs_grep_node_fallback: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'workspace/workspaceFs',
comment: 'The fs grep path falls back to the node implementation.',
properties: { reason: 'Why the fallback was taken' },
}),
fs_suggest_node_fallback: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'workspace/workspaceFs',
comment: 'The fs suggest path falls back to the node implementation.',
properties: { reason: 'Why the fallback was taken' },
}),
subagent_created: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'session/subagent',
comment: 'A subagent run is created.',
properties: {
subagent_name: 'Profile name of the subagent',
@@ -916,6 +959,7 @@ export const telemetryEventDefinitions = {
}),
mcp_connected: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'workspace/workspaceMcp',
comment: 'MCP servers connect at session start.',
properties: {
server_count: 'Number of servers connected',
@@ -924,6 +968,7 @@ export const telemetryEventDefinitions = {
}),
mcp_failed: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'workspace/workspaceMcp',
comment: 'MCP servers fail to connect at session start.',
properties: {
failed_count: 'Number of servers that failed',
@@ -932,11 +977,13 @@ export const telemetryEventDefinitions = {
}),
cron_missed: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/cron',
comment: 'Cron tasks fire late after being slept through.',
properties: { count: 'Number of tasks that missed their fire time' },
}),
cron_scheduled: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/cron',
comment: 'A cron task is scheduled.',
properties: {
recurring: 'Whether the task repeats',
@@ -945,6 +992,7 @@ export const telemetryEventDefinitions = {
}),
cron_deleted: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/cron',
comment: 'A cron task is deleted.',
properties: {
task_id: 'Cron task id',
@@ -953,6 +1001,7 @@ export const telemetryEventDefinitions = {
}),
cron_fired: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'features/cron',
comment: 'A cron task fires.',
properties: {
recurring: 'Whether the task repeats',
@@ -963,6 +1012,7 @@ export const telemetryEventDefinitions = {
}),
image_compress: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/media',
comment: 'An image is compressed before being sent to the model.',
properties: {
source: 'Where the image came from',
@@ -981,6 +1031,7 @@ export const telemetryEventDefinitions = {
}),
image_crop: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/media',
comment: 'An image is cropped to a region before being sent to the model.',
properties: {
source: 'Where the image came from',
@@ -996,6 +1047,7 @@ export const telemetryEventDefinitions = {
}),
video_upload: defineAgentTelemetryEvent({
owner: 'kimi-code',
+ domain: 'agent/media',
comment: 'A video is uploaded for the model.',
properties: {
model: 'Model the video is uploaded for',
@@ -1010,16 +1062,19 @@ export const telemetryEventDefinitions = {
}),
session_started: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'workspace/sessionLifecycle',
comment: 'A session becomes active (created, forked, or resumed).',
properties: { resumed: 'Whether the session was resumed from disk' },
}),
session_load_failed: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'workspace/sessionLifecycle',
comment: 'A session resume fails.',
properties: { reason: 'Error code, error name, or unknown' },
}),
wire_repair: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'wire',
comment: 'A corrupted wire journal is truncated to its valid prefix and healed on disk.',
properties: {
kind: 'Corruption kind: unparseable middle line or torn final line',
@@ -1030,11 +1085,13 @@ export const telemetryEventDefinitions = {
}),
first_launch: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'host',
comment: 'The CLI runs for the first time on this device.',
properties: {},
}),
exit: defineTelemetryEvent({
owner: 'kimi-code',
+ domain: 'host',
comment: 'A CLI run exits.',
properties: { duration_ms: 'Run wall-clock time in milliseconds' },
}),
diff --git a/packages/agent-core-v2/test/app/telemetry/events.test.ts b/packages/agent-core-v2/test/app/telemetry/events.test.ts
index 5c0191ead84..2a39eb9b450 100644
--- a/packages/agent-core-v2/test/app/telemetry/events.test.ts
+++ b/packages/agent-core-v2/test/app/telemetry/events.test.ts
@@ -1,5 +1,15 @@
+import { readdirSync } from 'node:fs';
+import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
import { describe, expect, expectTypeOf, it } from 'vitest';
+import {
+ telemetryCoverageTierRoots,
+ telemetryDomainExemptions,
+ telemetryDomainKnownGaps,
+ telemetryPseudoDomains,
+} from '#/app/telemetry/coverage';
import {
agentTelemetryContextProperties,
telemetryEventDefinitions,
@@ -48,3 +58,63 @@ describe('telemetry event registry', () => {
}>();
});
});
+
+describe('telemetry domain coverage', () => {
+ const srcDir = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..', '..', 'src');
+ const tierRoots = new Set(telemetryCoverageTierRoots);
+
+ const domains: string[] = [];
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
+ if (!entry.isDirectory()) {
+ continue;
+ }
+ if (!tierRoots.has(entry.name)) {
+ domains.push(entry.name);
+ continue;
+ }
+ for (const sub of readdirSync(join(srcDir, entry.name), { withFileTypes: true })) {
+ if (sub.isDirectory()) {
+ domains.push(`${entry.name}/${sub.name}`);
+ }
+ }
+ }
+ const domainSet = new Set(domains);
+ const validDomains = new Set([...domains, ...telemetryPseudoDomains]);
+ const eventDomains = new Set(
+ Object.values(telemetryEventDefinitions).map((definition) => definition.meta.domain),
+ );
+
+ it('accounts for every source domain with an event, an exemption, or a known gap', () => {
+ for (const domain of domains) {
+ const covered = eventDomains.has(domain);
+ const exempt = domain in telemetryDomainExemptions;
+ const gap = domain in telemetryDomainKnownGaps;
+ expect(
+ covered || exempt || gap,
+ `${domain}: register an event with this domain, or list it in src/app/telemetry/coverage.ts`,
+ ).toBe(true);
+ expect(
+ [covered, exempt, gap].filter(Boolean).length,
+ `${domain}: a domain with events must not also appear in exemptions or known gaps`,
+ ).toBe(1);
+ }
+ });
+
+ it('references only existing domains, with non-empty reasons', () => {
+ for (const domain of eventDomains) {
+ expect(validDomains.has(domain), `event domain "${domain}"`).toBe(true);
+ }
+ for (const [domain, reason] of Object.entries(telemetryDomainExemptions)) {
+ expect(domainSet.has(domain), `exemption "${domain}"`).toBe(true);
+ expect(reason.length, `exemption "${domain}" reason`).toBeGreaterThan(0);
+ }
+ for (const [domain, reason] of Object.entries(telemetryDomainKnownGaps)) {
+ expect(domainSet.has(domain), `known gap "${domain}"`).toBe(true);
+ expect(reason.length, `known gap "${domain}" reason`).toBeGreaterThan(0);
+ }
+ const overlap = Object.keys(telemetryDomainExemptions).filter(
+ (domain) => domain in telemetryDomainKnownGaps,
+ );
+ expect(overlap, 'exemptions and known gaps must be disjoint').toEqual([]);
+ });
+});