@senad-d/observme 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.env.example +57 -0
  2. package/CHANGELOG.md +52 -0
  3. package/LICENSE +21 -0
  4. package/ObservMe-Production-Docs/00-README.md +79 -0
  5. package/ObservMe-Production-Docs/01-requirements-and-scope.md +207 -0
  6. package/ObservMe-Production-Docs/02-reference-architecture.md +306 -0
  7. package/ObservMe-Production-Docs/03-pi-event-and-session-model.md +266 -0
  8. package/ObservMe-Production-Docs/04-telemetry-semantic-conventions.md +722 -0
  9. package/ObservMe-Production-Docs/05-otel-pipeline-and-collector.md +355 -0
  10. package/ObservMe-Production-Docs/06-security-privacy-redaction.md +294 -0
  11. package/ObservMe-Production-Docs/07-extension-implementation-blueprint.md +447 -0
  12. package/ObservMe-Production-Docs/08-query-grafana-integration.md +276 -0
  13. package/ObservMe-Production-Docs/09-dashboards-alerts-slos.md +640 -0
  14. package/ObservMe-Production-Docs/10-testing-release-operations.md +319 -0
  15. package/ObservMe-Production-Docs/11-deployment-runbooks.md +203 -0
  16. package/ObservMe-Production-Docs/12-configuration-reference.md +337 -0
  17. package/ObservMe-Production-Docs/13-source-notes.md +33 -0
  18. package/ObservMe-Production-Docs/pi-session-format.md +427 -0
  19. package/README.md +356 -0
  20. package/SECURITY.md +45 -0
  21. package/dashboards/observme-agent-node-graphs.json +250 -0
  22. package/dashboards/observme-agents.json +1880 -0
  23. package/dashboards/observme-alerts.yaml +113 -0
  24. package/dashboards/observme-branches-compactions.json +1042 -0
  25. package/dashboards/observme-cost.json +1254 -0
  26. package/dashboards/observme-errors.json +1659 -0
  27. package/dashboards/observme-export-health.json +1802 -0
  28. package/dashboards/observme-latency.json +1494 -0
  29. package/dashboards/observme-llm-conversations.json +730 -0
  30. package/dashboards/observme-logs-llm.json +644 -0
  31. package/dashboards/observme-models.json +933 -0
  32. package/dashboards/observme-overview.json +2129 -0
  33. package/dashboards/observme-slo-health.json +737 -0
  34. package/dashboards/observme-slos.yaml +56 -0
  35. package/dashboards/observme-tools.json +902 -0
  36. package/dashboards/observme-trace-journey.json +1793 -0
  37. package/docs/STRUCTURE.md +49 -0
  38. package/docs/agent-subagent-observability-requirements.md +997 -0
  39. package/docs/compatibility-matrix.md +39 -0
  40. package/docs/configuration-tui-design-standard.md +767 -0
  41. package/docs/configuration.md +38 -0
  42. package/docs/review-validation.md +115 -0
  43. package/docs/validation-flow.md +117 -0
  44. package/examples/collector.yaml +123 -0
  45. package/examples/observme.yaml +131 -0
  46. package/img/demo.gif +0 -0
  47. package/img/icon.svg +47 -0
  48. package/package.json +103 -0
  49. package/src/commands/obs-agents-runtime.ts +150 -0
  50. package/src/commands/obs-agents.ts +491 -0
  51. package/src/commands/obs-args.ts +63 -0
  52. package/src/commands/obs-backfill.ts +1334 -0
  53. package/src/commands/obs-command-support.ts +43 -0
  54. package/src/commands/obs-cost.ts +228 -0
  55. package/src/commands/obs-diagnostics.ts +22 -0
  56. package/src/commands/obs-errors.ts +156 -0
  57. package/src/commands/obs-health.ts +301 -0
  58. package/src/commands/obs-link.ts +90 -0
  59. package/src/commands/obs-logs.ts +194 -0
  60. package/src/commands/obs-loki-summary.ts +184 -0
  61. package/src/commands/obs-session.ts +259 -0
  62. package/src/commands/obs-status.ts +359 -0
  63. package/src/commands/obs-tools.ts +274 -0
  64. package/src/commands/obs-trace.ts +411 -0
  65. package/src/commands/obs.ts +211 -0
  66. package/src/config/bootstrap-project-config.ts +300 -0
  67. package/src/config/defaults.ts +143 -0
  68. package/src/config/load-config.ts +631 -0
  69. package/src/config/project-paths.ts +61 -0
  70. package/src/config/schema.ts +405 -0
  71. package/src/config/validate.ts +456 -0
  72. package/src/constants.ts +4 -0
  73. package/src/diagnostics/sanitize.ts +6 -0
  74. package/src/extension.ts +38 -0
  75. package/src/otel/logs.ts +160 -0
  76. package/src/otel/metrics.ts +165 -0
  77. package/src/otel/otlp-endpoint.ts +10 -0
  78. package/src/otel/sdk.ts +114 -0
  79. package/src/otel/shutdown.ts +102 -0
  80. package/src/otel/traces.ts +166 -0
  81. package/src/pi/agent-lineage.ts +378 -0
  82. package/src/pi/agent-tree-tracker.ts +258 -0
  83. package/src/pi/event-handlers/agent-turn.ts +155 -0
  84. package/src/pi/event-handlers/lifecycle.ts +642 -0
  85. package/src/pi/event-handlers/llm.ts +115 -0
  86. package/src/pi/event-handlers/session-events.ts +159 -0
  87. package/src/pi/event-handlers/tool-bash.ts +275 -0
  88. package/src/pi/handler-internals.ts +2154 -0
  89. package/src/pi/handler-runtime.ts +633 -0
  90. package/src/pi/handler-types.ts +261 -0
  91. package/src/pi/handlers.ts +75 -0
  92. package/src/pi/subagent-spawn.ts +975 -0
  93. package/src/pi/subagent-types.ts +29 -0
  94. package/src/privacy/content-capture.ts +104 -0
  95. package/src/privacy/hash.ts +93 -0
  96. package/src/privacy/redact.ts +619 -0
  97. package/src/privacy/secret-patterns.ts +185 -0
  98. package/src/privacy/truncate.ts +69 -0
  99. package/src/query/grafana-readiness.ts +164 -0
  100. package/src/query/grafana-transport.ts +481 -0
  101. package/src/query/grafana.ts +371 -0
  102. package/src/query/loki.ts +332 -0
  103. package/src/query/prometheus.ts +388 -0
  104. package/src/query/tempo.ts +332 -0
  105. package/src/safety/sensitive-input.ts +208 -0
  106. package/src/semconv/attributes.ts +279 -0
  107. package/src/semconv/metrics.ts +146 -0
  108. package/src/semconv/spans.ts +19 -0
  109. package/src/semconv/values.ts +13 -0
  110. package/src/util/bounded-map.ts +97 -0
  111. package/tsconfig.json +15 -0
