@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,16 +1,17 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { performance } from "node:perf_hooks";
3
3
  import type { Histogram, Span } from "@opentelemetry/api";
4
- import { trace } from "@opentelemetry/api";
5
4
  import type { ObservMeConfig } from "../config/schema.ts";
6
- import { ObservMeLogSdk } from "../otel/logs.ts";
7
- import { ObservMeMetricSdk } from "../otel/metrics.ts";
8
- import { startOtelSdk } from "../otel/sdk.ts";
9
- import { ObservMeTraceSdk } from "../otel/traces.ts";
5
+ import { createLogSessionScopedOtelSdk, type ObservMeLogSdk } from "../otel/logs.ts";
6
+ import { createMetricSessionScopedOtelSdk, type ObservMeMetricSdk } from "../otel/metrics.ts";
7
+ import { startOtelSdk, toOtelStartupError, type SessionScopedOtelSdk } from "../otel/sdk.ts";
8
+ import { runBoundedOtelOperation } from "../otel/shutdown.ts";
9
+ import { createTraceSessionScopedOtelSdk, type ObservMeTraceSdk } from "../otel/traces.ts";
10
10
  import { inheritTenantSaltEnvironment } from "../privacy/hash.ts";
11
11
  import { LOG_ATTRIBUTES, RESOURCE_ATTRIBUTES } from "../semconv/attributes.ts";
