@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,642 @@
1
+ import { open } from "node:fs/promises";
2
+ import { SpanStatusCode } from "@opentelemetry/api";
3
+ import {
4
+ clearObsAgentsRuntimeState,
5
+ startObsAgentsRuntimeState,
6
+ } from "../../commands/obs-agents-runtime.ts";
7
+ import {
8
+ clearObsSessionRuntimeState,
9
+ startObsSessionRuntimeState,
10
+ } from "../../commands/obs-session.ts";
11
+ import {
12
+ clearObsStatusExportError,
13
+ recordObsStatusExportResult,
14
+ updateObsStatusRuntimeState,
15
+ } from "../../commands/obs-status.ts";
16
+ import { bootstrapProjectObservMeConfig } from "../../config/bootstrap-project-config.ts";
17
+ import type {
18
+ LoadSessionConfigResult,
19
+ SessionConfigDiagnostics,
20
+ } from "../../config/load-config.ts";
21
+ import {
22
+ loadSessionConfig,
23
+ loadSessionConfigWithDiagnostics,
24
+ } from "../../config/load-config.ts";
25
+ import type { ObservMeConfig } from "../../config/schema.ts";
26
+ import { emitUnsafeCaptureWarning, normalizeConfigRejectionDiagnostic } from "../../config/validate.ts";
27
+ import { EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE } from "../../constants.ts";
28
+ import type { BoundedOtelOperationResult } from "../../otel/shutdown.ts";
29
+ import {
30
+ AGENT_LINEAGE_ATTRIBUTES,
31
+ COMMON_SPAN_ATTRIBUTES,
32
+ CONFIG_ATTRIBUTES,
33
+ LOG_ATTRIBUTES,
34
+ SESSION_ATTRIBUTES,
35
+ WORKFLOW_ATTRIBUTES,
36
+ } from "../../semconv/attributes.ts";
37
+ import { LOG_EVENT_NAMES } from "../../semconv/metrics.ts";
38
+ import { SPAN_NAMES } from "../../semconv/spans.ts";
39
+ import { createAgentLineageContext } from "../agent-lineage.ts";
40
+ import {
41
+ buildCommonSessionSpanAttributes,
42
+ buildLineageMetricSafeLogAttributes,
43
+ emitLifecycleLog,
44
+ emitStructuredLog,
45
+ endActiveSpan,
46
+ endAllActiveSpans,
47
+ errorClass,
48
+ hashValue,
49
+ isMissingFileError,
50
+ isRecord,
51
+ metricLabels,
52
+ normalizeMetricValue,
53
+ readBoolean,
54
+ readInteger,
55
+ readSpanId,
56
+ readSpanTraceId,
57
+ readString,
58
+ readUnknown,
59
+ resolveModelId,
60
+ resolveModelProvider,
61
+ resolveSessionFilePath,
62
+ resolveSessionId,
63
+ resolveSessionTraceParent,
64
+ resolveThinkingLevel,
65
+ startActiveRootSpan,
66
+ withoutUndefinedAttributes,
67
+ } from "../handler-internals.ts";
68
+ import {
69
+ isRootWorkflow,
70
+ monotonicNowMs,
71
+ startSessionTelemetry,
72
+ workflowFailed,
73
+ } from "../handler-runtime.ts";
74
+ import type { HandlerRegistrar, SerializedLifecycleQueue } from "../handler-runtime.ts";
75
+ import type {
76
+ AttributeMap,
77
+ Handler,
78
+ HandlerSessionState,
79
+ LoadSessionConfig,
80
+ MinimalSessionCorrelation,
81
+ ObservMeHandlerContext,
82
+ ObservMeTelemetrySession,
83
+ RegisterHandlersOptions,
84
+ SessionConfigLoadResult,
85
+ SessionRecoveryHeader,
86
+ StartSessionTelemetry,
87
+ StartupRecoveryState,
88
+ } from "../handler-types.ts";
89
+
90
+ export function registerLifecycleHandlers(
91
+ registrar: HandlerRegistrar,
92
+ state: HandlerSessionState,
93
+ options: RegisterHandlersOptions,
94
+ lifecycleQueue: SerializedLifecycleQueue,
95
+ ): void {
96
+ const loadConfigFn = options.loadConfig ?? loadSessionConfig;
97
+ const startTelemetryFn = options.startTelemetry ?? startSessionTelemetry;
98
+ const startHandler = createSessionStartHandler(state, loadConfigFn, startTelemetryFn, options);
99
+ const shutdownHandler = createSessionShutdownHandler(state);
100
+
101
+ registrar.add("session_start", lifecycleQueue.wrap(startHandler));
102
+ registrar.add("session_shutdown", lifecycleQueue.wrap(shutdownHandler));
103
+ }
104
+
105
+ export function buildSessionAttributes(
106
+ event: unknown,
107
+ ctx: ObservMeHandlerContext,
108
+ config: ObservMeConfig,
109
+ lineage: ObservMeTelemetrySession["lineage"],
110
+ recovery?: StartupRecoveryState,
111
+ ): AttributeMap {
112
+ const cwd = recovery?.header?.cwd ?? readString(ctx, "cwd") ?? process.cwd();
113
+ const sessionId = recovery?.header?.id ?? resolveSessionId(event, ctx, lineage);
114
+ const parentSessionId = recovery?.header?.parentSession ?? readString(event, "parentSessionId") ?? lineage.parentSessionId;
115
+ const sessionFile = recovery?.sessionFile ?? resolveSessionFilePath(event, ctx);
116
+
117
+ return withoutUndefinedAttributes({
118
+ [SESSION_ATTRIBUTES.PI_SESSION_ID]: sessionId,
119
+ [SESSION_ATTRIBUTES.PI_SESSION_NAME]: readString(event, "sessionName") ?? readString(event, "name") ?? "unknown",
120
+ [SESSION_ATTRIBUTES.PI_SESSION_CWD_HASH]: hashValue(cwd, config),
121
+ [SESSION_ATTRIBUTES.PI_SESSION_PARENT_SESSION_HASH]: parentSessionId ? hashValue(parentSessionId, config) : "",
122
+ [SESSION_ATTRIBUTES.PI_SESSION_PERSISTED]: readBoolean(event, "persisted") ?? recovery?.resumed ?? false,
123
+ [SESSION_ATTRIBUTES.PI_SESSION_FILE_HASH]: sessionFile ? hashValue(sessionFile, config) : "",
124
+ [SESSION_ATTRIBUTES.PI_SESSION_VERSION]: readString(recovery?.header, "version") ?? readString(event, "sessionVersion") ?? readString(event, "version") ?? "unknown",
125
+ [SESSION_ATTRIBUTES.PI_MODEL_PROVIDER_CURRENT]: resolveModelProvider(event, ctx),
126
+ [SESSION_ATTRIBUTES.PI_MODEL_ID_CURRENT]: resolveModelId(event, ctx),
127
+ [SESSION_ATTRIBUTES.PI_THINKING_LEVEL_CURRENT]: resolveThinkingLevel(event, ctx),
128
+ ...buildCommonSessionSpanAttributes(sessionId, config, lineage),
129
+ });
130
+ }
131
+
132
+ export async function readSessionHeaderFromFile(sessionFile: string): Promise<SessionRecoveryHeader | undefined> {
133
+ let file;
134
+
135
+ try {
136
+ file = await open(sessionFile, "r");
137
+ const buffer = Buffer.alloc(65_536);
138
+ const result = await file.read(buffer, 0, buffer.length, 0);
139
+ const firstLine = buffer.subarray(0, result.bytesRead).toString("utf8").split(/\r?\n/u)[0]?.trim();
140
+ if (!firstLine) return undefined;
141
+
142
+ return normalizeSessionHeader(JSON.parse(firstLine));
143
+ } catch (error) {
144
+ if (isMissingFileError(error) || error instanceof SyntaxError) return undefined;
145
+ throw error;
146
+ } finally {
147
+ await file?.close();
148
+ }
149
+ }
150
+
151
+ function createSessionStartHandler(
152
+ state: HandlerSessionState,
153
+ loadConfigFn: LoadSessionConfig,
154
+ startTelemetryFn: StartSessionTelemetry,
155
+ options: RegisterHandlersOptions,
156
+ ): Handler {
157
+ return handleSessionStart.bind(undefined, state, loadConfigFn, startTelemetryFn, options);
158
+ }
159
+
160
+ async function handleSessionStart(
161
+ state: HandlerSessionState,
162
+ loadConfigFn: LoadSessionConfig,
163
+ startTelemetryFn: StartSessionTelemetry,
164
+ options: RegisterHandlersOptions,
165
+ event: unknown,
166
+ ctx: ObservMeHandlerContext,
167
+ ): Promise<void> {
168
+ const previousSession = state.session;
169
+ if (previousSession) await shutDownPreviousSessionBeforeDuplicateStart(previousSession, ctx, state);
170
+
171
+ await ensureProjectConfigForHandler(options, ctx);
172
+ const loadedConfig = await loadSessionConfigForHandler(loadConfigFn, options, ctx);
173
+ const config = loadedConfig.config;
174
+ await emitUnsafeCaptureWarning(config, ctx);
175
+
176
+ const recovery = await resolveStartupRecovery(event, ctx, config, options);
177
+ const recoveryCorrelation = recovery.customCorrelation ?? recovery.header?.correlation;
178
+ const lineage = createAgentLineageContext({
179
+ config,
180
+ env: buildRecoveryLineageEnv(config, recoveryCorrelation, options.env),
181
+ trustedParentContext: options.trustedParentContext === true || recoveryCorrelation !== undefined,
182
+ requireCompletePropagationEnvelope:
183
+ options.requireCompleteParentEnvelope ?? (options.trustedParentContext === true && recoveryCorrelation === undefined),
184
+ failOpenInvalidPropagation: true,
185
+ });
186
+ const session = await startTelemetryFn({ config, lineage, now: options.now });
187
+ session.now = options.now ?? session.now ?? monotonicNowMs;
188
+ updateObsStatusRuntimeState({ config: session.config, configDiagnostics: loadedConfig.diagnostics });
189
+ clearObsStatusExportError();
190
+ state.session = session;
191
+ const attributes = buildSessionAttributes(event, ctx, session.config, lineage, recovery);
192
+ const labels = metricLabels(session.config, lineage);
193
+
194
+ session.sessionAttributes = attributes;
195
+ const traceParent = resolveSessionTraceParent(lineage);
196
+ session.sessionSpan = startActiveRootSpan(session, SPAN_NAMES.PI_SESSION, attributes, "session", traceParent);
197
+ emitConfigRejectionDiagnostic(session, loadedConfig.diagnostics, ctx);
198
+ recordSessionTracePropagationFailure(session, traceParent);
199
+ startObsSessionRuntimeState({
200
+ sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
201
+ traceId: readSpanTraceId(session.sessionSpan),
202
+ traceUrlTemplate: session.config.query.links.traceUrlTemplate,
203
+ });
204
+ startObsAgentsRuntimeState({
205
+ lineage,
206
+ agentTree: session.agentTree,
207
+ sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
208
+ traceId: readSpanTraceId(session.sessionSpan),
209
+ });
210
+ session.workflowStartedAtMs = session.now();
211
+ session.metrics.sessionsStarted.add(1, labels);
212
+ session.metrics.activeAgents.add(1, labels);
213
+ session.activeAgentRecorded = true;
214
+ session.sessionSpan.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, attributes);
215
+ emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, attributes);
216
+ if (recovery.resumed && session.config.replayOnStart) emitStartupReplayTelemetry(session, attributes);
217
+
218
+ if (isRootWorkflow(lineage)) {
219
+ session.metrics.workflowsStarted.add(1, labels);
220
+ emitLifecycleLog(session.logger, LOG_EVENT_NAMES.WORKFLOW_STARTED, attributes);
221
+ }
222
+
223
+ await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE);
224
+ state.session = session;
225
+ }
226
+
227
+ function createSessionShutdownHandler(state: HandlerSessionState): Handler {
228
+ return handleSessionShutdown.bind(undefined, state);
229
+ }
230
+
231
+ async function handleSessionShutdown(
232
+ state: HandlerSessionState,
233
+ event: unknown,
234
+ ctx: ObservMeHandlerContext,
235
+ ): Promise<void> {
236
+ const session = state.session;
237
+ if (!session) return;
238
+
239
+ await shutDownTelemetrySession(session, event, ctx, state);
240
+ }
241
+
242
+ async function resolveStartupRecovery(
243
+ event: unknown,
244
+ ctx: ObservMeHandlerContext,
245
+ config: ObservMeConfig,
246
+ options: RegisterHandlersOptions,
247
+ ): Promise<StartupRecoveryState> {
248
+ const sessionFile = resolveSessionFilePath(event, ctx);
249
+ const readHeader = options.readSessionHeader ?? readSessionHeaderFromFile;
250
+ const header = sessionFile ? await readHeader(sessionFile) : undefined;
251
+ const customCorrelation = config.agent.writeCorrelationEntry ? readExplicitCustomCorrelation(event) : undefined;
252
+
253
+ return {
254
+ resumed: isExistingSessionStart(event),
255
+ sessionFile,
256
+ header,
257
+ customCorrelation,
258
+ };
259
+ }
260
+
261
+ function emitStartupReplayTelemetry(session: ObservMeTelemetrySession, attributes: AttributeMap): void {
262
+ const replayAttributes = {
263
+ ...attributes,
264
+ [COMMON_SPAN_ATTRIBUTES.OBSERVME_REPLAYED]: true,
265
+ };
266
+
267
+ session.sessionSpan?.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, replayAttributes);
268
+ emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, replayAttributes);
269
+ }
270
+
271
+ function buildRecoveryLineageEnv(
272
+ config: ObservMeConfig,
273
+ correlation: MinimalSessionCorrelation | undefined,
274
+ env: NodeJS.ProcessEnv = process.env,
275
+ ): NodeJS.ProcessEnv {
276
+ if (!correlation) return env;
277
+
278
+ return {
279
+ ...env,
280
+ ...definedEnvValue(config.workflow.idEnv, correlation.workflowId),
281
+ ...definedEnvValue(config.agent.idEnv, correlation.agentId),
282
+ ...definedEnvValue(config.agent.parentIdEnv, correlation.parentAgentId),
283
+ ...definedEnvValue(config.agent.rootIdEnv, correlation.rootAgentId),
284
+ ...definedEnvValue(config.agent.parentSessionIdEnv, correlation.parentSessionId),
285
+ ...definedEnvValue(config.agent.depthEnv, correlation.depth === undefined ? undefined : String(correlation.depth)),
286
+ ...definedEnvValue(config.agent.spawnIdEnv, correlation.spawnId),
287
+ ...definedEnvValue(config.agent.capabilityEnv, correlation.capability),
288
+ };
289
+ }
290
+
291
+ function definedEnvValue(name: string, value: string | undefined): NodeJS.ProcessEnv {
292
+ return value === undefined || value === "" ? {} : { [name]: value };
293
+ }
294
+
295
+ function readExplicitCustomCorrelation(event: unknown): MinimalSessionCorrelation | undefined {
296
+ const value = readUnknown(event, "customCorrelation") ?? readUnknown(event, "observmeCorrelation");
297
+ return normalizeMinimalCorrelation(value);
298
+ }
299
+
300
+ function normalizeSessionHeader(value: unknown): SessionRecoveryHeader | undefined {
301
+ if (!isRecord(value) || readString(value, "type") !== "session") return undefined;
302
+
303
+ return withoutUndefinedObjectValues({
304
+ type: "session",
305
+ version: readString(value, "version") ?? readInteger(value, "version"),
306
+ id: readString(value, "id"),
307
+ timestamp: readString(value, "timestamp"),
308
+ cwd: readString(value, "cwd"),
309
+ parentSession: readString(value, "parentSession"),
310
+ correlation: normalizeMinimalCorrelation(readUnknown(value, "observmeCorrelation") ?? readUnknown(value, "correlation")),
311
+ });
312
+ }
313
+
314
+ function normalizeMinimalCorrelation(value: unknown): MinimalSessionCorrelation | undefined {
315
+ if (!isRecord(value)) return undefined;
316
+
317
+ const correlation = withoutUndefinedObjectValues({
318
+ workflowId: readString(value, "workflowId"),
319
+ agentId: readString(value, "agentId"),
320
+ parentAgentId: readString(value, "parentAgentId"),
321
+ rootAgentId: readString(value, "rootAgentId"),
322
+ parentSessionId: readString(value, "parentSessionId"),
323
+ depth: readInteger(value, "depth"),
324
+ spawnId: readString(value, "spawnId"),
325
+ capability: readString(value, "capability"),
326
+ });
327
+
328
+ return Object.keys(correlation).length === 0 ? undefined : correlation;
329
+ }
330
+
331
+ function withoutUndefinedObjectValues<T extends Record<string, unknown>>(value: T): T {
332
+ const definedEntries: Array<[string, unknown]> = [];
333
+ for (const entry of Object.entries(value)) {
334
+ if (entry[1] !== undefined) definedEntries.push(entry);
335
+ }
336
+ return Object.fromEntries(definedEntries) as T;
337
+ }
338
+
339
+ function isExistingSessionStart(event: unknown): boolean {
340
+ const reason = readString(event, "reason");
341
+ return reason === "resume" || reason === "reload" || readBoolean(event, "resumed") === true || readBoolean(event, "existingSession") === true;
342
+ }
343
+
344
+ async function loadSessionConfigForHandler(
345
+ loadConfigFn: LoadSessionConfig,
346
+ options: RegisterHandlersOptions,
347
+ ctx: ObservMeHandlerContext,
348
+ ): Promise<SessionConfigLoadResult> {
349
+ const loadOptions = { ctx, cwd: ctx.cwd, configDirName: options.configDirName, env: options.env };
350
+
351
+ if (!options.loadConfig) return loadSessionConfigWithDiagnostics(loadOptions);
352
+
353
+ const loaded = await loadConfigFn(loadOptions);
354
+ return isLoadSessionConfigResult(loaded)
355
+ ? normalizeLoadSessionConfigResult(loaded)
356
+ : { config: loaded, diagnostics: undefined };
357
+ }
358
+
359
+ function isLoadSessionConfigResult(value: ObservMeConfig | LoadSessionConfigResult): value is LoadSessionConfigResult {
360
+ return isRecord(value) && isRecord(value.config) && isRecord(value.diagnostics);
361
+ }
362
+
363
+ function normalizeLoadSessionConfigResult(loaded: LoadSessionConfigResult): LoadSessionConfigResult {
364
+ if (!loaded.diagnostics.rejection) return loaded;
365
+
366
+ return {
367
+ config: loaded.config,
368
+ diagnostics: {
369
+ ...loaded.diagnostics,
370
+ rejection: normalizeConfigRejectionDiagnostic(loaded.diagnostics.rejection),
371
+ },
372
+ };
373
+ }
374
+
375
+ async function ensureProjectConfigForHandler(
376
+ options: RegisterHandlersOptions,
377
+ ctx: ObservMeHandlerContext,
378
+ ): Promise<void> {
379
+ // Pi emits session_start for startup, reload, new, resume, and fork flows. ObservMe keeps
380
+ // bootstrap idempotent across all of them: create once for trusted projects, then never overwrite.
381
+ await bootstrapProjectObservMeConfig(ctx, {
382
+ configDirName: options.configDirName,
383
+ ensureProjectConfig: options.ensureProjectConfig,
384
+ });
385
+ }
386
+
387
+ async function shutDownPreviousSessionBeforeDuplicateStart(
388
+ session: ObservMeTelemetrySession,
389
+ ctx: ObservMeHandlerContext,
390
+ state: HandlerSessionState,
391
+ ): Promise<void> {
392
+ emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_DUPLICATE_START, buildDuplicateSessionStartAttributes(session));
393
+
394
+ try {
395
+ await shutDownTelemetrySession(session, duplicateSessionStartShutdownEvent(), ctx, state);
396
+ } catch (error) {
397
+ recordDuplicateSessionStartShutdownError(session, error);
398
+ clearObsSessionRuntimeState();
399
+ clearObsAgentsRuntimeState();
400
+ state.session = undefined;
401
+ }
402
+ }
403
+
404
+ function buildDuplicateSessionStartAttributes(session: ObservMeTelemetrySession): AttributeMap {
405
+ return withoutUndefinedAttributes({
406
+ [LOG_ATTRIBUTES.PI_SESSION_ID]: readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
407
+ [LOG_ATTRIBUTES.PI_WORKFLOW_ID]: session.lineage.workflowId,
408
+ [LOG_ATTRIBUTES.PI_WORKFLOW_ROOT_AGENT_ID]: session.lineage.workflowRootAgentId,
409
+ [LOG_ATTRIBUTES.PI_AGENT_ID]: session.lineage.agentId,
410
+ [LOG_ATTRIBUTES.PI_AGENT_ROOT_ID]: session.lineage.rootAgentId,
411
+ reason: "active_session_replaced_before_new_start",
412
+ });
413
+ }
414
+
415
+ function duplicateSessionStartShutdownEvent(): Record<string, unknown> {
416
+ return {
417
+ duplicateSessionStart: true,
418
+ status: "ok",
419
+ };
420
+ }
421
+
422
+ function recordDuplicateSessionStartShutdownError(session: ObservMeTelemetrySession, error: unknown): void {
423
+ session.metrics.handlerErrors.add(1, { operation: "session_start.duplicate_shutdown" });
424
+ emitLifecycleLog(
425
+ session.logger,
426
+ LOG_EVENT_NAMES.HANDLER_FAILED,
427
+ handlerErrorAttributes("session_start.duplicate_shutdown", error),
428
+ "ERROR",
429
+ );
430
+ }
431
+
432
+ async function shutDownTelemetrySession(
433
+ session: ObservMeTelemetrySession,
434
+ event: unknown,
435
+ ctx: ObservMeHandlerContext,
436
+ state: HandlerSessionState,
437
+ ): Promise<void> {
438
+ const labels = metricLabels(session.config, session.lineage);
439
+ const shutdownAttributes = buildShutdownAttributes(event, session);
440
+ const failed = workflowFailed(event);
441
+
442
+ session.metrics.sessionsShutdown.add(1, labels);
443
+ if (session.activeAgentRecorded) session.metrics.activeAgents.add(-1, labels);
444
+ recordWorkflowShutdownTelemetry(session, shutdownAttributes, failed, labels);
445
+ endAllActiveSpans(session);
446
+ session.sessionSpan?.addEvent(LOG_EVENT_NAMES.SESSION_SHUTDOWN, shutdownAttributes);
447
+ if (failed) session.sessionSpan?.setStatus({ code: SpanStatusCode.ERROR });
448
+ endActiveSpan(session, session.sessionSpan);
449
+ await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, undefined);
450
+ await recordControllerOperationResult(session, "flush");
451
+ await recordControllerOperationResult(session, "shutdown");
452
+ clearObsSessionRuntimeState();
453
+ clearObsAgentsRuntimeState();
454
+ state.session = undefined;
455
+ }
456
+
457
+ async function recordControllerOperationResult(
458
+ session: ObservMeTelemetrySession,
459
+ operation: BoundedOtelOperationResult["operation"],
460
+ ): Promise<void> {
461
+ const result = await runControllerOperation(session, operation);
462
+ recordObsStatusExportResult(result);
463
+ recordExportOperationResult(session, result);
464
+ }
465
+
466
+ async function runControllerOperation(
467
+ session: ObservMeTelemetrySession,
468
+ operation: BoundedOtelOperationResult["operation"],
469
+ ): Promise<BoundedOtelOperationResult> {
470
+ try {
471
+ return await session.controller[operation](session.config.shutdown.flushTimeoutMs);
472
+ } catch (error) {
473
+ return { operation, completed: false, timedOut: false, error };
474
+ }
475
+ }
476
+
477
+ function recordSessionTracePropagationFailure(
478
+ session: ObservMeTelemetrySession,
479
+ resolution: ReturnType<typeof resolveSessionTraceParent>,
480
+ ): void {
481
+ if (!resolution.failureReason) return;
482
+
483
+ const linkedContext = resolution.links?.[0]?.context;
484
+ const attributes = withoutUndefinedAttributes({
485
+ [LOG_ATTRIBUTES.EVENT_NAME]: LOG_EVENT_NAMES.TRACE_CONTEXT_PROPAGATION_FAILED,
486
+ [LOG_ATTRIBUTES.EVENT_CATEGORY]: "agent-tree",
487
+ [LOG_ATTRIBUTES.PI_WORKFLOW_ID]: session.lineage.workflowId,
488
+ [LOG_ATTRIBUTES.PI_AGENT_ID]: session.lineage.agentId,
489
+ [LOG_ATTRIBUTES.PI_AGENT_ROOT_ID]: session.lineage.rootAgentId,
490
+ [AGENT_LINEAGE_ATTRIBUTES.PI_AGENT_ORPHANED]: session.lineage.orphaned ? true : undefined,
491
+ [LOG_ATTRIBUTES.TRACE_ID]: linkedContext?.traceId,
492
+ [LOG_ATTRIBUTES.SPAN_ID]: linkedContext?.spanId,
493
+ [LOG_ATTRIBUTES.ERROR_TYPE]: resolution.failureReason,
494
+ });
495
+ const labels = {
496
+ agent_role: session.lineage.role,
497
+ subagent_depth: String(Math.max(0, Math.min(session.lineage.depth, session.config.workflow.maxDepthWarning))),
498
+ reason: "trace_context_fallback",
499
+ };
500
+
501
+ session.metrics.traceContextPropagationFailures.add(1, labels);
502
+ session.sessionSpan?.addEvent(LOG_EVENT_NAMES.TRACE_CONTEXT_PROPAGATION_FAILED, attributes);
503
+ emitStructuredLog(session.logger, LOG_EVENT_NAMES.TRACE_CONTEXT_PROPAGATION_FAILED, "agent-tree", attributes, "ERROR");
504
+ if (!session.lineage.orphaned) return;
505
+
506
+ session.metrics.orphanAgents.add(1, { status: "orphaned", reason: "orphaned" });
507
+ session.sessionSpan?.addEvent(LOG_EVENT_NAMES.AGENT_ORPHANED, attributes);
508
+ emitStructuredLog(session.logger, LOG_EVENT_NAMES.AGENT_ORPHANED, "agent-tree", attributes, "ERROR");
509
+ }
510
+
511
+ function emitConfigRejectionDiagnostic(
512
+ session: ObservMeTelemetrySession,
513
+ diagnostics: SessionConfigDiagnostics | undefined,
514
+ ctx: ObservMeHandlerContext,
515
+ ): void {
516
+ const rejection = diagnostics?.rejection;
517
+ if (!rejection) return;
518
+
519
+ const attributes = withoutUndefinedAttributes({
520
+ ...buildLineageMetricSafeLogAttributes(session),
521
+ [LOG_ATTRIBUTES.TRACE_ID]: readSpanTraceId(session.sessionSpan),
522
+ [LOG_ATTRIBUTES.SPAN_ID]: readSpanId(session.sessionSpan),
523
+ [CONFIG_ATTRIBUTES.OBSERVME_CONFIG_SOURCE]: diagnostics.effectiveSource,
524
+ [CONFIG_ATTRIBUTES.OBSERVME_CONFIG_REJECTION_ISSUE_CODES]: [...rejection.issueCodes],
525
+ [CONFIG_ATTRIBUTES.OBSERVME_CONFIG_REJECTION_ISSUE_COUNT]: rejection.issueCount,
526
+ });
527
+
528
+ tryEmitConfigRejectionLog(session, attributes);
529
+ notifyConfigRejection(ctx, diagnostics);
530
+ }
531
+
532
+ function tryEmitConfigRejectionLog(session: ObservMeTelemetrySession, attributes: AttributeMap): void {
533
+ try {
534
+ emitStructuredLog(session.logger, LOG_EVENT_NAMES.CONFIG_REJECTED, "config", attributes, "ERROR");
535
+ } catch {
536
+ return;
537
+ }
538
+ }
539
+
540
+ function notifyConfigRejection(ctx: ObservMeHandlerContext, diagnostics: SessionConfigDiagnostics): void {
541
+ if (ctx.hasUI === false || !ctx.ui?.notify || !diagnostics.rejection) return;
542
+
543
+ const rejection = diagnostics.rejection;
544
+ const message = `ObservMe rejected ${formatConfigDiagnosticSource(diagnostics.effectiveSource)} configuration (${rejection.issueCount} issue(s): ${rejection.issueCodes.join(", ")}) and applied safe defaults.`;
545
+
546
+ try {
547
+ void Promise.resolve(ctx.ui.notify(message, "warning")).catch(ignoreConfigDiagnosticError);
548
+ } catch {
549
+ return;
550
+ }
551
+ }
552
+
553
+ function formatConfigDiagnosticSource(source: SessionConfigDiagnostics["effectiveSource"]): string {
554
+ return source.replaceAll("_", " ");
555
+ }
556
+
557
+ function ignoreConfigDiagnosticError(): undefined {
558
+ return undefined;
559
+ }
560
+
561
+ function recordWorkflowShutdownTelemetry(
562
+ session: ObservMeTelemetrySession,
563
+ attributes: AttributeMap,
564
+ failed: boolean,
565
+ labels: Record<string, string>,
566
+ ): void {
567
+ emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_SHUTDOWN, attributes);
568
+ if (!isRootWorkflow(session.lineage)) return;
569
+
570
+ const durationMs = attributes[WORKFLOW_ATTRIBUTES.PI_WORKFLOW_DURATION_MS];
571
+ if (typeof durationMs === "number") {
572
+ session.metrics.workflowDurationMs.record(durationMs, { ...labels, status: failed ? "error" : "ok" });
573
+ }
574
+
575
+ if (failed) {
576
+ session.metrics.workflowErrors.add(1, labels);
577
+ emitLifecycleLog(session.logger, LOG_EVENT_NAMES.WORKFLOW_FAILED, attributes, "ERROR");
578
+ return;
579
+ }
580
+
581
+ session.metrics.workflowsCompleted.add(1, labels);
582
+ emitLifecycleLog(session.logger, LOG_EVENT_NAMES.WORKFLOW_COMPLETED, attributes);
583
+ }
584
+
585
+ function recordExportOperationResult(session: ObservMeTelemetrySession, result: BoundedOtelOperationResult): void {
586
+ if (result.completed && !result.timedOut && !result.error) return;
587
+
588
+ const attributes = exportFailureAttributes(result);
589
+ session.metrics.exportErrors.add(1, exportFailureMetricLabels(result));
590
+ emitLifecycleLog(session.logger, LOG_EVENT_NAMES.EXPORT_FAILED, attributes, "ERROR");
591
+ }
592
+
593
+ function exportFailureAttributes(result: BoundedOtelOperationResult): AttributeMap {
594
+ return {
595
+ operation: result.operation,
596
+ reason: exportFailureReason(result),
597
+ status: result.timedOut ? "timeout" : "error",
598
+ [LOG_ATTRIBUTES.ERROR_TYPE]: exportFailureErrorClass(result),
599
+ };
600
+ }
601
+
602
+ function exportFailureMetricLabels(result: BoundedOtelOperationResult): Record<string, string> {
603
+ return {
604
+ operation: result.operation,
605
+ reason: exportFailureReason(result),
606
+ error_class: exportFailureErrorClass(result),
607
+ };
608
+ }
609
+
610
+ function exportFailureReason(result: BoundedOtelOperationResult): "export_error" | "export_timeout" {
611
+ return result.timedOut ? "export_timeout" : "export_error";
612
+ }
613
+
614
+ function exportFailureErrorClass(result: BoundedOtelOperationResult): string {
615
+ if (result.timedOut) return "timeout";
616
+ if (!result.error) return "unknown";
617
+ return normalizeMetricValue(errorClass(result.error), "error");
618
+ }
619
+
620
+ function buildShutdownAttributes(event: unknown, session: ObservMeTelemetrySession): AttributeMap {
621
+ return withoutUndefinedAttributes({
622
+ [LOG_ATTRIBUTES.PI_SESSION_ID]: readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
623
+ [LOG_ATTRIBUTES.PI_WORKFLOW_ID]: session.lineage.workflowId,
624
+ [LOG_ATTRIBUTES.PI_WORKFLOW_ROOT_AGENT_ID]: session.lineage.workflowRootAgentId,
625
+ [LOG_ATTRIBUTES.PI_AGENT_ID]: session.lineage.agentId,
626
+ [LOG_ATTRIBUTES.PI_AGENT_ROOT_ID]: session.lineage.rootAgentId,
627
+ [WORKFLOW_ATTRIBUTES.PI_WORKFLOW_DURATION_MS]: resolveWorkflowDurationMs(session),
628
+ [WORKFLOW_ATTRIBUTES.PI_WORKFLOW_STATUS]: workflowFailed(event) ? "error" : "ok",
629
+ });
630
+ }
631
+
632
+ function resolveWorkflowDurationMs(session: ObservMeTelemetrySession): number | undefined {
633
+ if (session.workflowStartedAtMs === undefined) return undefined;
634
+ return Math.max(0, (session.now?.() ?? Date.now()) - session.workflowStartedAtMs);
635
+ }
636
+
637
+ function handlerErrorAttributes(name: string, error: unknown): AttributeMap {
638
+ return {
639
+ handler: name,
640
+ [LOG_ATTRIBUTES.ERROR_TYPE]: errorClass(error),
641
+ };
642
+ }