@@ -0,0 +1,447 @@
1
+ # ObservMe Extension Implementation Blueprint
2
+
3
+ ## 1. Runtime Assumptions
4
+
5
+ ObservMe is a TypeScript Pi extension loaded by Pi. It registers event handlers and commands using Pi's extension API, initializes OpenTelemetry SDK components only after `session_start`, and exports telemetry to an OTLP endpoint.
6
+
7
+ Pi extension factories may run in invocations that never start a session. Therefore ObservMe must not start background resources such as metric readers, exporter timers, sockets, or file watchers directly from the factory. The factory should register handlers and commands; session-scoped telemetry startup belongs in `session_start`, and cleanup belongs in `session_shutdown`.
8
+
9
+ ## 2. Repository Layout
10
+
11
+ ```text
12
+ observme/
13
+ ├── package.json
14
+ ├── tsconfig.json
15
+ ├── README.md
16
+ ├── src/
17
+ │ ├── index.ts # Pi extension entrypoint
18
+ │ ├── config/
19
+ │ │ ├── load-config.ts
20
+ │ │ ├── schema.ts
21
+ │ │ └── defaults.ts
22
+ │ ├── otel/
23
+ │ │ ├── sdk.ts
24
+ │ │ ├── traces.ts
25
+ │ │ ├── metrics.ts
26
+ │ │ ├── logs.ts
27
+ │ │ └── shutdown.ts
28
+ │ ├── pi/
29
+ │ │ ├── handlers.ts
30
+ │ │ ├── session.ts
31
+ │ │ ├── event-normalizer.ts
32
+ │ │ ├── agent-lineage.ts
33
+ │ │ ├── agent-tree-tracker.ts
34
+ │ │ └── turn-tracker.ts
35
+ │ ├── semconv/
36
+ │ │ ├── attributes.ts
37
+ │ │ ├── metrics.ts
38
+ │ │ └── spans.ts
39
+ │ ├── privacy/
40
+ │ │ ├── redact.ts
41
+ │ │ ├── secret-patterns.ts
42
+ │ │ ├── hash.ts
43
+ │ │ └── truncate.ts
44
+ │ ├── commands/
45
+ │ │ ├── obs-status.ts
46
+ │ │ ├── obs-health.ts
47
+ │ │ ├── obs-session.ts
48
+ │ │ ├── obs-cost.ts
49
+ │ │ ├── obs-agents.ts
50
+ │ │ └── obs-link.ts
51
+ │ ├── query/
52
+ │ │ ├── grafana.ts
53
+ │ │ ├── tempo.ts
54
+ │ │ ├── loki.ts
55
+ │ │ └── prometheus.ts
56
+ │ └── util/
57
+ │ ├── safe-json.ts
58
+ │ ├── time.ts
59
+ │ ├── trace-context.ts
60
+ │ └── bounded-map.ts
61
+ └── test/
62
+ ├── redaction.test.ts
63
+ ├── event-mapping.test.ts
64
+ ├── metrics.test.ts
65
+ ├── exporter-failure.test.ts
66
+ ├── agent-lineage.test.ts
67
+ └── cardinality.test.ts
68
+ ```
69
+
70
+ ## 3. Dependencies
71
+
72
+ Required:
73
+
74
+ ```json
75
+ {
76
+ "dependencies": {
77
+ "@opentelemetry/api": "1.x",
78
+ "@opentelemetry/api-logs": "0.x",
79
+ "@opentelemetry/sdk-node": "0.x",
80
+ "@opentelemetry/sdk-trace-node": "2.x",
81
+ "@opentelemetry/sdk-metrics": "2.x",
82
+ "@opentelemetry/sdk-logs": "0.x",
83
+ "@opentelemetry/exporter-trace-otlp-proto": "0.x",
84
+ "@opentelemetry/exporter-metrics-otlp-proto": "0.x",
85
+ "@opentelemetry/exporter-logs-otlp-proto": "0.x",
86
+ "@opentelemetry/resources": "2.x",
87
+ "@opentelemetry/semantic-conventions": "1.x"
88
+ },
89
+ "devDependencies": {
90
+ "typescript": "^5.x",
91
+ "vitest": "^latest"
92
+ }
93
+ }
94
+ ```
95
+
96
+ Pin exact versions in production and test upgrades before release. Do not assume all `@opentelemetry/*` packages share the same major version; OpenTelemetry JS commonly mixes stable API packages with SDK/exporter packages on different major/minor lines. Use the `*-otlp-proto` exporters for OTLP/HTTP protobuf per current OpenTelemetry JS docs, or the `*-otlp-grpc` exporters when using gRPC.
97
+
98
+ ## 4. Entrypoint Pattern
99
+
100
+ ```typescript
101
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
102
+ import { loadBootstrapConfig, loadSessionConfig } from "./config/load-config";
103
+ import { createRuntime } from "./runtime";
104
+ import { registerHandlers } from "./pi/handlers";
105
+ import { registerObsCommands } from "./commands";
106
+
107
+ export default async function observme(pi: ExtensionAPI) {
108
+ const bootstrapConfig = await loadBootstrapConfig();
109
+ const runtime = createRuntime(bootstrapConfig);
110
+
111
+ // Register handlers and commands in the factory, but defer OTEL SDK startup
112
+ // until session_start so background exporters/timers are session-scoped.
113
+ registerHandlers(pi, runtime, loadSessionConfig);
114
+ registerObsCommands(pi, runtime);
115
+ }
116
+ ```
117
+
118
+ ## 5. Telemetry Runtime Interface
119
+
120
+ ```typescript
121
+ export interface ObservMeTelemetry {
122
+ tracer: Tracer;
123
+ meter: Meter;
124
+ logger: Logger;
125
+ metrics: ObservMeMetrics;
126
+ spans: SpanRegistry;
127
+ agent: AgentLineageContext;
128
+ flush(timeoutMs: number): Promise<void>;
129
+ shutdown(timeoutMs: number): Promise<void>;
130
+ }
131
+
132
+ export interface AgentLineageContext {
133
+ workflowId: string;
134
+ workflowRootAgentId: string;
135
+ agentId: string;
136
+ parentAgentId?: string;
137
+ rootAgentId: string;
138
+ depth: number;
139
+ role: "root" | "subagent" | "orchestrator" | "worker" | "reviewer" | "unknown";
140
+ capability?: string;
141
+ }
142
+ ```
143
+
144
+ ## 6. Span Registry
145
+
146
+ Track active spans in memory only.
147
+
148
+ ```typescript
149
+ type SpanKey = string;
150
+
151
+ interface SpanRegistry {
152
+ sessionSpan?: Span;
153
+ activeAgentRuns: Map<string, Span>;
154
+ activeTurns: Map<string, Span>;
155
+ activeToolCalls: Map<string, Span>;
156
+ activeLlmRequests: Map<string, Span>;
157
+ activeSubagentSpawns: Map<string, Span>;
158
+ activeAgentWaits: Map<string, Span>;
159
+ activeAgentJoins: Map<string, Span>;
160
+ }
161
+ ```
162
+
163
+ Bound all maps:
164
+
165
+ ```yaml
166
+ limits:
167
+ maxActiveAgentRuns: 16
168
+ maxActiveTurns: 128
169
+ maxActiveToolCalls: 1024
170
+ maxActiveLlmRequests: 128
171
+ maxActiveSubagentSpawns: 128
172
+ maxActiveAgentWaits: 128
173
+ maxActiveAgentJoins: 128
174
+ ```
175
+
176
+ When exceeded:
177
+
178
+ - End oldest span with error status `observme.evicted=true`
179
+ - Increment `observme_telemetry_dropped_total{reason="span_registry_full"}`
180
+
181
+ ## 7. Event Handler Design
182
+
183
+ ### Session Start
184
+
185
+ ```typescript
186
+ pi.on("session_start", async (event, ctx) => {
187
+ // Load session-scoped config with ctx.cwd. Only read project-local config when
188
+ // ctx.isProjectTrusted() is true; otherwise use global/env/default config.
189
+ const config = await loadSessionConfig(ctx, runtime.bootstrapConfig);
190
+ const agent = buildAgentLineageContext(config, process.env);
191
+ const telemetry = await runtime.startSession(config, agent, ctx);
192
+
193
+ const attrs = buildSessionAttributes(event, ctx, agent);
194
+ telemetry.spans.sessionSpan = telemetry.tracer.startSpan("pi.session", { attributes: attrs });
195
+ telemetry.metrics.sessionsStarted.add(1, metricLabels(ctx, agent));
196
+ if (agent.role === "root" || agent.role === "orchestrator") {
197
+ telemetry.metrics.workflowsStarted.add(1, workflowMetricLabels(ctx, agent));
198
+ emitLog("workflow.started", attrs);
199
+ }
200
+ telemetry.metrics.activeAgents.add(1, metricLabels(ctx, agent));
201
+ emitLog("session.started", attrs);
202
+ });
203
+ ```
204
+
205
+ ### Agent Run and Turn Start
206
+
207
+ ```typescript
208
+ pi.on("agent_start", async (_event, ctx) => {
209
+ const telemetry = runtime.telemetryOrNoop();
210
+ const runId = nextAgentRunId(ctx, telemetry.agent);
211
+ const span = startChildSpan(
212
+ telemetry.spans.sessionSpan,
213
+ "pi.agent.run",
214
+ buildAgentRunAttributes(runId, ctx, telemetry.agent),
215
+ );
216
+ telemetry.spans.activeAgentRuns.set(runId, span);
217
+ telemetry.metrics.agentRuns.add(1, metricLabels(ctx, telemetry.agent));
218
+ });
219
+
220
+ pi.on("turn_start", async (event, ctx) => {
221
+ const telemetry = runtime.telemetryOrNoop();
222
+ const runSpan = currentAgentRunSpan(ctx) ?? telemetry.spans.sessionSpan;
223
+ const turnId = getTurnId(event, ctx, currentAgentRunId(ctx));
224
+ const span = startChildSpan(runSpan, "pi.turn", buildTurnAttributes(event, ctx, telemetry.agent));
225
+ telemetry.spans.activeTurns.set(turnId, span);
226
+ telemetry.metrics.turnsStarted.add(1, metricLabels(ctx, telemetry.agent));
227
+ });
228
+
229
+ pi.on("turn_end", async (event, ctx) => {
230
+ const telemetry = runtime.telemetryOrNoop();
231
+ const turnId = getTurnId(event, ctx, currentAgentRunId(ctx));
232
+ const span = telemetry.spans.activeTurns.get(turnId);
233
+ span?.setAttributes(buildTurnEndAttributes(event, ctx));
234
+ span?.end();
235
+ telemetry.spans.activeTurns.delete(turnId);
236
+ });
237
+
238
+ pi.on("agent_end", async (_event, ctx) => {
239
+ const telemetry = runtime.telemetryOrNoop();
240
+ const runId = currentAgentRunId(ctx);
241
+ const span = telemetry.spans.activeAgentRuns.get(runId);
242
+ span?.end();
243
+ telemetry.spans.activeAgentRuns.delete(runId);
244
+ });
245
+ ```
246
+
247
+ ### Provider Request
248
+
249
+ ```typescript
250
+ pi.on("before_provider_request", async (event, ctx) => {
251
+ const telemetry = runtime.telemetryOrNoop();
252
+ const turnId = currentTurnId(ctx);
253
+ const parent = telemetry.spans.activeTurns.get(turnId) ?? currentAgentRunSpan(ctx) ?? telemetry.spans.sessionSpan;
254
+ // Pi's event exposes the provider-specific payload; use ctx.model plus safe payload inspection.
255
+ const span = startChildSpan(parent, "pi.llm.request", buildLlmRequestAttrs(event.payload, ctx.model, ctx));
256
+ telemetry.spans.activeLlmRequests.set(makeLlmKey(event, ctx), span);
257
+ });
258
+ ```
259
+
260
+ ### Tool Execution Start and Tool Call Metadata
261
+
262
+ ```typescript
263
+ pi.on("tool_execution_start", async (event, ctx) => {
264
+ const telemetry = runtime.telemetryOrNoop();
265
+ const parent = currentTurnSpan(ctx) ?? currentAgentRunSpan(ctx) ?? telemetry.spans.sessionSpan;
266
+ const span = startChildSpan(parent, "pi.tool.call", buildToolExecutionAttrs(event, ctx, telemetry.agent));
267
+ telemetry.spans.activeToolCalls.set(event.toolCallId, span);
268
+ telemetry.metrics.toolCalls.add(1, { tool_name: safeToolName(event.toolName), agent_role: telemetry.agent.role });
269
+ });
270
+
271
+ pi.on("tool_call", async (event, ctx) => {
272
+ const telemetry = runtime.telemetryOrNoop();
273
+ const span = telemetry.spans.activeToolCalls.get(event.toolCallId);
274
+ span?.setAttributes(buildToolCallInputAttrs(event, ctx));
275
+ });
276
+
277
+ pi.on("tool_execution_end", async (event, ctx) => {
278
+ const telemetry = runtime.telemetryOrNoop();
279
+ const span = telemetry.spans.activeToolCalls.get(event.toolCallId);
280
+ span?.setAttributes(buildToolResultAttrs(event, ctx));
281
+ if (event.isError) span?.setStatus({ code: SpanStatusCode.ERROR, message: safeErrorClass(event.result) });
282
+ span?.end();
283
+ telemetry.spans.activeToolCalls.delete(event.toolCallId);
284
+ });
285
+ ```
286
+
287
+ ### Subagent Spawn
288
+
289
+ There is no dedicated Pi event for "subagent spawned". Detect this at the point ObservMe wraps a subagent tool/command or an extension intentionally launches another Pi process.
290
+
291
+ ```typescript
292
+ async function runSubagent(command: string, args: string[], ctx: ExtensionContext) {
293
+ const telemetry = runtime.telemetryOrNoop();
294
+ const spawnId = newSpawnId();
295
+ const parent = currentTurnSpan(ctx) ?? currentAgentRunSpan(ctx) ?? telemetry.spans.sessionSpan;
296
+ const span = startChildSpan(parent, "pi.agent.spawn", buildSpawnAttrs(spawnId, ctx, telemetry.agent));
297
+
298
+ const env = buildChildEnv(process.env, telemetry.agent, span.spanContext(), spawnId);
299
+ recordAgentTreeSpawn(telemetry, spawnId, ctx);
300
+ try {
301
+ telemetry.metrics.subagentsSpawned.add(1, { agent_role: "subagent", spawn_type: "command" });
302
+ // Use a child_process/spawn helper or tool operation that supports explicit env.
303
+ // pi.exec() is useful for simple commands but does not currently accept env.
304
+ return await spawnProcessWithEnv(command, args, { env, signal: ctx.signal });
305
+ } catch (err) {
306
+ span.setStatus({ code: SpanStatusCode.ERROR, message: safeErrorClass(err) });
307
+ telemetry.metrics.subagentSpawnFailures.add(1, { spawn_type: "command", error_class: safeErrorClass(err) });
308
+ throw err;
309
+ } finally {
310
+ span.end();
311
+ }
312
+ }
313
+ ```
314
+
315
+ Propagate W3C `traceparent`/`tracestate`, `OBSERVME_WORKFLOW_ID`, `OBSERVME_PARENT_AGENT_ID`, `OBSERVME_ROOT_AGENT_ID`, `OBSERVME_PARENT_SESSION_ID`, `OBSERVME_AGENT_DEPTH`, and `OBSERVME_SPAWN_ID`. Do not place raw command lines or inherited environment values into telemetry.
316
+
317
+ When the parent waits for a child or receives child results, create `pi.agent.wait` and `pi.agent.join` spans/events with child status, propagated-failure status, active-child count, and join status. These spans make the critical path visible in orchestrator traces.
318
+
319
+ ### Session Shutdown
320
+
321
+ ```typescript
322
+ pi.on("session_shutdown", async (_event, _ctx) => {
323
+ const telemetry = runtime.telemetryOrNoop();
324
+ endAllActiveSpans(telemetry.spans);
325
+ telemetry.spans.sessionSpan?.end();
326
+ await withTimeout(runtime.shutdownSession(), runtime.config.shutdown.flushTimeoutMs);
327
+ });
328
+ ```
329
+
330
+ ## 8. Handling Assistant Messages
331
+
332
+ Assistant messages are important because finalized `message_end` events contain provider/model metadata, usage, stop reason, diagnostics, tool calls, and cost. ObservMe should update and end the active GenAI span from the finalized assistant message, not from partial streaming data alone.
333
+
334
+ Pseudo-flow:
335
+
336
+ ```typescript
337
+ function onAssistantMessage(message: AssistantMessage, context: PiContext) {
338
+ const span = findCurrentLlmSpan(context);
339
+ span?.setAttributes(extractUsageAttributes(message.usage));
340
+ span?.setAttribute("pi.llm.stop_reason", message.stopReason);
341
+ span?.setAttribute("gen_ai.response.finish_reasons", [mapStopReason(message.stopReason)]);
342
+
343
+ metrics.inputTokens.add(message.usage.input, labels);
344
+ metrics.outputTokens.add(message.usage.output, labels);
345
+ metrics.cacheReadTokens.add(message.usage.cacheRead, labels);
346
+ metrics.cacheWriteTokens.add(message.usage.cacheWrite, labels);
347
+ if (message.usage.cacheWrite1h !== undefined) metrics.cacheWrite1hTokens.add(message.usage.cacheWrite1h, labels);
348
+ if (message.usage.reasoning !== undefined) metrics.reasoningTokens.add(message.usage.reasoning, labels);
349
+ metrics.totalCost.add(message.usage.cost.total, labels);
350
+
351
+ if (message.stopReason === "error") {
352
+ span?.setAttribute("error.type", classifyError(message.errorMessage));
353
+ span?.setAttribute("pi.llm.error_message_hash", hashError(message.errorMessage));
354
+ span?.setStatus({ code: SpanStatusCode.ERROR });
355
+ metrics.llmErrors.add(1, labels);
356
+ }
357
+
358
+ span?.end();
359
+ }
360
+ ```
361
+
362
+ ## 9. Logs
363
+
364
+ Use OTEL logs for structured event streams.
365
+
366
+ ```typescript
367
+ logger.emit({
368
+ severityText: "INFO",
369
+ body: "tool.call.completed",
370
+ attributes: {
371
+ "event.name": "tool.call.completed",
372
+ "pi.session.id": sessionId,
373
+ "pi.agent.id": agentId,
374
+ "pi.agent.run.id": agentRunId,
375
+ "pi.turn.id": turnId,
376
+ "pi.tool.name": toolName,
377
+ "pi.tool.success": success,
378
+ },
379
+ });
380
+ ```
381
+
382
+ ## 10. Error Handling
383
+
384
+ All handlers must catch errors.
385
+
386
+ ```typescript
387
+ function safeHandler(name: string, fn: Handler): Handler {
388
+ return async (event, ctx) => {
389
+ try {
390
+ await fn(event, ctx);
391
+ } catch (err) {
392
+ recordInternalError(name, err);
393
+ }
394
+ };
395
+ }
396
+ ```
397
+
398
+ Never throw from an ObservMe event handler into Pi.
399
+
400
+ ## 11. Config Loading Order
401
+
402
+ 1. Built-in defaults
403
+ 2. Global config `~/.pi/agent/observme.yaml`
404
+ 3. Project config `<cwd>/<CONFIG_DIR_NAME>/observme.yaml` only when `ctx.isProjectTrusted()` is true (normally `.pi/observme.yaml`; use Pi's exported `CONFIG_DIR_NAME` instead of hardcoding `.pi`)
405
+ 4. Environment variables
406
+ 5. CLI or Pi extension options if available
407
+
408
+ Factory-safe config loading may read only defaults, global config, and environment variables. Session-scoped loading on `session_start` may add trusted project config because it has access to `ctx.cwd` and `ctx.isProjectTrusted()`. Reapply environment/runtime overrides after project config so the precedence order remains correct.
409
+
410
+ ## 12. Backfill Command
411
+
412
+ Backfill is optional and disabled by default.
413
+
414
+ Command:
415
+
416
+ ```text
417
+ /obs backfill --current-session --since 1h
418
+ ```
419
+
420
+ Rules:
421
+
422
+ - Mark telemetry with `observme.replayed=true`
423
+ - Do not backfill content unless capture settings allow it
424
+ - Rate limit export
425
+ - Confirm with user before sending historical content
426
+
427
+ ## 13. Versioning
428
+
429
+ Extension versioning:
430
+
431
+ ```text
432
+ MAJOR.MINOR.PATCH
433
+ ```
434
+
435
+ Breaking semconv changes require MAJOR.
436
+
437
+ ## 14. Build Artifact
438
+
439
+ Recommended output:
440
+
441
+ ```text
442
+ dist/observme.js
443
+ dist/observme.d.ts
444
+ examples/observme.yaml
445
+ examples/collector.yaml
446
+ dashboards/*.json
447
+ ```