@senad-d/observme 0.1.4 → 0.1.6

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 (71) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +24 -8
  3. package/docs/agent-subagent-observability-requirements.md +4 -3
  4. package/docs/compatibility-matrix.md +14 -3
  5. package/docs/configuration.md +2 -2
  6. package/docs/extension-integration.md +20 -13
  7. package/docs/reference/03-pi-event-and-session-model.md +6 -6
  8. package/docs/reference/04-telemetry-semantic-conventions.md +1 -0
  9. package/docs/reference/06-security-privacy-redaction.md +13 -6
  10. package/docs/reference/08-query-grafana-integration.md +30 -7
  11. package/docs/reference/11-deployment-runbooks.md +21 -0
  12. package/docs/reference/12-configuration-reference.md +30 -4
  13. package/docs/review-validation.md +10 -0
  14. package/examples/integrations/subagent-runner.ts +8 -5
  15. package/examples/observme.yaml +2 -2
  16. package/package.json +12 -4
  17. package/src/commands/obs-agents.ts +17 -12
  18. package/src/commands/obs-backfill.ts +2 -2
  19. package/src/commands/obs-command-support.ts +2 -1
  20. package/src/commands/obs-cost.ts +15 -7
  21. package/src/commands/obs-health.ts +22 -2
  22. package/src/commands/obs-loki-summary.ts +4 -8
  23. package/src/commands/obs-session.ts +35 -28
  24. package/src/commands/obs-status.ts +56 -13
  25. package/src/commands/obs-tools.ts +19 -8
  26. package/src/commands/obs-trace.ts +9 -0
  27. package/src/commands/obs.ts +6 -1
  28. package/src/config/bootstrap-project-config.ts +49 -32
  29. package/src/config/defaults.ts +0 -2
  30. package/src/config/load-config.ts +270 -92
  31. package/src/config/project-paths.ts +318 -6
  32. package/src/config/query-limits.ts +10 -0
  33. package/src/config/schema.ts +9 -8
  34. package/src/config/transport-security.ts +107 -0
  35. package/src/config/validate.ts +68 -11
  36. package/src/extension.ts +2 -12
  37. package/src/integration.ts +6 -2
  38. package/src/otel/logs.ts +24 -13
  39. package/src/otel/metrics.ts +24 -13
  40. package/src/otel/otlp-endpoint.ts +41 -2
  41. package/src/otel/otlp-http-options.ts +11 -0
  42. package/src/otel/sdk.ts +155 -9
  43. package/src/otel/shutdown.ts +39 -11
  44. package/src/otel/traces.ts +34 -16
  45. package/src/pi/agent-tree-tracker.ts +14 -1
  46. package/src/pi/compatibility.ts +30 -0
  47. package/src/pi/event-handlers/agent-turn.ts +16 -9
  48. package/src/pi/event-handlers/lifecycle.ts +207 -75
  49. package/src/pi/event-handlers/llm.ts +17 -9
  50. package/src/pi/event-handlers/session-events.ts +53 -19
  51. package/src/pi/event-handlers/tool-bash.ts +21 -27
  52. package/src/pi/handler-internals.ts +16 -26
  53. package/src/pi/handler-runtime.ts +142 -55
  54. package/src/pi/handler-types.ts +30 -15
  55. package/src/pi/handlers.ts +12 -1
  56. package/src/pi/integration-api.ts +27 -15
  57. package/src/pi/session-correlation.ts +167 -0
  58. package/src/pi/subagent-spawn.ts +164 -31
  59. package/src/privacy/redact.ts +574 -68
  60. package/src/privacy/secret-patterns.ts +6 -1
  61. package/src/query/grafana-readiness.ts +18 -2
  62. package/src/query/grafana-transport.ts +150 -27
  63. package/src/query/grafana-url.ts +36 -0
  64. package/src/query/grafana.ts +4 -136
  65. package/src/query/loki.ts +2 -4
  66. package/src/query/prometheus.ts +8 -10
  67. package/src/query/tempo.ts +2 -4
  68. package/src/query/trace-link.ts +186 -0
  69. package/src/safety/display-bounds.ts +84 -0
  70. package/src/semconv/metrics.ts +1 -0
  71. package/src/semconv/values.ts +2 -0
