@senad-d/observme 0.1.3 → 0.1.5

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 (83) hide show
  1. package/.env.example +4 -0
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +36 -11
  4. package/dashboards/observme-agent-node-graphs.json +5 -5
  5. package/dashboards/observme-agents.json +169 -4
  6. package/dashboards/observme-alerts.yaml +16 -3
  7. package/dashboards/observme-overview.json +6 -6
  8. package/dashboards/observme-trace-journey.json +4 -4
  9. package/docs/agent-subagent-observability-requirements.md +19 -10
  10. package/docs/compatibility-matrix.md +21 -4
  11. package/docs/configuration.md +7 -2
  12. package/docs/extension-integration.md +20 -13
  13. package/docs/reference/03-pi-event-and-session-model.md +6 -6
  14. package/docs/reference/04-telemetry-semantic-conventions.md +39 -1
  15. package/docs/reference/05-otel-pipeline-and-collector.md +23 -10
  16. package/docs/reference/06-security-privacy-redaction.md +13 -6
  17. package/docs/reference/08-query-grafana-integration.md +30 -7
  18. package/docs/reference/09-dashboards-alerts-slos.md +132 -3
  19. package/docs/reference/10-testing-release-operations.md +44 -14
  20. package/docs/reference/11-deployment-runbooks.md +82 -5
  21. package/docs/reference/12-configuration-reference.md +48 -5
  22. package/docs/review-validation.md +17 -0
  23. package/examples/README.md +7 -2
  24. package/examples/collector.yaml +8 -8
  25. package/examples/integrations/subagent-runner.ts +8 -5
  26. package/examples/observme.yaml +3 -2
  27. package/package.json +14 -4
  28. package/src/commands/obs-agents.ts +17 -12
  29. package/src/commands/obs-backfill.ts +2 -2
  30. package/src/commands/obs-command-support.ts +2 -1
  31. package/src/commands/obs-cost.ts +15 -7
  32. package/src/commands/obs-health.ts +22 -2
  33. package/src/commands/obs-loki-summary.ts +4 -8
  34. package/src/commands/obs-session.ts +35 -28
  35. package/src/commands/obs-status.ts +56 -13
  36. package/src/commands/obs-tools.ts +19 -8
  37. package/src/commands/obs-trace.ts +9 -0
  38. package/src/commands/obs.ts +6 -1
  39. package/src/config/bootstrap-project-config.ts +50 -32
  40. package/src/config/defaults.ts +1 -2
  41. package/src/config/load-config.ts +270 -81
  42. package/src/config/project-paths.ts +318 -6
  43. package/src/config/query-limits.ts +10 -0
  44. package/src/config/schema.ts +18 -8
  45. package/src/config/transport-security.ts +107 -0
  46. package/src/config/validate.ts +88 -12
  47. package/src/extension.ts +2 -12
  48. package/src/integration.ts +6 -2
  49. package/src/otel/logs.ts +24 -13
  50. package/src/otel/metrics.ts +24 -13
  51. package/src/otel/otlp-endpoint.ts +41 -2
  52. package/src/otel/otlp-http-options.ts +11 -0
  53. package/src/otel/sdk.ts +155 -9
  54. package/src/otel/shutdown.ts +39 -11
  55. package/src/otel/traces.ts +34 -16
  56. package/src/pi/active-agent-lease.ts +101 -0
  57. package/src/pi/agent-tree-tracker.ts +14 -1
  58. package/src/pi/compatibility.ts +121 -0
  59. package/src/pi/event-handlers/agent-turn.ts +16 -9
  60. package/src/pi/event-handlers/lifecycle.ts +312 -106
  61. package/src/pi/event-handlers/llm.ts +17 -9
  62. package/src/pi/event-handlers/session-events.ts +53 -19
  63. package/src/pi/event-handlers/tool-bash.ts +21 -27
  64. package/src/pi/handler-internals.ts +16 -26
  65. package/src/pi/handler-runtime.ts +159 -54
  66. package/src/pi/handler-types.ts +40 -17
  67. package/src/pi/handlers.ts +12 -1
  68. package/src/pi/integration-api.ts +27 -15
  69. package/src/pi/session-correlation.ts +167 -0
  70. package/src/pi/subagent-spawn.ts +164 -31
  71. package/src/privacy/redact.ts +574 -68
  72. package/src/privacy/secret-patterns.ts +6 -1
  73. package/src/query/grafana-readiness.ts +18 -2
  74. package/src/query/grafana-transport.ts +150 -27
  75. package/src/query/grafana-url.ts +36 -0
  76. package/src/query/grafana.ts +4 -136
  77. package/src/query/loki.ts +2 -4
  78. package/src/query/prometheus.ts +8 -10
  79. package/src/query/tempo.ts +2 -4
  80. package/src/query/trace-link.ts +186 -0
  81. package/src/safety/display-bounds.ts +84 -0
  82. package/src/semconv/metrics.ts +7 -0
  83. 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();