12
12
  import {
13
13
  LOG_EVENT_NAMES,
14
+ OBSERVME_AGENT_LEASE_METRIC_OPTIONS,
14
15
  OBSERVME_COUNTER_METRIC_NAMES,
15
16
  OBSERVME_GAUGE_METRIC_NAMES,
16
17
  OBSERVME_HISTOGRAM_METRIC_NAMES,
@@ -18,6 +19,7 @@ import {
18
19
  OFFICIAL_GENAI_METRIC_NAMES,
19
20
  } from "../semconv/metrics.ts";
20
21
  import { BoundedMap, type BoundedMapEviction } from "../util/bounded-map.ts";
22
+ import { createActiveAgentLease } from "./active-agent-lease.ts";
21
23
  import type { AgentLineageContext } from "./agent-lineage.ts";
22
24
  import { buildResourceLineageAttributes } from "./agent-lineage.ts";
23
25
  import type { AgentTreeNode } from "./agent-tree-tracker.ts";
@@ -30,6 +32,7 @@ import {
30
32
  evictToolCallState,
31
33
  evictWaitJoinState,
32
34
  isRecord,
35
+ metricLabels,
33
36
  normalizeMetricValue,
34
37
  readBoolean,
35
38
  readString,
@@ -46,9 +49,12 @@ import type {
46
49
  HandlerRegistration,
47
50
  HandlerSessionState,
48
51
  ObservMeHandlerContext,
52
+ PiEventName,
53
+ PiHandler,
49
54
  ObservMeMetrics,
50
55
  ObservMePiApi,
51
56
  ObservMeTelemetrySession,
57
+ RuntimeHandler,
52
58
  SpanRegistry,
53
59
  StartSessionTelemetryOptions,
54
60
  TelemetryMeter,
@@ -63,6 +69,7 @@ const piApiCompatibilityErrorMessage =
63
69
  "ObservMe/Pi API compatibility error: expected Pi ExtensionAPI with on(eventName, handler) before registering ObservMe handlers.";
64
70
  const eventRegistrationOrder = [
65
71
  "session_start",
72
+ "session_info_changed",
66
73
  "agent_start",
67
74
  "turn_start",
68
75
  "before_provider_request",
@@ -73,18 +80,15 @@ const eventRegistrationOrder = [
73
80
  "tool_result",
74
81
  "tool_execution_end",
75
82
  "user_bash",
76
- "bashExecution",
77
83
  "model_select",
78
- "model_change",
79
84
  "thinking_level_select",
80
- "thinking_level_change",
81
85
  "session_before_tree",
82
86
  "session_tree",
83
87
  "session_compact",
84
88
  "turn_end",
85
89
  "agent_end",
86
90
  "session_shutdown",
87
- ] as const;
91
+ ] as const satisfies readonly PiEventName[];
88
92
  const eventRegistrationIndexes = createEventRegistrationIndexes();
89
93
 
90
94
  let defaultHandlerErrorRecorder: HandlerErrorRecorder = recordInternalErrorFallback;
@@ -101,8 +105,8 @@ export class HandlerRegistrar {
101
105
  this.#errorRecorder = errorRecorder;
102
106
  }
103
107
 
104
- add(eventName: string, handler: Handler): void {
105
- this.#registrations.push({ eventName, handler });
108
+ add<Name extends PiEventName>(eventName: Name, handler: PiHandler<Name>): void {
109
+ this.#registrations.push({ eventName, handler: handler as unknown as RuntimeHandler });
106
110
  }
107
111
 
108
112
  commit(): void {
@@ -122,11 +126,12 @@ export class HandlerRegistrar {
122
126
  export class SerializedLifecycleQueue {
123
127
  #previous = Promise.resolve();
124
128
 
125
- wrap(fn: Handler): Handler {
126
- return runSerializedLifecycleHandler.bind(undefined, this, fn);
129
+ wrap<Name extends PiEventName>(fn: PiHandler<Name>): PiHandler<Name> {
130
+ const runtimeHandler = fn as unknown as RuntimeHandler;
131
+ return runSerializedLifecycleHandler.bind(undefined, this, runtimeHandler) as PiHandler<Name>;
127
132
  }
128
133
 
129
- run(fn: Handler, event: unknown, ctx: ObservMeHandlerContext): Promise<void> {
134
+ run(fn: RuntimeHandler, event: unknown, ctx: Parameters<RuntimeHandler>[1]): Promise<void> {
130
135
  // Pi docs guarantee /new, /resume, and /fork emit session_shutdown before session_start;
131
136
  // ctx.reload() emits session_shutdown then session_start while the old command call frame can
132
137
  // continue. ObservMe queues both lifecycle handlers together so async startup, shutdown,
@@ -143,19 +148,30 @@ export function resolveObservMePiApi(pi: unknown): ObservMePiApi {
143
148
  const on = pi.on;
144
149
  if (typeof on !== "function") throw new TypeError(piApiCompatibilityErrorMessage);
145
150
 
146
- return { on: callPiOn.bind(undefined, pi, on as ObservMePiApi["on"]) };
151
+ const appendEntry = typeof pi.appendEntry === "function"
152
+ ? (pi.appendEntry.bind(pi) as NonNullable<ObservMePiApi["appendEntry"]>)
153
+ : undefined;
154
+ const getThinkingLevel = typeof pi.getThinkingLevel === "function"
155
+ ? (pi.getThinkingLevel.bind(pi) as NonNullable<ObservMePiApi["getThinkingLevel"]>)
156
+ : undefined;
157
+
158
+ return {
159
+ on: on.bind(pi) as ObservMePiApi["on"],
160
+ appendEntry,
161
+ getThinkingLevel,
162
+ };
147
163
  }
148
164
 
149
165
  export function setDefaultHandlerErrorRecorder(recorder: HandlerErrorRecorder): void {
150
166
  defaultHandlerErrorRecorder = recorder;
151
167
  }
152
168
 
153
- export function safeHandler(
169
+ export function safeHandler<Event = unknown, Context = ObservMeHandlerContext>(
154
170
  name: string,
155
- fn: Handler,
171
+ fn: Handler<Event, Context>,
156
172
  recorder: HandlerErrorRecorder = defaultHandlerErrorRecorder,
157
- ): Handler {
158
- return runSafeHandler.bind(undefined, name, fn, recorder);
173
+ ): Handler<Event, Context> {
174
+ return (runSafeHandler<Event, Context>).bind(undefined, name, fn, recorder);
159
175
  }
160
176
 
161
177
  export function createStatefulHandlerErrorRecorder(
@@ -166,20 +182,37 @@ export function createStatefulHandlerErrorRecorder(
166
182
  }
167
183
 
168
184
  export async function startSessionTelemetry(options: StartSessionTelemetryOptions): Promise<ObservMeTelemetrySession> {
185
+ if (!options.config.enabled) throw new Error("ObservMe telemetry cannot start while ObservMe is disabled.");
186
+
169
187
  const config = withTelemetrySessionResourceAttributes(options.config, options.lineage);
170
- const traceSdk = new ObservMeTraceSdk({ config });
171
- const metricSdk = new ObservMeMetricSdk({ config });
172
- const logSdk = new ObservMeLogSdk({ config });
173
- const signalSdk = createCompositeOtelSignalSdk(traceSdk, metricSdk, logSdk);
188
+ const traceSdk = createTraceSessionScopedOtelSdk({ config, agent: options.lineage });
189
+ const metricSdk = createMetricSessionScopedOtelSdk({ config, agent: options.lineage });
190
+ const logSdk = createLogSessionScopedOtelSdk({ config, agent: options.lineage });
191
+ const signalSdk = createCompositeOtelSignalSdk(
192
+ traceSdk,
193
+ metricSdk,
194
+ logSdk,
195
+ config.shutdown.flushTimeoutMs,
196
+ );
174
197
  const controller = await startOtelSdk({
175
198
  config,
176
199
  agent: options.lineage,
177
200
  sdkFactory: returnCompositeSignalSdk.bind(undefined, signalSdk),
178
201
  });
179
- const tracer = traceSdk.tracer ?? trace.getTracer("@senad-d/observme");
202
+ const tracer = traceSdk.tracer;
180
203
  const metrics = createObservMeMetrics(metricSdk.meter);
181
204
  const sessionReference: HandlerSessionState = {};
182
205
  const getTelemetryDropTarget = resolveSessionTelemetryDropTarget.bind(undefined, sessionReference, metrics);
206
+ const spans = createSpanRegistry(config, metrics, getTelemetryDropTarget);
207
+ const agentTree = createAgentTreeTracker(config, options.lineage, metrics, getTelemetryDropTarget);
208
+ const turnSequences = createTurnSequenceRegistry(config, metrics, getTelemetryDropTarget);
209
+ const activeAgentLease = createActiveAgentLease({
210
+ instrument: metrics.agentLeaseExpiresUnixTimeSeconds,
211
+ leaseDurationMillis: config.metrics.activeAgentLeaseDurationMillis,
212
+ attributes: metricLabels(config, options.lineage),
213
+ wallClockNow: options.wallClockNow,
214
+ enabled: config.metrics.enabled,
215
+ });
183
216
  const session: ObservMeTelemetrySession = {
184
217
  config,
185
218
  lineage: options.lineage,
@@ -188,14 +221,15 @@ export async function startSessionTelemetry(options: StartSessionTelemetryOption
188
221
  meter: metricSdk.meter,
189
222
  logger: logSdk.logger,
190
223
  metrics,
191
- spans: createSpanRegistry(config, metrics, getTelemetryDropTarget),
192
- agentTree: createAgentTreeTracker(config, options.lineage, metrics, getTelemetryDropTarget),
224
+ spans,
225
+ activeAgentLease,
226
+ agentTree,
193
227
  now: options.now ?? monotonicNowMs,
194
228
  activeAgentRecorded: false,
195
229
  agentRunSequence: 0,
196
230
  llmRequestSequence: 0,
197
231
  toolCallSequence: 0,
198
- turnSequences: createTurnSequenceRegistry(config, metrics, getTelemetryDropTarget),
232
+ turnSequences,
199
233
  };
200
234
 
201
235
  sessionReference.session = session;
@@ -206,8 +240,9 @@ export function createCompositeOtelSignalSdk(
206
240
  traceSdk: ObservMeTraceSdk,
207
241
  metricSdk: ObservMeMetricSdk,
208
242
  logSdk: ObservMeLogSdk,
243
+ cleanupTimeoutMs = 3_000,
209
244
  ): CompositeOtelSignalSdk {
210
- return new ObservMeCompositeOtelSignalSdk(traceSdk, metricSdk, logSdk);
245
+ return new ObservMeCompositeOtelSignalSdk(traceSdk, metricSdk, logSdk, cleanupTimeoutMs);
211
246
  }
212
247
 
213
248
  export function createObservMeMetrics(meter: TelemetryMeter): ObservMeMetrics {
@@ -252,6 +287,10 @@ export function createObservMeMetrics(meter: TelemetryMeter): ObservMeMetrics {
252
287
  eventsObserved: meter.createCounter(OBSERVME_COUNTER_METRIC_NAMES.EVENTS_OBSERVED_TOTAL),
253
288
  activeSpans: meter.createUpDownCounter(OBSERVME_GAUGE_METRIC_NAMES.ACTIVE_SPANS),
254
289
  activeAgents: meter.createUpDownCounter(OBSERVME_GAUGE_METRIC_NAMES.ACTIVE_AGENTS),
290
+ agentLeaseExpiresUnixTimeSeconds: meter.createObservableGauge(
291
+ OBSERVME_GAUGE_METRIC_NAMES.AGENT_LEASE_EXPIRES_UNIXTIME_SECONDS,
292
+ OBSERVME_AGENT_LEASE_METRIC_OPTIONS,
293
+ ),
255
294
  workflowDurationMs: createHistogram(meter, OBSERVME_HISTOGRAM_METRIC_NAMES.WORKFLOW_DURATION_MS),
256
295
  agentRunDurationMs: createHistogram(meter, OBSERVME_HISTOGRAM_METRIC_NAMES.AGENT_RUN_DURATION_MS),
257
296
  agentLifetimeDurationMs: createHistogram(meter, OBSERVME_HISTOGRAM_METRIC_NAMES.AGENT_LIFETIME_DURATION_MS),
@@ -378,8 +417,8 @@ export function buildTelemetryInstanceResourceAttributes(instanceId: string): Re
378
417
  };
379
418
  }
380
419
 
381
- function createEventRegistrationIndexes(): Map<string, number> {
382
- const indexes = new Map<string, number>();
420
+ function createEventRegistrationIndexes(): Map<PiEventName, number> {
421
+ const indexes = new Map<PiEventName, number>();
383
422
  for (const [index, name] of eventRegistrationOrder.entries()) indexes.set(name, index);
384
423
  return indexes;
385
424
  }
@@ -392,30 +431,30 @@ function compareHandlerRegistrations(left: HandlerRegistration, right: HandlerRe
392
431
  return registrationIndex(left.eventName) - registrationIndex(right.eventName);
393
432
  }
394
433
 
395
- function registrationIndex(eventName: string): number {
434
+ function registrationIndex(eventName: PiEventName): number {
396
435
  return eventRegistrationIndexes.get(eventName) ?? eventRegistrationOrder.length;
397
436
  }
398
437
 
399
438
  function registerObservedHandler(
400
439
  api: ObservMePiApi,
401
- name: string,
402
- handler: Handler,
440
+ name: PiEventName,
441
+ handler: RuntimeHandler,
403
442
  state: HandlerSessionState,
404
443
  errorRecorder: HandlerErrorRecorder,
405
444
  ): void {
406
445
  api.on(name, safeHandler(name, observeHandler(name, handler, state), errorRecorder));
407
446
  }
408
447
 
409
- function observeHandler(name: string, fn: Handler, state: HandlerSessionState): Handler {
448
+ function observeHandler(name: string, fn: RuntimeHandler, state: HandlerSessionState): RuntimeHandler {
410
449
  return runObservedHandler.bind(undefined, name, fn, state);
411
450
  }
412
451
 
413
452
  async function runObservedHandler(
414
453
  name: string,
415
- fn: Handler,
454
+ fn: RuntimeHandler,
416
455
  state: HandlerSessionState,
417
456
  event: unknown,
418
- ctx: ObservMeHandlerContext,
457
+ ctx: Parameters<RuntimeHandler>[1],
419
458
  ): Promise<void> {
420
459
  const startedAtMs = Date.now();
421
460
  const sessionBefore = state.session;
@@ -453,12 +492,12 @@ function recordHandlerObservation(
453
492
  session.metrics.handlerDurationMs.record(Math.max(0, Date.now() - startedAtMs), { operation, status });
454
493
  }
455
494
 
456
- async function runSafeHandler(
495
+ async function runSafeHandler<Event, Context>(
457
496
  name: string,
458
- fn: Handler,
497
+ fn: Handler<Event, Context>,
459
498
  recorder: HandlerErrorRecorder,
460
- event: unknown,
461
- ctx: ObservMeHandlerContext,
499
+ event: Event,
500
+ ctx: Context,
462
501
  ): Promise<void> {
463
502
  try {
464
503
  await fn(event, ctx);
@@ -469,14 +508,18 @@ async function runSafeHandler(
469
508
 
470
509
  async function runSerializedLifecycleHandler(
471
510
  queue: SerializedLifecycleQueue,
472
- fn: Handler,
511
+ fn: RuntimeHandler,
473
512
  event: unknown,
474
- ctx: ObservMeHandlerContext,
513
+ ctx: Parameters<RuntimeHandler>[1],
475
514
  ): Promise<void> {
476
515
  await queue.run(fn, event, ctx);
477
516
  }
478
517
 
479
- async function invokeHandler(fn: Handler, event: unknown, ctx: ObservMeHandlerContext): Promise<void> {
518
+ async function invokeHandler(
519
+ fn: RuntimeHandler,
520
+ event: unknown,
521
+ ctx: Parameters<RuntimeHandler>[1],
522
+ ): Promise<void> {
480
523
  await fn(event, ctx);
481
524
  }
482
525
 
@@ -484,10 +527,6 @@ function ignoreLifecycleQueueError(): undefined {
484
527
  return undefined;
485
528
  }
486
529
 
487
- function callPiOn(pi: Record<string, unknown>, on: ObservMePiApi["on"], eventName: string, handler: Handler): void {
488
- on.call(pi, eventName, handler);
489
- }
490
-
491
530
  function returnCompositeSignalSdk(signalSdk: CompositeOtelSignalSdk): CompositeOtelSignalSdk {
492
531
  return signalSdk;
493
532
  }
@@ -610,17 +649,46 @@ class ObservMeCompositeOtelSignalSdk implements CompositeOtelSignalSdk {
610
649
  readonly traceSdk: ObservMeTraceSdk;
611
650
  readonly metricSdk: ObservMeMetricSdk;
612
651
  readonly logSdk: ObservMeLogSdk;
613
-
614
- constructor(traceSdk: ObservMeTraceSdk, metricSdk: ObservMeMetricSdk, logSdk: ObservMeLogSdk) {
652
+ readonly #cleanupTimeoutMs: number;
653
+ readonly #startedSignalSdks: SessionScopedOtelSdk[] = [];
654
+ #startingSignalSdk?: SessionScopedOtelSdk;
655
+ #state: CompositeOtelSignalSdk["state"] = "idle";
656
+
657
+ constructor(
658
+ traceSdk: ObservMeTraceSdk,
659
+ metricSdk: ObservMeMetricSdk,
660
+ logSdk: ObservMeLogSdk,
661
+ cleanupTimeoutMs: number,
662
+ ) {
615
663
  this.traceSdk = traceSdk;
616
664
  this.metricSdk = metricSdk;
617
665
  this.logSdk = logSdk;
666
+ this.#cleanupTimeoutMs = cleanupTimeoutMs;
618
667
  }
619
668
 
620
- start(): void {
621
- this.traceSdk.start();
622
- this.metricSdk.start();
623
- this.logSdk.start();
669
+ get state(): CompositeOtelSignalSdk["state"] {
670
+ return this.#state;
671
+ }
672
+
673
+ async start(): Promise<void> {
674
+ if (this.#state === "started") return;
675
+ if (this.#state !== "idle") throw new Error(`ObservMe OTEL signal SDK cannot start from ${this.#state}.`);
676
+
677
+ this.#state = "starting";
678
+ try {
679
+ await this.startSignalSdk(this.traceSdk);
680
+ await this.startSignalSdk(this.metricSdk);
681
+ await this.startSignalSdk(this.logSdk);
682
+ this.#state = "started";
683
+ } catch (error) {
684
+ const cleanup = await runBoundedOtelOperation(
685
+ "shutdown",
686
+ this.shutdownStartedSignalSdks.bind(this),
687
+ this.#cleanupTimeoutMs,
688
+ );
689
+ this.#state = "failed";
690
+ throw toOtelStartupError(error, cleanup);
691
+ }
624
692
  }
625
693
 
626
694
  async forceFlush(): Promise<void> {
@@ -628,6 +696,43 @@ class ObservMeCompositeOtelSignalSdk implements CompositeOtelSignalSdk {
628
696
  }
629
697
 
630
698
  async shutdown(): Promise<void> {
631
- await Promise.all([this.traceSdk.shutdown(), this.metricSdk.shutdown(), this.logSdk.shutdown()]);
699
+ if (this.#state === "shutdown") return;
700
+
701
+ try {
702
+ await this.shutdownStartedSignalSdks();
703
+ } finally {
704
+ this.#state = "shutdown";
705
+ }
706
+ }
707
+
708
+ private async startSignalSdk(sdk: SessionScopedOtelSdk): Promise<void> {
709
+ this.#startingSignalSdk = sdk;
710
+ await sdk.start?.();
711
+ this.#startedSignalSdks.push(sdk);
712
+ this.#startingSignalSdk = undefined;
632
713
  }
714
+
715
+ private async shutdownStartedSignalSdks(): Promise<void> {
716
+ const signalSdks = this.takeStartedSignalSdks();
717
+ const results = await Promise.allSettled(signalSdks.map(shutdownCompositeSignalSdk));
718
+
719
+ for (const result of results) {
720
+ if (result.status === "rejected") throw result.reason;
721
+ }
722
+ }
723
+
724
+ private takeStartedSignalSdks(): SessionScopedOtelSdk[] {
725
+ const signalSdks = [...this.#startedSignalSdks];
726
+ if (this.#startingSignalSdk && !signalSdks.includes(this.#startingSignalSdk)) {
727
+ signalSdks.push(this.#startingSignalSdk);
728
+ }
729
+
730
+ this.#startedSignalSdks.length = 0;
731
+ this.#startingSignalSdk = undefined;
732
+ return signalSdks.reverse();
733
+ }
734
+ }
735
+
736
+ async function shutdownCompositeSignalSdk(sdk: SessionScopedOtelSdk): Promise<void> {
737
+ await sdk.shutdown?.();
633
738
  }
@@ -1,5 +1,10 @@
1
- import type { Counter, Histogram, Meter, Span, Tracer, UpDownCounter } from "@opentelemetry/api";
1
+ import type { Counter, Histogram, Meter, ObservableGauge, Span, Tracer, UpDownCounter } from "@opentelemetry/api";
2
2
  import type { Logger } from "@opentelemetry/api-logs";
3
+ import type {
4
+ ExtensionAPI,
5
+ ExtensionContext,
6
+ ExtensionEvent,
7
+ } from "@earendil-works/pi-coding-agent";
3
8
  import type { EnsureProjectConfig as BootstrapEnsureProjectConfig } from "../config/bootstrap-project-config.ts";
4
9
  import type {
5
10
  LoadSessionConfigOptions,
@@ -13,6 +18,7 @@ import type { ObservMeOtelSdkController } from "../otel/sdk.ts";
13
18
  import type { ObservMeTraceSdk } from "../otel/traces.ts";
14
19
  import type { BoundedMap } from "../util/bounded-map.ts";
15
20
  import type { AgentLineageContext } from "./agent-lineage.ts";
21
+ import type { ActiveAgentLeaseController } from "./active-agent-lease.ts";
16
22
  import type { AgentTreeTracker } from "./agent-tree-tracker.ts";
17
23
  import type {
18
24
  AgentWaitJoinState,
@@ -22,11 +28,24 @@ import type {
22
28
 
23
29
  export type AttributePrimitive = boolean | number | string | string[];
24
30
  export type AttributeMap = Record<string, AttributePrimitive>;
25
- export type TelemetryMeter = Pick<Meter, "createCounter" | "createHistogram" | "createUpDownCounter">;
31
+ export type TelemetryMeter = Pick<
32
+ Meter,
33
+ "createCounter" | "createHistogram" | "createObservableGauge" | "createUpDownCounter"
34
+ >;
26
35
  export type TelemetryTracer = Pick<Tracer, "startSpan">;
27
36
  export type TelemetryLogger = Pick<Logger, "emit">;
28
- export type Handler = (event: unknown, ctx: ObservMeHandlerContext) => Promise<void> | void;
37
+ export type PiEventName = ExtensionEvent["type"];
38
+ export type PiEvent<Name extends PiEventName> = Extract<ExtensionEvent, { type: Name }>;
39
+ export type Handler<Event = unknown, Context = ObservMeHandlerContext> = (
40
+ event: Event,
41
+ ctx: Context,
42
+ ) => Promise<void> | void;
43
+ export type PiHandler<Name extends PiEventName> = Handler<PiEvent<Name>, ExtensionContext>;
44
+ export type RuntimeHandler = Handler<unknown, ExtensionContext>;
29
45
  export type HandlerErrorRecorder = (name: string, error: unknown) => void;
46
+ export type AppendEntry = ExtensionAPI["appendEntry"];
47
+ export type GetThinkingLevel = ExtensionAPI["getThinkingLevel"];
48
+ export type ObservMeSessionManager = ExtensionContext["sessionManager"];
30
49
  export type LoadSessionConfig = (
31
50
  options: LoadSessionConfigOptions,
32
51
  ) => Promise<ObservMeConfig | LoadSessionConfigResult>;
@@ -52,7 +71,6 @@ export interface SessionRecoveryHeader {
52
71
  readonly timestamp?: string;
53
72
  readonly cwd?: string;
54
73
  readonly parentSession?: string;
55
- readonly correlation?: MinimalSessionCorrelation;
56
74
  }
57
75
 
58
76
  export interface StartupRecoveryState {
@@ -63,24 +81,21 @@ export interface StartupRecoveryState {
63
81
  }
64
82
 
65
83
  export interface ObservMeHandlerContext {
66
- readonly cwd?: string;
67
- readonly hasUI?: boolean;
68
- readonly sessionFile?: string;
69
- readonly session_file?: string;
70
- readonly sessionId?: string;
71
- readonly session_id?: string;
72
- readonly model?: unknown;
73
- readonly thinking?: unknown;
84
+ readonly cwd?: ExtensionContext["cwd"];
85
+ readonly hasUI?: ExtensionContext["hasUI"];
86
+ readonly sessionManager?: ObservMeSessionManager;
87
+ readonly model?: Exclude<ExtensionContext["model"], undefined>;
74
88
  readonly ui?: {
75
89
  notify?: (message: string, level?: "warning" | "info" | "error") => Promise<void> | void;
76
90
  setStatus?: (key: string, value: string | undefined) => Promise<void> | void;
77
91
  };
78
92
  readonly isProjectTrusted?: () => boolean | Promise<boolean>;
79
- readonly [key: string]: unknown;
80
93
  }
81
94
 
82
95
  export interface ObservMePiApi {
83
- on: (eventName: string, handler: Handler) => void;
96
+ on: <Name extends PiEventName>(eventName: Name, handler: PiHandler<Name>) => void;
97
+ appendEntry?: AppendEntry;
98
+ getThinkingLevel?: GetThinkingLevel;
84
99
  }
85
100
 
86
101
  export interface RegisterHandlersOptions {
@@ -88,18 +103,22 @@ export interface RegisterHandlersOptions {
88
103
  readonly startTelemetry?: StartSessionTelemetry;
89
104
  readonly env?: NodeJS.ProcessEnv;
90
105
  readonly now?: () => number;
106
+ readonly wallClockNow?: () => number;
91
107
  readonly configDirName?: string;
92
108
  readonly trustedParentContext?: boolean;
93
109
  readonly requireCompleteParentEnvelope?: boolean;
94
110
  readonly readSessionHeader?: ReadSessionHeader;
95
111
  readonly ensureProjectConfig?: EnsureProjectConfig;
96
112
  readonly onHandlerError?: HandlerErrorRecorder;
113
+ readonly appendEntry?: AppendEntry;
114
+ readonly getThinkingLevel?: GetThinkingLevel;
97
115
  }
98
116
 
99
117
  export interface StartSessionTelemetryOptions {
100
118
  readonly config: ObservMeConfig;
101
119
  readonly lineage: AgentLineageContext;
102
120
  readonly now?: () => number;
121
+ readonly wallClockNow?: () => number;
103
122
  }
104
123
 
105
124
  export interface TurnSequenceRegistry {
@@ -119,6 +138,7 @@ export interface ObservMeTelemetrySession {
119
138
  readonly logger: TelemetryLogger;
120
139
  readonly metrics: ObservMeMetrics;
121
140
  readonly spans: SpanRegistry;
141
+ readonly activeAgentLease?: ActiveAgentLeaseController;
122
142
  agentTree: AgentTreeTracker;
123
143
  sessionSpan?: Span;
124
144
  sessionAttributes?: AttributeMap;
@@ -179,6 +199,7 @@ export interface ObservMeMetrics {
179
199
  readonly eventsObserved: Counter;
180
200
  readonly activeSpans: UpDownCounter;
181
201
  readonly activeAgents: UpDownCounter;
202
+ readonly agentLeaseExpiresUnixTimeSeconds: ObservableGauge;
182
203
  readonly workflowDurationMs: Histogram;
183
204
  readonly agentRunDurationMs: Histogram;
184
205
  readonly agentLifetimeDurationMs: Histogram;
@@ -241,18 +262,20 @@ export interface CompositeOtelSignalSdk {
241
262
  readonly traceSdk: ObservMeTraceSdk;
242
263
  readonly metricSdk: ObservMeMetricSdk;
243
264
  readonly logSdk: ObservMeLogSdk;
244
- start: () => void;
265
+ readonly state: "idle" | "starting" | "started" | "failed" | "shutdown";
266
+ start: () => Promise<void>;
245
267
  forceFlush: () => Promise<void>;
246
268
  shutdown: () => Promise<void>;
247
269
  }
248
270
 
249
271
  export interface HandlerSessionState {
250
272
  session?: ObservMeTelemetrySession;
273
+ pendingCleanup?: ObservMeTelemetrySession;
251
274
  }
252
275
 
253
276
  export interface HandlerRegistration {
254
- readonly eventName: string;
255
- readonly handler: Handler;
277
+ readonly eventName: PiEventName;
278
+ readonly handler: RuntimeHandler;
256
279
  }
257
280
 
258
281
  export interface SessionConfigLoadResult {
@@ -31,11 +31,13 @@ export {
31
31
  workflowFailed,
32
32
  } from "./handler-runtime.ts";
33
33
  export type {
34
+ AppendEntry,
34
35
  AttributeMap,
35
36
  AttributePrimitive,
36
37
  BranchPreparationState,
37
38
  CompositeOtelSignalSdk,
38
39
  EnsureProjectConfig,
40
+ GetThinkingLevel,
39
41
  Handler,
40
42
  HandlerErrorRecorder,
41
43
  LoadSessionConfig,
@@ -43,8 +45,12 @@ export type {
43
45
  ObservMeHandlerContext,
44
46
  ObservMeMetrics,
45
47
  ObservMePiApi,
48
+ ObservMeSessionManager,
46
49
  ObservMeTelemetrySession,
47
50
  PendingBashOperationState,
51
+ PiEvent,
52
+ PiEventName,
53
+ PiHandler,
48
54
  ReadSessionHeader,
49
55
  RegisterHandlersOptions,
50
56
  SessionRecoveryHeader,
@@ -65,9 +71,14 @@ export function registerHandlers(pi: unknown, options: RegisterHandlersOptions =
65
71
  const errorRecorder = createStatefulHandlerErrorRecorder(state, options.onHandlerError);
66
72
  const registrar = new HandlerRegistrar(api, state, errorRecorder);
67
73
  const lifecycleQueue = new SerializedLifecycleQueue();
74
+ const runtimeOptions = {
75
+ ...options,
76
+ appendEntry: options.appendEntry ?? api.appendEntry,
77
+ getThinkingLevel: options.getThinkingLevel ?? api.getThinkingLevel,
78
+ };
68
79
 
69
80
  setDefaultHandlerErrorRecorder(errorRecorder);
70
- registerLifecycleHandlers(registrar, state, options, lifecycleQueue);
81
+ registerLifecycleHandlers(registrar, state, runtimeOptions, lifecycleQueue);
71
82
  registerAgentTurnHandlers(registrar, state);
72
83
  registerLlmHandlers(registrar, state);
73
84
  registerToolBashHandlers(registrar, state);