@@ -1,5 +1,10 @@
1
1
  import { open } from "node:fs/promises";
2
2
  import { SpanStatusCode } from "@opentelemetry/api";
3
+ import type {
4
+ ExtensionContext,
5
+ SessionShutdownEvent,
6
+ SessionStartEvent,
7
+ } from "@earendil-works/pi-coding-agent";
3
8
  import {
4
9
  clearObsAgentsRuntimeState,
5
10
  startObsAgentsRuntimeState,
@@ -25,10 +30,10 @@ import {
25
30
  import type { ObservMeConfig } from "../../config/schema.ts";
26
31
  import { emitUnsafeCaptureWarning, normalizeConfigRejectionDiagnostic } from "../../config/validate.ts";
27
32
  import { EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE } from "../../constants.ts";
28
- import type { BoundedOtelOperationResult } from "../../otel/shutdown.ts";
33
+ import { ObservMeOtelStartupError } from "../../otel/sdk.ts";
34
+ import type { BoundedOtelOperationResult, OtelOperationSettlement } from "../../otel/shutdown.ts";
29
35
  import {
30
36
  AGENT_LINEAGE_ATTRIBUTES,
31
- COMMON_SPAN_ATTRIBUTES,
32
37
  CONFIG_ATTRIBUTES,
33
38
  LOG_ATTRIBUTES,
34
39
  SESSION_ATTRIBUTES,
@@ -55,7 +60,6 @@ import {
55
60
  readSpanId,
56
61
  readSpanTraceId,
57
62
  readString,
58
- readUnknown,
59
63
  resolveModelId,
60
64
  resolveModelProvider,
61
65
  resolveSessionFilePath,
@@ -71,15 +75,19 @@ import {
71
75
  startSessionTelemetry,
72
76
  workflowFailed,
73
77
  } from "../handler-runtime.ts";
78
+ import {
79
+ appendSessionCorrelationEntry,
80
+ readLatestSessionCorrelation,
81
+ } from "../session-correlation.ts";
74
82
  import type { HandlerRegistrar, SerializedLifecycleQueue } from "../handler-runtime.ts";
75
83
  import type {
76
84
  AttributeMap,
77
- Handler,
78
85
  HandlerSessionState,
79
86
  LoadSessionConfig,
80
87
  MinimalSessionCorrelation,
81
88
  ObservMeHandlerContext,
82
89
  ObservMeTelemetrySession,
90
+ PiHandler,
83
91
  RegisterHandlersOptions,
84
92
  SessionConfigLoadResult,
85
93
  SessionRecoveryHeader,
@@ -108,23 +116,33 @@ export function buildSessionAttributes(
108
116
  config: ObservMeConfig,
109
117
  lineage: ObservMeTelemetrySession["lineage"],
110
118
  recovery?: StartupRecoveryState,
119
+ initialThinkingLevel?: unknown,
111
120
  ): 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);
121
+ const sessionManager = ctx.sessionManager;
122
+ const managerHeader = normalizeSessionHeader(sessionManager?.getHeader());
123
+ const header = managerHeader ?? recovery?.header;
124
+ const cwd = sessionManager?.getCwd() ?? header?.cwd ?? ctx.cwd ?? process.cwd();
125
+ const sessionId = sessionManager?.getSessionId() ?? header?.id ?? resolveSessionId(event, ctx, lineage);
126
+ const parentSessionId = header?.parentSession ?? lineage.parentSessionId;
127
+ const sessionFile = sessionManager?.getSessionFile() ?? recovery?.sessionFile ?? resolveSessionFilePath(event, ctx);
128
+ const sessionName = sessionManager
129
+ ? sessionManager.getSessionName() ?? "unknown"
130
+ : readString(event, "sessionName") ?? readString(event, "name") ?? "unknown";
131
+ const persisted = sessionManager
132
+ ? sessionFile !== undefined
133
+ : readBoolean(event, "persisted") ?? recovery?.resumed ?? false;
116
134
 
117
135
  return withoutUndefinedAttributes({
118
136
  [SESSION_ATTRIBUTES.PI_SESSION_ID]: sessionId,
119
- [SESSION_ATTRIBUTES.PI_SESSION_NAME]: readString(event, "sessionName") ?? readString(event, "name") ?? "unknown",
137
+ [SESSION_ATTRIBUTES.PI_SESSION_NAME]: sessionName,
120
138
  [SESSION_ATTRIBUTES.PI_SESSION_CWD_HASH]: hashValue(cwd, config),
121
139
  [SESSION_ATTRIBUTES.PI_SESSION_PARENT_SESSION_HASH]: parentSessionId ? hashValue(parentSessionId, config) : "",
122
- [SESSION_ATTRIBUTES.PI_SESSION_PERSISTED]: readBoolean(event, "persisted") ?? recovery?.resumed ?? false,
140
+ [SESSION_ATTRIBUTES.PI_SESSION_PERSISTED]: persisted,
123
141
  [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),
142
+ [SESSION_ATTRIBUTES.PI_SESSION_VERSION]: readString(header, "version") ?? "unknown",
143
+ [SESSION_ATTRIBUTES.PI_MODEL_PROVIDER_CURRENT]: resolveModelProvider(ctx),
144
+ [SESSION_ATTRIBUTES.PI_MODEL_ID_CURRENT]: resolveModelId(ctx),
145
+ [SESSION_ATTRIBUTES.PI_THINKING_LEVEL_CURRENT]: resolveThinkingLevel(initialThinkingLevel),
128
146
  ...buildCommonSessionSpanAttributes(sessionId, config, lineage),
129
147
  });
130
148
  }
@@ -153,7 +171,7 @@ function createSessionStartHandler(
153
171
  loadConfigFn: LoadSessionConfig,
154
172
  startTelemetryFn: StartSessionTelemetry,
155
173
  options: RegisterHandlersOptions,
156
- ): Handler {
174
+ ): PiHandler<"session_start"> {
157
175
  return handleSessionStart.bind(undefined, state, loadConfigFn, startTelemetryFn, options);
158
176
  }
159
177
 
@@ -162,19 +180,29 @@ async function handleSessionStart(
162
180
  loadConfigFn: LoadSessionConfig,
163
181
  startTelemetryFn: StartSessionTelemetry,
164
182
  options: RegisterHandlersOptions,
165
- event: unknown,
166
- ctx: ObservMeHandlerContext,
183
+ event: SessionStartEvent,
184
+ ctx: ExtensionContext,
167
185
  ): Promise<void> {
186
+ if (!(await resolvePendingTelemetryCleanupBeforeStart(state, ctx))) return;
187
+
168
188
  const previousSession = state.session;
169
- if (previousSession) await shutDownPreviousSessionBeforeDuplicateStart(previousSession, ctx, state);
189
+ if (previousSession && !(await shutDownPreviousSessionBeforeDuplicateStart(previousSession, ctx, state))) return;
170
190
 
171
191
  await ensureProjectConfigForHandler(options, ctx);
172
192
  const loadedConfig = await loadSessionConfigForHandler(loadConfigFn, options, ctx);
173
193
  const config = loadedConfig.config;
174
- await emitUnsafeCaptureWarning(config, ctx);
194
+ updateObsStatusRuntimeState({ config, configDiagnostics: loadedConfig.diagnostics });
195
+ clearObsStatusExportError();
175
196
 
197
+ if (!config.enabled) {
198
+ clearDisabledTelemetryRuntimeState(state);
199
+ await clearExtensionStatus(ctx);
200
+ return;
201
+ }
202
+
203
+ await emitUnsafeCaptureWarning(config, ctx);
176
204
  const recovery = await resolveStartupRecovery(event, ctx, config, options);
177
- const recoveryCorrelation = recovery.customCorrelation ?? recovery.header?.correlation;
205
+ const recoveryCorrelation = recovery.customCorrelation;
178
206
  const lineage = createAgentLineageContext({
179
207
  config,
180
208
  env: buildRecoveryLineageEnv(config, recoveryCorrelation, options.env),
@@ -183,19 +211,31 @@ async function handleSessionStart(
183
211
  options.requireCompleteParentEnvelope ?? (options.trustedParentContext === true && recoveryCorrelation === undefined),
184
212
  failOpenInvalidPropagation: true,
185
213
  });
186
- const session = await startTelemetryFn({
187
- config,
188
- lineage,
189
- now: options.now,
190
- wallClockNow: options.wallClockNow,
191
- });
214
+ let session: ObservMeTelemetrySession;
215
+ try {
216
+ session = await startTelemetryFn({
217
+ config,
218
+ lineage,
219
+ now: options.now,
220
+ wallClockNow: options.wallClockNow,
221
+ });
222
+ } catch (error) {
223
+ await handleTelemetryStartupFailure(state, ctx, error);
224
+ return;
225
+ }
226
+
192
227
  session.now = options.now ?? session.now ?? monotonicNowMs;
193
228
  state.session = session;
194
229
 
195
230
  try {
196
- updateObsStatusRuntimeState({ config: session.config, configDiagnostics: loadedConfig.diagnostics });
197
- clearObsStatusExportError();
198
- const attributes = buildSessionAttributes(event, ctx, session.config, lineage, recovery);
231
+ const attributes = buildSessionAttributes(
232
+ event,
233
+ ctx,
234
+ session.config,
235
+ lineage,
236
+ recovery,
237
+ options.getThinkingLevel?.(),
238
+ );
199
239
  const labels = metricLabels(session.config, lineage);
200
240
 
201
241
  session.sessionAttributes = attributes;
@@ -206,7 +246,7 @@ async function handleSessionStart(
206
246
  startObsSessionRuntimeState({
207
247
  sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
208
248
  traceId: readSpanTraceId(session.sessionSpan),
209
- traceUrlTemplate: session.config.query.links.traceUrlTemplate,
249
+ config: session.config,
210
250
  });
211
251
  startObsAgentsRuntimeState({
212
252
  lineage,
@@ -218,29 +258,31 @@ async function handleSessionStart(
218
258
  session.metrics.sessionsStarted.add(1, labels);
219
259
  session.sessionSpan.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, attributes);
220
260
  emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, attributes);
221
- if (recovery.resumed && session.config.replayOnStart) emitStartupReplayTelemetry(session, attributes);
222
261
 
223
262
  if (isRootWorkflow(lineage)) {
224
263
  session.metrics.workflowsStarted.add(1, labels);
225
264
  emitLifecycleLog(session.logger, LOG_EVENT_NAMES.WORKFLOW_STARTED, attributes);
226
265
  }
227
266
 
228
- await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE);
267
+ ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE);
229
268
  activateSessionActiveAgent(session, labels);
269
+ if (session.config.agent.writeCorrelationEntry) {
270
+ appendSessionCorrelationEntry(options.appendEntry, lineage, recovery.customCorrelation);
271
+ }
230
272
  } catch (error) {
231
273
  await cleanUpFailedSessionStart(session, ctx, state);
232
274
  throw error;
233
275
  }
234
276
  }
235
277
 
236
- function createSessionShutdownHandler(state: HandlerSessionState): Handler {
278
+ function createSessionShutdownHandler(state: HandlerSessionState): PiHandler<"session_shutdown"> {
237
279
  return handleSessionShutdown.bind(undefined, state);
238
280
  }
239
281
 
240
282
  async function handleSessionShutdown(
241
283
  state: HandlerSessionState,
242
- event: unknown,
243
- ctx: ObservMeHandlerContext,
284
+ event: SessionShutdownEvent,
285
+ ctx: ExtensionContext,
244
286
  ): Promise<void> {
245
287
  const session = state.session;
246
288
  if (!session) return;
@@ -256,8 +298,13 @@ async function resolveStartupRecovery(
256
298
  ): Promise<StartupRecoveryState> {
257
299
  const sessionFile = resolveSessionFilePath(event, ctx);
258
300
  const readHeader = options.readSessionHeader ?? readSessionHeaderFromFile;
259
- const header = sessionFile ? await readHeader(sessionFile) : undefined;
260
- const customCorrelation = config.agent.writeCorrelationEntry ? readExplicitCustomCorrelation(event) : undefined;
301
+ let header: SessionRecoveryHeader | undefined;
302
+ if (ctx.sessionManager) {
303
+ header = normalizeSessionHeader(ctx.sessionManager.getHeader());
304
+ } else if (sessionFile) {
305
+ header = await readHeader(sessionFile);
306
+ }
307
+ const customCorrelation = config.agent.writeCorrelationEntry ? readActiveBranchCorrelation(ctx) : undefined;
261
308
 
262
309
  return {
263
310
  resumed: isExistingSessionStart(event),
@@ -267,16 +314,6 @@ async function resolveStartupRecovery(
267
314
  };
268
315
  }
269
316
 
270
- function emitStartupReplayTelemetry(session: ObservMeTelemetrySession, attributes: AttributeMap): void {
271
- const replayAttributes = {
272
- ...attributes,
273
- [COMMON_SPAN_ATTRIBUTES.OBSERVME_REPLAYED]: true,
274
- };
275
-
276
- session.sessionSpan?.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, replayAttributes);
277
- emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, replayAttributes);
278
- }
279
-
280
317
  function buildRecoveryLineageEnv(
281
318
  config: ObservMeConfig,
282
319
  correlation: MinimalSessionCorrelation | undefined,
@@ -291,19 +328,28 @@ function buildRecoveryLineageEnv(
291
328
  ...definedEnvValue(config.agent.parentIdEnv, correlation.parentAgentId),
292
329
  ...definedEnvValue(config.agent.rootIdEnv, correlation.rootAgentId),
293
330
  ...definedEnvValue(config.agent.parentSessionIdEnv, correlation.parentSessionId),
294
- ...definedEnvValue(config.agent.depthEnv, correlation.depth === undefined ? undefined : String(correlation.depth)),
331
+ ...definedEnvValue(config.agent.depthEnv, recoveryPropagationDepth(correlation)),
295
332
  ...definedEnvValue(config.agent.spawnIdEnv, correlation.spawnId),
296
333
  ...definedEnvValue(config.agent.capabilityEnv, correlation.capability),
297
334
  };
298
335
  }
299
336
 
337
+ function recoveryPropagationDepth(correlation: MinimalSessionCorrelation): string | undefined {
338
+ if (correlation.depth === undefined) return undefined;
339
+ const parentDepth = correlation.parentAgentId ? Math.max(0, correlation.depth - 1) : correlation.depth;
340
+ return String(parentDepth);
341
+ }
342
+
300
343
  function definedEnvValue(name: string, value: string | undefined): NodeJS.ProcessEnv {
301
344
  return value === undefined || value === "" ? {} : { [name]: value };
302
345
  }
303
346
 
304
- function readExplicitCustomCorrelation(event: unknown): MinimalSessionCorrelation | undefined {
305
- const value = readUnknown(event, "customCorrelation") ?? readUnknown(event, "observmeCorrelation");
306
- return normalizeMinimalCorrelation(value);
347
+ function readActiveBranchCorrelation(ctx: ObservMeHandlerContext): StartupRecoveryState["customCorrelation"] {
348
+ try {
349
+ return readLatestSessionCorrelation(ctx.sessionManager?.getBranch());
350
+ } catch {
351
+ return undefined;
352
+ }
307
353
  }
308
354
 
309
355
  function normalizeSessionHeader(value: unknown): SessionRecoveryHeader | undefined {
@@ -316,27 +362,9 @@ function normalizeSessionHeader(value: unknown): SessionRecoveryHeader | undefin
316
362
  timestamp: readString(value, "timestamp"),
317
363
  cwd: readString(value, "cwd"),
318
364
  parentSession: readString(value, "parentSession"),
319
- correlation: normalizeMinimalCorrelation(readUnknown(value, "observmeCorrelation") ?? readUnknown(value, "correlation")),
320
365
  });
321
366
  }
322
367
 
323
- function normalizeMinimalCorrelation(value: unknown): MinimalSessionCorrelation | undefined {
324
- if (!isRecord(value)) return undefined;
325
-
326
- const correlation = withoutUndefinedObjectValues({
327
- workflowId: readString(value, "workflowId"),
328
- agentId: readString(value, "agentId"),
329
- parentAgentId: readString(value, "parentAgentId"),
330
- rootAgentId: readString(value, "rootAgentId"),
331
- parentSessionId: readString(value, "parentSessionId"),
332
- depth: readInteger(value, "depth"),
333
- spawnId: readString(value, "spawnId"),
334
- capability: readString(value, "capability"),
335
- });
336
-
337
- return Object.keys(correlation).length === 0 ? undefined : correlation;
338
- }
339
-
340
368
  function withoutUndefinedObjectValues<T extends Record<string, unknown>>(value: T): T {
341
369
  const definedEntries: Array<[string, unknown]> = [];
342
370
  for (const entry of Object.entries(value)) {
@@ -397,19 +425,84 @@ async function shutDownPreviousSessionBeforeDuplicateStart(
397
425
  session: ObservMeTelemetrySession,
398
426
  ctx: ObservMeHandlerContext,
399
427
  state: HandlerSessionState,
400
- ): Promise<void> {
428
+ ): Promise<boolean> {
401
429
  emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_DUPLICATE_START, buildDuplicateSessionStartAttributes(session));
402
430
 
403
431
  try {
404
- await shutDownTelemetrySession(session, duplicateSessionStartShutdownEvent(), ctx, state);
432
+ const result = await shutDownTelemetrySession(session, duplicateSessionStartShutdownEvent(), ctx, state);
433
+ if (otelCleanupCompleted(result)) return true;
434
+ notifyPendingTelemetryCleanup(ctx);
435
+ return false;
405
436
  } catch (error) {
406
437
  recordDuplicateSessionStartShutdownError(session, error);
407
438
  clearObsSessionRuntimeState();
408
439
  clearObsAgentsRuntimeState();
409
440
  state.session = undefined;
441
+ state.pendingCleanup = session;
442
+ notifyPendingTelemetryCleanup(ctx);
443
+ return false;
410
444
  }
411
445
  }
412
446
 
447
+ async function resolvePendingTelemetryCleanupBeforeStart(
448
+ state: HandlerSessionState,
449
+ ctx: ObservMeHandlerContext,
450
+ ): Promise<boolean> {
451
+ const pendingSession = state.pendingCleanup;
452
+ if (!pendingSession) return true;
453
+
454
+ const result = await recordControllerOperationResult(pendingSession, "shutdown");
455
+ if (otelCleanupCompleted(result)) {
456
+ if (state.pendingCleanup === pendingSession) state.pendingCleanup = undefined;
457
+ return true;
458
+ }
459
+
460
+ retainUnresolvedTelemetryCleanup(state, pendingSession, result);
461
+ notifyPendingTelemetryCleanup(ctx);
462
+ return false;
463
+ }
464
+
465
+ function retainUnresolvedTelemetryCleanup(
466
+ state: HandlerSessionState,
467
+ session: ObservMeTelemetrySession,
468
+ result: BoundedOtelOperationResult | undefined,
469
+ ): void {
470
+ if (!result || otelCleanupCompleted(result)) return;
471
+
472
+ state.pendingCleanup = session;
473
+ void result.settlement?.then(releaseSettledTelemetryCleanup.bind(undefined, state, session));
474
+ }
475
+
476
+ function releaseSettledTelemetryCleanup(
477
+ state: HandlerSessionState,
478
+ session: ObservMeTelemetrySession,
479
+ settlement: OtelOperationSettlement,
480
+ ): void {
481
+ if (!otelCleanupCompleted(settlement) || state.pendingCleanup !== session) return;
482
+ state.pendingCleanup = undefined;
483
+ }
484
+
485
+ function otelCleanupCompleted(
486
+ result: Pick<BoundedOtelOperationResult, "completed" | "timedOut" | "error">,
487
+ ): boolean {
488
+ return result.completed && !result.timedOut && !result.error;
489
+ }
490
+
491
+ function notifyPendingTelemetryCleanup(ctx: ObservMeHandlerContext): void {
492
+ if (ctx.hasUI === false || !ctx.ui?.notify) return;
493
+
494
+ const message = "ObservMe telemetry startup was deferred because prior OTEL shutdown cleanup is still unresolved.";
495
+ try {
496
+ void Promise.resolve(ctx.ui.notify(message, "warning")).catch(ignorePendingCleanupDiagnosticError);
497
+ } catch {
498
+ return;
499
+ }
500
+ }
501
+
502
+ function ignorePendingCleanupDiagnosticError(): undefined {
503
+ return undefined;
504
+ }
505
+
413
506
  function buildDuplicateSessionStartAttributes(session: ObservMeTelemetrySession): AttributeMap {
414
507
  return withoutUndefinedAttributes({
415
508
  [LOG_ATTRIBUTES.PI_SESSION_ID]: readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
@@ -444,6 +537,7 @@ async function cleanUpFailedSessionStart(
444
537
  state: HandlerSessionState,
445
538
  ): Promise<void> {
446
539
  const labels = metricLabels(session.config, session.lineage);
540
+ let shutdownResult: BoundedOtelOperationResult | undefined;
447
541
 
448
542
  try {
449
543
  deactivateSessionActiveAgent(session, labels);
@@ -451,10 +545,11 @@ async function cleanUpFailedSessionStart(
451
545
  endActiveSpan(session, session.sessionSpan);
452
546
  await clearExtensionStatus(ctx);
453
547
  await recordControllerOperationResult(session, "flush");
454
- await recordControllerOperationResult(session, "shutdown");
548
+ shutdownResult = await recordControllerOperationResult(session, "shutdown");
455
549
  } finally {
456
550
  disposeSessionActiveAgentLease(session);
457
551
  clearTelemetrySessionRuntimeState(session, state);
552
+ retainUnresolvedTelemetryCleanup(state, session, shutdownResult);
458
553
  }
459
554
  }
460
555
 
@@ -463,10 +558,11 @@ async function shutDownTelemetrySession(
463
558
  event: unknown,
464
559
  ctx: ObservMeHandlerContext,
465
560
  state: HandlerSessionState,
466
- ): Promise<void> {
561
+ ): Promise<BoundedOtelOperationResult> {
467
562
  const labels = metricLabels(session.config, session.lineage);
468
563
  const shutdownAttributes = buildShutdownAttributes(event, session);
469
564
  const failed = workflowFailed(event);
565
+ let shutdownResult: BoundedOtelOperationResult | undefined;
470
566
 
471
567
  try {
472
568
  deactivateSessionActiveAgent(session, labels);
@@ -478,10 +574,12 @@ async function shutDownTelemetrySession(
478
574
  endActiveSpan(session, session.sessionSpan);
479
575
  await clearExtensionStatus(ctx);
480
576
  await recordControllerOperationResult(session, "flush");
481
- await recordControllerOperationResult(session, "shutdown");
577
+ shutdownResult = await recordControllerOperationResult(session, "shutdown");
578
+ return shutdownResult;
482
579
  } finally {
483
580
  disposeSessionActiveAgentLease(session);
484
581
  clearTelemetrySessionRuntimeState(session, state);
582
+ retainUnresolvedTelemetryCleanup(state, session, shutdownResult);
485
583
  }
486
584
  }
487
585
 
@@ -520,6 +618,39 @@ function clearTelemetrySessionRuntimeState(
520
618
  if (state.session === session) state.session = undefined;
521
619
  }
522
620
 
621
+ function clearDisabledTelemetryRuntimeState(state: HandlerSessionState): void {
622
+ state.session = undefined;
623
+ clearObsSessionRuntimeState();
624
+ clearObsAgentsRuntimeState();
625
+ }
626
+
627
+ async function handleTelemetryStartupFailure(
628
+ state: HandlerSessionState,
629
+ ctx: ObservMeHandlerContext,
630
+ error: unknown,
631
+ ): Promise<void> {
632
+ clearDisabledTelemetryRuntimeState(state);
633
+ if (!(error instanceof ObservMeOtelStartupError)) throw error;
634
+
635
+ recordObsStatusExportResult({ operation: "startup", error });
636
+ await clearExtensionStatus(ctx);
637
+ notifyTelemetryStartupFailure(ctx, error.message);
638
+ }
639
+
640
+ function notifyTelemetryStartupFailure(ctx: ObservMeHandlerContext, message: string): void {
641
+ if (ctx.hasUI === false || !ctx.ui?.notify) return;
642
+
643
+ try {
644
+ void Promise.resolve(ctx.ui.notify(message, "error")).catch(ignoreStartupDiagnosticError);
645
+ } catch {
646
+ return;
647
+ }
648
+ }
649
+
650
+ function ignoreStartupDiagnosticError(): undefined {
651
+ return undefined;
652
+ }
653
+
523
654
  async function clearExtensionStatus(ctx: ObservMeHandlerContext): Promise<void> {
524
655
  try {
525
656
  await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, undefined);
@@ -531,10 +662,11 @@ async function clearExtensionStatus(ctx: ObservMeHandlerContext): Promise<void>
531
662
  async function recordControllerOperationResult(
532
663
  session: ObservMeTelemetrySession,
533
664
  operation: BoundedOtelOperationResult["operation"],
534
- ): Promise<void> {
665
+ ): Promise<BoundedOtelOperationResult> {
535
666
  const result = await runControllerOperation(session, operation);
536
667
  recordObsStatusExportResult(result);
537
668
  recordExportOperationResult(session, result);
669
+ return result;
538
670
  }
539
671
 
540
672
  async function runControllerOperation(
@@ -1,4 +1,8 @@
1
1
  import { SpanStatusCode } from "@opentelemetry/api";
2
+ import type {
3
+ BeforeProviderRequestEvent,
4
+ ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
2
6
  import { recordObsSessionLlmCall } from "../../commands/obs-session.ts";
3
7
  import { LOG_EVENT_NAMES } from "../../semconv/metrics.ts";
4
8
  import { SPAN_NAMES } from "../../semconv/spans.ts";
@@ -26,7 +30,7 @@ import {
26
30
  startActiveChildSpan,
27
31
  } from "../handler-internals.ts";
28
32
  import type { HandlerRegistrar } from "../handler-runtime.ts";
29
- import type { Handler, HandlerSessionState, ObservMeHandlerContext } from "../handler-types.ts";
33
+ import type { HandlerSessionState, PiEvent, PiHandler } from "../handler-types.ts";
30
34
  import { recordBashExecution } from "./tool-bash.ts";
31
35
 
32
36
  export function registerLlmHandlers(registrar: HandlerRegistrar, state: HandlerSessionState): void {
@@ -35,14 +39,14 @@ export function registerLlmHandlers(registrar: HandlerRegistrar, state: HandlerS
35
39
  registrar.add("message_end", createMessageEndHandler(state));
36
40
  }
37
41
 
38
- function createBeforeProviderRequestHandler(state: HandlerSessionState): Handler {
42
+ function createBeforeProviderRequestHandler(state: HandlerSessionState): PiHandler<"before_provider_request"> {
39
43
  return handleBeforeProviderRequest.bind(undefined, state);
40
44
  }
41
45
 
42
46
  function handleBeforeProviderRequest(
43
47
  state: HandlerSessionState,
44
- event: unknown,
45
- ctx: ObservMeHandlerContext,
48
+ event: BeforeProviderRequestEvent,
49
+ ctx: ExtensionContext,
46
50
  ): void {
47
51
  const session = state.session;
48
52
  if (!session) return;
@@ -61,14 +65,14 @@ function handleBeforeProviderRequest(
61
65
  emitLifecycleLog(session.logger, LOG_EVENT_NAMES.LLM_REQUEST_STARTED, attributes);
62
66
  }
63
67
 
64
- function createAfterProviderResponseHandler(state: HandlerSessionState): Handler {
68
+ function createAfterProviderResponseHandler(state: HandlerSessionState): PiHandler<"after_provider_response"> {
65
69
  return handleAfterProviderResponse.bind(undefined, state);
66
70
  }
67
71
 
68
72
  function handleAfterProviderResponse(
69
73
  state: HandlerSessionState,
70
- event: unknown,
71
- _ctx: ObservMeHandlerContext,
74
+ event: PiEvent<"after_provider_response">,
75
+ _ctx: ExtensionContext,
72
76
  ): void {
73
77
  const session = state.session;
74
78
  if (!session) return;
@@ -77,11 +81,15 @@ function handleAfterProviderResponse(
77
81
  span?.setAttributes(buildLlmResponseAttributes(event));
78
82
  }
79
83
 
80
- function createMessageEndHandler(state: HandlerSessionState): Handler {
84
+ function createMessageEndHandler(state: HandlerSessionState): PiHandler<"message_end"> {
81
85
  return handleMessageEnd.bind(undefined, state);
82
86
  }
83
87
 
84
- function handleMessageEnd(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
88
+ function handleMessageEnd(
89
+ state: HandlerSessionState,
90
+ event: PiEvent<"message_end">,
91
+ _ctx: ExtensionContext,
92
+ ): void {
85
93
  const session = state.session;
86
94
  if (!session) return;
87
95