196
+
197
+ if (!config.enabled) {
198
+ clearDisabledTelemetryRuntimeState(state);
199
+ await clearExtensionStatus(ctx);
200
+ return;
201
+ }
175
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,55 +211,78 @@ async function handleSessionStart(
183
211
  options.requireCompleteParentEnvelope ?? (options.trustedParentContext === true && recoveryCorrelation === undefined),
184
212
  failOpenInvalidPropagation: true,
185
213
  });
186
- const session = await startTelemetryFn({ config, lineage, now: options.now });
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
+
187
227
  session.now = options.now ?? session.now ?? monotonicNowMs;
188
- updateObsStatusRuntimeState({ config: session.config, configDiagnostics: loadedConfig.diagnostics });
189
- clearObsStatusExportError();
190
228
  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
229
 
218
- if (isRootWorkflow(lineage)) {
219
- session.metrics.workflowsStarted.add(1, labels);
220
- emitLifecycleLog(session.logger, LOG_EVENT_NAMES.WORKFLOW_STARTED, attributes);
230
+ try {
231
+ const attributes = buildSessionAttributes(
232
+ event,
233
+ ctx,
234
+ session.config,
235
+ lineage,
236
+ recovery,
237
+ options.getThinkingLevel?.(),
238
+ );
239
+ const labels = metricLabels(session.config, lineage);
240
+
241
+ session.sessionAttributes = attributes;
242
+ const traceParent = resolveSessionTraceParent(lineage);
243
+ session.sessionSpan = startActiveRootSpan(session, SPAN_NAMES.PI_SESSION, attributes, "session", traceParent);
244
+ emitConfigRejectionDiagnostic(session, loadedConfig.diagnostics, ctx);
245
+ recordSessionTracePropagationFailure(session, traceParent);
246
+ startObsSessionRuntimeState({
247
+ sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
248
+ traceId: readSpanTraceId(session.sessionSpan),
249
+ config: session.config,
250
+ });
251
+ startObsAgentsRuntimeState({
252
+ lineage,
253
+ agentTree: session.agentTree,
254
+ sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
255
+ traceId: readSpanTraceId(session.sessionSpan),
256
+ });
257
+ session.workflowStartedAtMs = session.now();
258
+ session.metrics.sessionsStarted.add(1, labels);
259
+ session.sessionSpan.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, attributes);
260
+ emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, attributes);
261
+
262
+ if (isRootWorkflow(lineage)) {
263
+ session.metrics.workflowsStarted.add(1, labels);
264
+ emitLifecycleLog(session.logger, LOG_EVENT_NAMES.WORKFLOW_STARTED, attributes);
265
+ }
266
+
267
+ ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE);
268
+ activateSessionActiveAgent(session, labels);
269
+ if (session.config.agent.writeCorrelationEntry) {
270
+ appendSessionCorrelationEntry(options.appendEntry, lineage, recovery.customCorrelation);
271
+ }
272
+ } catch (error) {
273
+ await cleanUpFailedSessionStart(session, ctx, state);
274
+ throw error;
221
275
  }
222
-
223
- await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE);
224
- state.session = session;
225
276
  }
226
277
 
227
- function createSessionShutdownHandler(state: HandlerSessionState): Handler {
278
+ function createSessionShutdownHandler(state: HandlerSessionState): PiHandler<"session_shutdown"> {
228
279
  return handleSessionShutdown.bind(undefined, state);
229
280
  }
230
281
 
231
282
  async function handleSessionShutdown(
232
283
  state: HandlerSessionState,
233
- event: unknown,
234
- ctx: ObservMeHandlerContext,
284
+ event: SessionShutdownEvent,
285
+ ctx: ExtensionContext,
235
286
  ): Promise<void> {
236
287
  const session = state.session;
237
288
  if (!session) return;
@@ -247,8 +298,13 @@ async function resolveStartupRecovery(
247
298
  ): Promise<StartupRecoveryState> {
248
299
  const sessionFile = resolveSessionFilePath(event, ctx);
249
300
  const readHeader = options.readSessionHeader ?? readSessionHeaderFromFile;
250
- const header = sessionFile ? await readHeader(sessionFile) : undefined;
251
- 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;
252
308
 
253
309
  return {
254
310
  resumed: isExistingSessionStart(event),
@@ -258,16 +314,6 @@ async function resolveStartupRecovery(
258
314
  };
259
315
  }
260
316
 
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
317
  function buildRecoveryLineageEnv(
272
318
  config: ObservMeConfig,
273
319
  correlation: MinimalSessionCorrelation | undefined,
@@ -282,19 +328,28 @@ function buildRecoveryLineageEnv(
282
328
  ...definedEnvValue(config.agent.parentIdEnv, correlation.parentAgentId),
283
329
  ...definedEnvValue(config.agent.rootIdEnv, correlation.rootAgentId),
284
330
  ...definedEnvValue(config.agent.parentSessionIdEnv, correlation.parentSessionId),
285
- ...definedEnvValue(config.agent.depthEnv, correlation.depth === undefined ? undefined : String(correlation.depth)),
331
+ ...definedEnvValue(config.agent.depthEnv, recoveryPropagationDepth(correlation)),
286
332
  ...definedEnvValue(config.agent.spawnIdEnv, correlation.spawnId),
287
333
  ...definedEnvValue(config.agent.capabilityEnv, correlation.capability),
288
334
  };
289
335
  }
290
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
+
291
343
  function definedEnvValue(name: string, value: string | undefined): NodeJS.ProcessEnv {
292
344
  return value === undefined || value === "" ? {} : { [name]: value };
293
345
  }
294
346
 
295
- function readExplicitCustomCorrelation(event: unknown): MinimalSessionCorrelation | undefined {
296
- const value = readUnknown(event, "customCorrelation") ?? readUnknown(event, "observmeCorrelation");
297
- return normalizeMinimalCorrelation(value);
347
+ function readActiveBranchCorrelation(ctx: ObservMeHandlerContext): StartupRecoveryState["customCorrelation"] {
348
+ try {
349
+ return readLatestSessionCorrelation(ctx.sessionManager?.getBranch());
350
+ } catch {
351
+ return undefined;
352
+ }
298
353
  }
299
354
 
300
355
  function normalizeSessionHeader(value: unknown): SessionRecoveryHeader | undefined {
@@ -307,27 +362,9 @@ function normalizeSessionHeader(value: unknown): SessionRecoveryHeader | undefin
307
362
  timestamp: readString(value, "timestamp"),
308
363
  cwd: readString(value, "cwd"),
309
364
  parentSession: readString(value, "parentSession"),
310
- correlation: normalizeMinimalCorrelation(readUnknown(value, "observmeCorrelation") ?? readUnknown(value, "correlation")),
311
365
  });
312
366
  }
313
367
 
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
368
  function withoutUndefinedObjectValues<T extends Record<string, unknown>>(value: T): T {
332
369
  const definedEntries: Array<[string, unknown]> = [];
333
370
  for (const entry of Object.entries(value)) {
@@ -388,19 +425,84 @@ async function shutDownPreviousSessionBeforeDuplicateStart(
388
425
  session: ObservMeTelemetrySession,
389
426
  ctx: ObservMeHandlerContext,
390
427
  state: HandlerSessionState,
391
- ): Promise<void> {
428
+ ): Promise<boolean> {
392
429
  emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_DUPLICATE_START, buildDuplicateSessionStartAttributes(session));
393
430
 
394
431
  try {
395
- 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;
396
436
  } catch (error) {
397
437
  recordDuplicateSessionStartShutdownError(session, error);
398
438
  clearObsSessionRuntimeState();
399
439
  clearObsAgentsRuntimeState();
400
440
  state.session = undefined;
441
+ state.pendingCleanup = session;
442
+ notifyPendingTelemetryCleanup(ctx);
443
+ return false;
444
+ }
445
+ }
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;
401
499
  }
402
500
  }
403
501
 
502
+ function ignorePendingCleanupDiagnosticError(): undefined {
503
+ return undefined;
504
+ }
505
+
404
506
  function buildDuplicateSessionStartAttributes(session: ObservMeTelemetrySession): AttributeMap {
405
507
  return withoutUndefinedAttributes({
406
508
  [LOG_ATTRIBUTES.PI_SESSION_ID]: readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
@@ -429,38 +531,142 @@ function recordDuplicateSessionStartShutdownError(session: ObservMeTelemetrySess
429
531
  );
430
532
  }
431
533
 
534
+ async function cleanUpFailedSessionStart(
535
+ session: ObservMeTelemetrySession,
536
+ ctx: ObservMeHandlerContext,
537
+ state: HandlerSessionState,
538
+ ): Promise<void> {
539
+ const labels = metricLabels(session.config, session.lineage);
540
+ let shutdownResult: BoundedOtelOperationResult | undefined;
541
+
542
+ try {
543
+ deactivateSessionActiveAgent(session, labels);
544
+ endAllActiveSpans(session);
545
+ endActiveSpan(session, session.sessionSpan);
546
+ await clearExtensionStatus(ctx);
547
+ await recordControllerOperationResult(session, "flush");
548
+ shutdownResult = await recordControllerOperationResult(session, "shutdown");
549
+ } finally {
550
+ disposeSessionActiveAgentLease(session);
551
+ clearTelemetrySessionRuntimeState(session, state);
552
+ retainUnresolvedTelemetryCleanup(state, session, shutdownResult);
553
+ }
554
+ }
555
+
432
556
  async function shutDownTelemetrySession(
433
557
  session: ObservMeTelemetrySession,
434
558
  event: unknown,
435
559
  ctx: ObservMeHandlerContext,
436
560
  state: HandlerSessionState,
437
- ): Promise<void> {
561
+ ): Promise<BoundedOtelOperationResult> {
438
562
  const labels = metricLabels(session.config, session.lineage);
439
563
  const shutdownAttributes = buildShutdownAttributes(event, session);
440
564
  const failed = workflowFailed(event);
565
+ let shutdownResult: BoundedOtelOperationResult | undefined;
441
566
 
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");
567
+ try {
568
+ deactivateSessionActiveAgent(session, labels);
569
+ session.metrics.sessionsShutdown.add(1, labels);
570
+ recordWorkflowShutdownTelemetry(session, shutdownAttributes, failed, labels);
571
+ endAllActiveSpans(session);
572
+ session.sessionSpan?.addEvent(LOG_EVENT_NAMES.SESSION_SHUTDOWN, shutdownAttributes);
573
+ if (failed) session.sessionSpan?.setStatus({ code: SpanStatusCode.ERROR });
574
+ endActiveSpan(session, session.sessionSpan);
575
+ await clearExtensionStatus(ctx);
576
+ await recordControllerOperationResult(session, "flush");
577
+ shutdownResult = await recordControllerOperationResult(session, "shutdown");
578
+ return shutdownResult;
579
+ } finally {
580
+ disposeSessionActiveAgentLease(session);
581
+ clearTelemetrySessionRuntimeState(session, state);
582
+ retainUnresolvedTelemetryCleanup(state, session, shutdownResult);
583
+ }
584
+ }
585
+
586
+ function activateSessionActiveAgent(
587
+ session: ObservMeTelemetrySession,
588
+ labels: Record<string, string>,
589
+ ): void {
590
+ if (session.activeAgentRecorded) return;
591
+
592
+ session.metrics.activeAgents.add(1, labels);
593
+ session.activeAgentRecorded = true;
594
+ session.activeAgentLease?.activate();
595
+ }
596
+
597
+ function deactivateSessionActiveAgent(
598
+ session: ObservMeTelemetrySession,
599
+ labels: Record<string, string>,
600
+ ): void {
601
+ session.activeAgentLease?.deactivate();
602
+ if (!session.activeAgentRecorded) return;
603
+
604
+ session.activeAgentRecorded = false;
605
+ session.metrics.activeAgents.add(-1, labels);
606
+ }
607
+
608
+ function disposeSessionActiveAgentLease(session: ObservMeTelemetrySession): void {
609
+ session.activeAgentLease?.dispose();
610
+ }
611
+
612
+ function clearTelemetrySessionRuntimeState(
613
+ session: ObservMeTelemetrySession,
614
+ state: HandlerSessionState,
615
+ ): void {
452
616
  clearObsSessionRuntimeState();
453
617
  clearObsAgentsRuntimeState();
618
+ if (state.session === session) state.session = undefined;
619
+ }
620
+
621
+ function clearDisabledTelemetryRuntimeState(state: HandlerSessionState): void {
454
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
+
654
+ async function clearExtensionStatus(ctx: ObservMeHandlerContext): Promise<void> {
655
+ try {
656
+ await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, undefined);
657
+ } catch {
658
+ return;
659
+ }
455
660
  }
456
661
 
457
662
  async function recordControllerOperationResult(
458
663
  session: ObservMeTelemetrySession,
459
664
  operation: BoundedOtelOperationResult["operation"],
460
- ): Promise<void> {
665
+ ): Promise<BoundedOtelOperationResult> {
461
666
  const result = await runControllerOperation(session, operation);
462
667
  recordObsStatusExportResult(result);
463
668
  recordExportOperationResult(session, result);
669
+ return result;
464
670
  }
465
671
 
466
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