@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,13 +1,13 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  requestObservMeIntegration,
4
- type ObservMeChildStatus,
5
4
  type ObservMeIntegrationApi,
6
5
  type ObservMeJoinStatus,
7
6
  type ObservMeProcessEnvironment,
8
7
  type ObservMeSpawnReason,
9
8
  type ObservMeSpawnType,
10
9
  type ObservMeStartedSubagent,
10
+ type ObservMeTerminalChildStatus,
11
11
  } from "@senad-d/observme/integration";
12
12
 
13
13
  /**
@@ -70,7 +70,6 @@ export class ObservableSubagentRunner<Request, Handle, Value> {
70
70
 
71
71
  try {
72
72
  handle = await this.#transport.launch(options.request, context, options.signal);
73
- completeObservMeLaunch(observme, started);
74
73
  } catch (error) {
75
74
  failObservMeLaunch(observme, started, error);
76
75
  throw error;
@@ -90,10 +89,12 @@ export class ObservableSubagentRunner<Request, Handle, Value> {
90
89
  try {
91
90
  const result = await this.#transport.wait(handle, signal);
92
91
  endObservMeWait(observme, started, wait, result);
92
+ completeObservMeChild(observme, started, childStatus(result.status));
93
93
  recordObservMeJoin(observme, started, result);
94
94
  return result;
95
95
  } catch (error) {
96
96
  endObservMeWaitFailure(observme, started, wait);
97
+ completeObservMeChild(observme, started, "failed");
97
98
  recordObservMeJoinFailure(observme, started);
98
99
  throw error;
99
100
  }
@@ -127,14 +128,16 @@ function createLaunchContext(
127
128
  };
128
129
  }
129
130
 
130
- function completeObservMeLaunch(
131
+ function completeObservMeChild(
131
132
  observme: ObservMeIntegrationApi | undefined,
132
133
  started: ObservMeStartedSubagent | undefined,
134
+ status: ObservMeTerminalChildStatus,
133
135
  ): void {
134
136
  if (!observme || !started) return;
135
137
  observme.completeSubagent(started.spawnId, {
136
138
  childAgentId: started.childAgentId,
137
- childStatus: "active",
139
+ childStatus: status,
140
+ outcome: status,
138
141
  });
139
142
  }
140
143
 
@@ -245,7 +248,7 @@ function recordObservMeJoinFailure(
245
248
  });
246
249
  }
247
250
 
248
- function childStatus(status: ChildRunResult<unknown>["status"]): ObservMeChildStatus {
251
+ function childStatus(status: ChildRunResult<unknown>["status"]): ObservMeTerminalChildStatus {
249
252
  if (status === "completed") return "completed";
250
253
  if (status === "cancelled") return "cancelled";
251
254
  return "failed";
@@ -6,7 +6,6 @@ observme:
6
6
  enabled: true
7
7
  environment: development
8
8
  tenant: local-dev
9
- replayOnStart: false
10
9
 
11
10
  otlp:
12
11
  endpoint: http://localhost:4318
@@ -14,7 +13,6 @@ observme:
14
13
  timeoutMs: 3000
15
14
  headers: {}
16
15
  tls:
17
- enabled: false
18
16
  insecureSkipVerify: false
19
17
  signalEndpoints:
20
18
  traces: http://localhost:4318/v1/traces
@@ -46,6 +44,8 @@ observme:
46
44
  propagateTraceContext: true
47
45
  propagateToSubagents: true
48
46
  capabilityEnv: OBSERVME_AGENT_CAPABILITY
47
+ # Opt in to one validated, branch-local custom entry for reload/resume recovery.
48
+ # This entry never participates in LLM context.
49
49
  writeCorrelationEntry: false
50
50
 
51
51
  traces:
@@ -61,6 +61,7 @@ observme:
61
61
  enabled: true
62
62
  exportIntervalMillis: 15000
63
63
  exportTimeoutMillis: 3000
64
+ activeAgentLeaseDurationMillis: 60000
64
65
 
65
66
  logs:
66
67
  enabled: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senad-d/observme",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "ObservMe: a Pi extension that instruments Pi agent sessions and exports OpenTelemetry traces, metrics, and logs to an external observability stack (OTel Collector, Grafana Tempo/Loki/Prometheus).",
6
6
  "license": "MIT",
@@ -57,7 +57,9 @@
57
57
  "lint:fix": "eslint . --fix",
58
58
  "format:check": "node scripts/check-format.mjs",
59
59
  "test": "node --test test/*.test.mjs test/*.test.ts",
60
+ "test:active-agent-lease:contracts": "node --test test/active-agent-lease.test.ts test/alerts.test.mjs test/cardinality.test.ts test/config-defaults.test.mjs test/config-loader.test.mjs test/config-validation.test.mjs test/dashboards.test.mjs test/metrics.test.ts test/pi-handlers.test.mjs test/semconv-metrics.test.mjs",
60
61
  "test:integration:collector": "node --test test/integration/collector-debug.test.mjs",
62
+ "test:integration:active-agent-lease": "node --test test/integration/active-agent-lease.test.mjs",
61
63
  "test:integration:grafana-stack": "node --test test/integration/grafana-stack.test.mjs",
62
64
  "test:coverage": "node scripts/test-coverage.mjs",
63
65
  "smoke:discover": "node scripts/smoke-observability.mjs",
@@ -65,12 +67,20 @@
65
67
  "smoke:handlers": "node scripts/smoke-handler-execution.mjs",
66
68
  "smoke:pi-lifecycle": "node scripts/smoke-pi-lifecycle.mjs",
67
69
  "smoke:pi-runtime": "node scripts/smoke-pi-runtime.mjs",
70
+ "validate:pi-compatibility": "npm run typecheck && npm run typecheck:test && node --test test/pi-compatibility.test.ts test/event-mapping.test.ts test/template.test.mjs && npm run smoke:packaged && npm run smoke:handlers && npm run smoke:pi-lifecycle && npm run smoke:pi-runtime",
68
71
  "validate:grafana-obs": "node scripts/validate-grafana-obs-flow.mjs",
69
72
  "check": "node --check scripts/check-package-contents.mjs && node --check scripts/test-coverage.mjs && node --check scripts/smoke-observability.mjs && node --check scripts/smoke-packaged-install.mjs && node --check scripts/smoke-handler-execution.mjs && node --check scripts/smoke-pi-lifecycle.mjs && node --check scripts/smoke-pi-runtime.mjs && node --check scripts/validate-grafana-obs-flow.mjs",
70
73
  "pack:dry-run": "npm pack --dry-run --json",
71
74
  "check:pack": "node scripts/check-package-contents.mjs",
72
75
  "validate": "npm run lint && npm run test && npm run check:pack && npm run smoke:packaged && npm run smoke:handlers && npm run smoke:pi-lifecycle && npm run smoke:pi-runtime"
73
76
  },
77
+ "observmeCompatibility": {
78
+ "pi": {
79
+ "minimumVersion": "0.80.5",
80
+ "releaseTestedVersion": "0.80.6",
81
+ "supportedRange": ">=0.80.5 <0.81.0"
82
+ }
83
+ },
74
84
  "pi": {
75
85
  "extensions": [
76
86
  "./src/extension.ts"
@@ -86,8 +96,8 @@
86
96
  "typebox": "*"
87
97
  },
88
98
  "devDependencies": {
89
- "@earendil-works/pi-ai": "^0.80.2",
90
- "@earendil-works/pi-coding-agent": "^0.80.2",
99
+ "@earendil-works/pi-ai": "0.80.6",
100
+ "@earendil-works/pi-coding-agent": "0.80.6",
91
101
  "@eslint/js": "^10.0.1",
92
102
  "@types/node": "^26.1.1",
93
103
  "c8": "^11.0.0",
@@ -95,7 +105,7 @@
95
105
  "globals": "^17.7.0",
96
106
  "typebox": "^1.3.6",
97
107
  "typescript": "6.0.3",
98
- "typescript-eslint": "^8.63.0"
108
+ "typescript-eslint": "8.63.0"
99
109
  },
100
110
  "dependencies": {
101
111
  "@opentelemetry/api": "1.9.1",
@@ -6,6 +6,11 @@ import type { PrometheusFetch, PrometheusMetricSeries, QueryResult } from "../qu
6
6
  import { createPrometheusQueryClient } from "../query/prometheus.ts";
7
7
  import type { TimeRange, TraceSummary } from "../query/tempo.ts";
8
8
  import { createTempoQueryClient } from "../query/tempo.ts";
9
+ import {
10
+ boundObsCommandOutput,
11
+ normalizeObsBackendLabel,
12
+ normalizeObsBackendLabelRecord,
13
+ } from "../safety/display-bounds.ts";
9
14
  import { COMMON_SPAN_ATTRIBUTES } from "../semconv/attributes.ts";
10
15
  import { completeObsSubcommand, isExactObsSubcommandRequest } from "./obs-args.ts";
11
16
  import { loadObsCommandConfig, notifyObsCommand } from "./obs-command-support.ts";
@@ -187,8 +192,8 @@ export function renderObsAgents(snapshot: ObsAgentsSnapshot): string {
187
192
  const latestChild = readLatestChild(snapshot.children);
188
193
  const lines = [
189
194
  `Workflow: ${formatUnknown(snapshot.workflowId)} root=${formatUnknown(snapshot.workflowRootAgentId ?? snapshot.rootAgentId)}`,
190
- `Agent: ${formatUnknown(snapshot.agentId)} (${snapshot.role} depth=${snapshot.depth})`,
191
- `Session: ${formatUnknown(normalizeOptionalString(snapshot.sessionId))}`,
195
+ `Agent: ${formatUnknown(snapshot.agentId)} (${formatUnknown(snapshot.role)} depth=${snapshot.depth})`,
196
+ `Session: ${formatUnknown(snapshot.sessionId)}`,
192
197
  `Subagents spawned in current trace: ${snapshot.fanoutCount}`,
193
198
  `Current tree: depth=${snapshot.treeDepth} width=${snapshot.treeWidth} active=${snapshot.activeChildren} orphaned=${snapshot.orphanCount}`,
194
199
  `Recent children: ${renderRecentChildren(snapshot.children, snapshot.recentChildrenLimit)}`,
@@ -200,7 +205,7 @@ export function renderObsAgents(snapshot: ObsAgentsSnapshot): string {
200
205
  `Aggregate agent metrics (last ${OBS_AGENTS_WINDOW}): ${renderAggregateRows(snapshot.aggregateRows)}`,
201
206
  `Lineage drill-down: ${renderLineageDrilldown(snapshot)}`,
202
207
  );
203
- return lines.join("\n");
208
+ return boundObsCommandOutput(lines.join("\n"));
204
209
  }
205
210
 
206
211
  class ObsAgentsCommand {
@@ -322,9 +327,9 @@ function toObsAgentAggregateRow(series: PrometheusMetricSeries): ObsAgentAggrega
322
327
  if (value === undefined) return undefined;
323
328
 
324
329
  return {
325
- labels: { ...series.metric },
330
+ labels: normalizeObsBackendLabelRecord(series.metric),
326
331
  value,
327
- timestampUnixSeconds: series.value?.timestampUnixSeconds,
332
+ timestampUnixSeconds: normalizeObsBackendLabel(series.value?.timestampUnixSeconds),
328
333
  };
329
334
  }
330
335
 
@@ -379,12 +384,12 @@ function selectRecentChildrenForRender(
379
384
 
380
385
  function renderRecentChild(child: ObsAgentChildRow): string {
381
386
  const orphan = child.orphaned ? " orphaned" : "";
382
- return `${child.agentId} status=${child.status} depth=${child.depth}${orphan}`;
387
+ return `${formatUnknown(child.agentId)} status=${formatUnknown(child.status)} depth=${child.depth}${orphan}`;
383
388
  }
384
389
 
385
390
  function renderLatestChild(child: ObsAgentChildRow, hints: readonly ObsAgentWaitJoinHint[]): string {
386
391
  const joinHint = readLatestHintForChild(child.agentId, hints, "join");
387
- return `${child.agentId} status=${child.status} active=${child.activeChildren} join=${formatDuration(joinHint?.durationMs)}`;
392
+ return `${formatUnknown(child.agentId)} status=${formatUnknown(child.status)} active=${child.activeChildren} join=${formatDuration(joinHint?.durationMs)}`;
388
393
  }
389
394
 
390
395
  function readLatestHintForChild(
@@ -408,7 +413,8 @@ function renderWaitJoinHint(hint: ObsAgentWaitJoinHint | undefined): string {
408
413
  if (!hint) return "none";
409
414
 
410
415
  const status = hint.joinStatus ?? hint.childStatus ?? (hint.active ? "waiting" : "complete");
411
- return `${hint.kind}:${hint.childAgentId ?? hint.spawnId ?? hint.id} status=${status} duration=${formatDuration(hint.durationMs)}`;
416
+ const target = hint.childAgentId ?? hint.spawnId ?? hint.id;
417
+ return `${formatUnknown(hint.kind)}:${formatUnknown(target)} status=${formatUnknown(status)} duration=${formatDuration(hint.durationMs)}`;
412
418
  }
413
419
 
414
420
  function renderAggregateRows(rows: ObsAgentsAggregateRows): string {
@@ -416,7 +422,7 @@ function renderAggregateRows(rows: ObsAgentsAggregateRows): string {
416
422
  }
417
423
 
418
424
  function renderLineageDrilldown(snapshot: ObsAgentsSnapshot): string {
419
- const attrs = Object.keys(snapshot.tempoSearchAttributes).join(", ") || "none";
425
+ const attrs = Object.keys(snapshot.tempoSearchAttributes).map(formatUnknown).join(", ") || "none";
420
426
  const traceCount = snapshot.traces.length;
421
427
  const latestTrace = normalizeOptionalString(snapshot.traces[0]?.traceId) ?? normalizeOptionalString(snapshot.traceId);
422
428
  const traceSuffix = latestTrace ? ` latest_trace=${latestTrace}` : "";
@@ -454,8 +460,7 @@ function normalizeRecentChildrenLimit(value: number | undefined): number {
454
460
  }
455
461
 
456
462
  function normalizeOptionalString(value: string | undefined): string | undefined {
457
- const trimmed = value?.trim();
458
- return trimmed || undefined;
463
+ return normalizeObsBackendLabel(value);
459
464
  }
460
465
 
461
466
  function isObsAgentsRequest(args: string): boolean {
@@ -463,7 +468,7 @@ function isObsAgentsRequest(args: string): boolean {
463
468
  }
464
469
 
465
470
  function formatUnknown(value: string | undefined): string {
466
- return value ?? "unknown";
471
+ return normalizeObsBackendLabel(value) ?? "unknown";
467
472
  }
468
473
 
469
474
  function formatDuration(value: number | undefined): string {
@@ -2,7 +2,7 @@ import { setTimeout as delay } from "node:timers/promises";
2
2
  import type { ExtensionAPI, SessionEntry, SessionHeader } from "@earendil-works/pi-coding-agent";
3
3
  import type { LoadSessionConfigOptions } from "../config/load-config.ts";
4
4
  import type { ObservMeConfig } from "../config/schema.ts";
5
- import { ObservMeLogSdk } from "../otel/logs.ts";
5
+ import { type ObservMeLogSdk, createLogSessionScopedOtelSdk } from "../otel/logs.ts";
6
6
  import type { BoundedOtelOperationResult } from "../otel/shutdown.ts";
7
7
  import { flushOtelSdk, shutdownOtelSdk } from "../otel/shutdown.ts";
8
8
  import { applyContentCapturePolicy } from "../privacy/content-capture.ts";
@@ -308,7 +308,7 @@ export function createObsBackfillLogExporter(
308
308
  config: ObservMeConfig,
309
309
  options: ObsBackfillOperationOptions = {},
310
310
  ): ObsBackfillExporter {
311
- const sdk = new ObservMeLogSdk({ config });
311
+ const sdk = createLogSessionScopedOtelSdk({ config });
312
312
  sdk.start();
313
313
  return new ObsBackfillLogExporter(sdk, resolveBackfillOperationTimeoutMs(options, OBS_BACKFILL_DEFAULT_OPERATION_TIMEOUT_MS));
314
314
  }
@@ -1,6 +1,7 @@
1
1
  import type { LoadSessionConfigOptions } from "../config/load-config.ts";
2
2
  import { loadSessionConfig } from "../config/load-config.ts";
3
3
  import type { ObservMeConfig } from "../config/schema.ts";
4
+ import { boundObsCommandOutput } from "../safety/display-bounds.ts";
4
5
 
5
6
  export type ObsCommandNotificationType = "info" | "warning" | "error";
6
7
 
@@ -33,7 +34,7 @@ export async function notifyObsCommand(
33
34
  message: string,
34
35
  type: ObsCommandNotificationType,
35
36
  ): Promise<void> {
36
- await ctx.ui?.notify?.(message, type);
37
+ await ctx.ui?.notify?.(boundObsCommandOutput(message), type);
37
38
  }
38
39
 
39
40
  export function normalizeObsCommandTimeoutMs(value: number | undefined, fallback: number, invalidFallback = fallback): number {
@@ -3,6 +3,11 @@ import type { LoadSessionConfigOptions } from "../config/load-config.ts";
3
3
  import type { ObservMeConfig } from "../config/schema.ts";
4
4
  import type { PrometheusFetch, PrometheusMetricSeries, QueryResult } from "../query/prometheus.ts";
5
5
  import { createPrometheusQueryClient } from "../query/prometheus.ts";
6
+ import {
7
+ boundObsCommandOutput,
8
+ normalizeObsBackendLabel,
9
+ selectObsCommandRows,
10
+ } from "../safety/display-bounds.ts";
6
11
  import { completeObsSubcommand, parseObsSubcommandArgs } from "./obs-args.ts";
7
12
  import { loadObsCommandConfig, notifyObsCommand } from "./obs-command-support.ts";
8
13
  import { appendObsRecoveryHint, formatObsCommandFailure } from "./obs-diagnostics.ts";
@@ -114,15 +119,19 @@ export async function getObsCostSnapshot(
114
119
 
115
120
  export function renderObsCost(snapshot: ObsCostSnapshot): string {
116
121
  const rows = snapshot.rows.map(normalizeObsCostRow).filter(isObsCostRow);
117
- const lines = [`Cost by model/provider (last ${snapshot.window})`];
122
+ const selection = selectObsCommandRows(rows);
123
+ const window = normalizeObsBackendLabel(snapshot.window) ?? OBS_COST_WINDOW;
124
+ const lines = [`Cost by model/provider (last ${window})`];
118
125
 
119
126
  if (rows.length === 0) {
120
127
  lines.push(appendObsRecoveryHint("No cost metrics found.", OBS_COST_NO_METRICS_NEXT_ACTION));
121
- return lines.join("\n");
128
+ return boundObsCommandOutput(lines.join("\n"));
122
129
  }
123
130
 
124
- lines.push(...rows.map(renderObsCostRow), `Total: ${formatUsd(sumObsCostRows(rows))}`);
125
- return lines.join("\n");
131
+ lines.push(...selection.rows.map(renderObsCostRow));
132
+ if (selection.omittedCount > 0) lines.push(`… ${selection.omittedCount} cost row(s) omitted`);
133
+ lines.push(`Total: ${formatUsd(sumObsCostRows(rows))}`);
134
+ return boundObsCommandOutput(lines.join("\n"));
126
135
  }
127
136
 
128
137
  class ObsCostCommand {
@@ -187,12 +196,11 @@ function parseCostUsd(value: string | number | undefined): number | undefined {
187
196
  }
188
197
 
189
198
  function normalizeMetricLabel(value: string | undefined): string {
190
- return normalizeOptionalString(value) ?? "unknown";
199
+ return normalizeObsBackendLabel(value) ?? "unknown";
191
200
  }
192
201
 
193
202
  function normalizeOptionalString(value: string | undefined): string | undefined {
194
- const trimmed = value?.trim();
195
- return trimmed || undefined;
203
+ return normalizeObsBackendLabel(value);
196
204
  }
197
205
 
198
206
  function renderObsCostRow(row: ObsCostRow): string {
@@ -1,6 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { LoadSessionConfigOptions } from "../config/load-config.ts";
3
3
  import type { ObservMeConfig } from "../config/schema.ts";
4
+ import {
5
+ createObsTransportSecuritySnapshot,
6
+ type ObsTransportSecuritySnapshot,
7
+ } from "../config/transport-security.ts";
4
8
  import { readDiagnosticMessage, sanitizeDiagnosticText } from "../diagnostics/sanitize.ts";
5
9
  import { getGrafanaHealth, type GrafanaFetch } from "../query/grafana.ts";
6
10
  import { completeObsSubcommand, isExactObsSubcommandRequest } from "./obs-args.ts";
@@ -31,6 +35,7 @@ export interface ObsHealthCheckResult {
31
35
  export interface ObsHealthSnapshot {
32
36
  readonly timeoutMs: number;
33
37
  readonly checks: readonly ObsHealthCheckResult[];
38
+ readonly transportSecurity?: ObsTransportSecuritySnapshot;
34
39
  }
35
40
 
36
41
  export interface ObsHealthSnapshotOptions {
@@ -112,11 +117,18 @@ export async function getObsHealthSnapshot(
112
117
  getGrafanaHealth(config, { fetch: options.fetch, timeoutMs }),
113
118
  ]);
114
119
 
115
- return { timeoutMs, checks: [collectorCheck, ...grafanaHealth.checks] };
120
+ return {
121
+ timeoutMs,
122
+ checks: [collectorCheck, ...grafanaHealth.checks],
123
+ transportSecurity: createObsTransportSecuritySnapshot(config),
124
+ };
116
125
  }
117
126
 
118
127
  export function renderObsHealth(snapshot: ObsHealthSnapshot): string {
119
- return snapshot.checks.map(renderObsHealthCheck).join("\n");
128
+ return [
129
+ ...formatObsHealthTransportSecurity(snapshot.transportSecurity),
130
+ ...snapshot.checks.map(renderObsHealthCheck),
131
+ ].join("\n");
120
132
  }
121
133
 
122
134
  class ObsHealthCommand {
@@ -229,6 +241,14 @@ function failedHealthResult(label: string, kind: ObsHealthCheckKind, error: unkn
229
241
  };
230
242
  }
231
243
 
244
+ function formatObsHealthTransportSecurity(security: ObsTransportSecuritySnapshot | undefined): string[] {
245
+ if (!security) return [];
246
+ return [
247
+ `Collector transport security: ${security.collector}`,
248
+ `Grafana transport security: ${security.grafana}`,
249
+ ];
250
+ }
251
+
232
252
  function renderObsHealthCheck(check: ObsHealthCheckResult): string {
233
253
  const status = formatObsHealthStatus(check);
234
254
  const detail = formatObsHealthDetail(check);
@@ -1,4 +1,6 @@
1
+ import { normalizeQueryResultCount } from "../config/query-limits.ts";
1
2
  import type { LogResult, TimeRange } from "../query/loki.ts";
3
+ import { normalizeObsBackendLabel } from "../safety/display-bounds.ts";
2
4
  import { LOG_ATTRIBUTES } from "../semconv/attributes.ts";
3
5
 
4
6
  export interface ObsLokiLogSummaryRow {
@@ -25,10 +27,8 @@ export interface ObsLokiTimeRangeOptions {
25
27
  }
26
28
 
27
29
  const defaultRangeHours = 1;
28
- const minimumMaxLogs = 1;
29
30
  const millisecondsPerHour = 60 * 60 * 1000;
30
31
  const nanosecondsPerMillisecond = 1_000_000n;
31
- const maximumDisplayValueLength = 96;
32
32
  const safeEventBodyPattern = /^[A-Za-z0-9_.:-]{1,96}$/u;
33
33
  const integerNanosecondsPattern = /^-?\d+$/u;
34
34
  const unknownTimestamp = "unknown-time";
@@ -51,8 +51,7 @@ export function formatObsLokiWindow(options: ObsLokiTimeRangeOptions = {}): stri
51
51
  }
52
52
 
53
53
  export function normalizeObsLokiMaxLogs(value: number | undefined): number {
54
- if (value === undefined || !Number.isFinite(value) || value < minimumMaxLogs) return minimumMaxLogs;
55
- return Math.trunc(value);
54
+ return normalizeQueryResultCount(value);
56
55
  }
57
56
 
58
57
  export function toObsLokiLogSummaryRow(log: LogResult): ObsLokiLogSummaryRow {
@@ -140,10 +139,7 @@ function normalizeObsLokiLogSummaryRow(row: ObsLokiLogSummaryRow): ObsLokiLogSum
140
139
  }
141
140
 
142
141
  function normalizeDisplayValue(value: string | undefined): string | undefined {
143
- const normalized = value?.replace(/\s+/gu, " ").trim();
144
- if (!normalized) return undefined;
145
- if (normalized.length <= maximumDisplayValueLength) return normalized;
146
- return `${normalized.slice(0, maximumDisplayValueLength - 1)}…`;
142
+ return normalizeObsBackendLabel(value);
147
143
  }
148
144
 
149
145
  function renderObsLokiLogSummaryRow(row: ObsLokiLogSummaryRow): string {
@@ -1,5 +1,9 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { ObservMeConfig } from "../config/schema.ts";
3
+ import { readDiagnosticMessage, sanitizeDiagnosticText } from "../diagnostics/sanitize.ts";
4
+ import { buildGrafanaTraceLink } from "../query/trace-link.ts";
2
5
  import { completeObsSubcommand, isExactObsSubcommandRequest } from "./obs-args.ts";
6
+ import { notifyObsCommand } from "./obs-command-support.ts";
3
7
 
4
8
  export interface ObsSessionCommandContext {
5
9
  readonly ui: {
@@ -15,6 +19,7 @@ export interface ObsSessionSnapshot {
15
19
  readonly toolCalls: number;
16
20
  readonly costUsd: number;
17
21
  readonly traceLink?: string;
22
+ readonly traceLinkError?: string;
18
23
  }
19
24
 
20
25
  export interface ObsSessionRuntimeStatePatch {
@@ -25,12 +30,13 @@ export interface ObsSessionRuntimeStatePatch {
25
30
  readonly toolCalls?: number;
26
31
  readonly costUsd?: number;
27
32
  readonly traceLink?: string;
33
+ readonly traceLinkError?: string;
28
34
  }
29
35
 
30
36
  export interface StartObsSessionRuntimeStateOptions {
31
37
  readonly sessionId?: string;
32
38
  readonly traceId?: string;
33
- readonly traceUrlTemplate?: string;
39
+ readonly config?: ObservMeConfig;
34
40
  }
35
41
 
36
42
  export type ObsSessionProvider = (ctx: ObsSessionCommandContext) => Promise<ObsSessionSnapshot> | ObsSessionSnapshot;
@@ -47,6 +53,12 @@ interface MutableObsSessionRuntimeState {
47
53
  toolCalls: number;
48
54
  costUsd: number;
49
55
  traceLink?: string;
56
+ traceLinkError?: string;
57
+ }
58
+
59
+ interface ObsSessionTraceLinkState {
60
+ readonly traceLink?: string;
61
+ readonly traceLinkError?: string;
50
62
  }
51
63
 
52
64
  const OBS_COMMAND_NAME = "obs";
@@ -71,15 +83,15 @@ export async function handleObsSessionCommand(
71
83
  options: RegisterObsSessionCommandOptions = {},
72
84
  ): Promise<void> {
73
85
  if (!isObsSessionRequest(args)) {
74
- await notifySession(ctx, "Usage: /obs session", "warning");
86
+ await notifyObsCommand(ctx, "Usage: /obs session", "warning");
75
87
  return;
76
88
  }
77
89
 
78
90
  try {
79
91
  const snapshot = await resolveObsSessionSnapshot(ctx, options);
80
- await notifySession(ctx, renderObsSession(snapshot), "info");
92
+ await notifyObsCommand(ctx, renderObsSession(snapshot), snapshot.traceLinkError ? "warning" : "info");
81
93
  } catch (error) {
82
- await notifySession(ctx, `ObservMe session unavailable: ${formatError(error)}`, "error");
94
+ await notifyObsCommand(ctx, `ObservMe session unavailable: ${formatError(error)}`, "error");
83
95
  }
84
96
  }
85
97
 
@@ -103,12 +115,13 @@ export function renderObsSession(snapshot: ObsSessionSnapshot): string {
103
115
  ];
104
116
 
105
117
  if (normalized.traceLink) lines.push(`Open trace: ${normalized.traceLink}`);
118
+ else if (normalized.traceLinkError) lines.push(`Trace link unavailable: ${normalized.traceLinkError}`);
106
119
  return lines.join("\n");
107
120
  }
108
121
 
109
122
  export function startObsSessionRuntimeState(options: StartObsSessionRuntimeStateOptions): void {
110
123
  const traceId = normalizeTraceId(options.traceId);
111
- const traceLink = buildObsSessionTraceLink(traceId, options.traceUrlTemplate);
124
+ const traceLinkState = createObsSessionTraceLinkState(options.config, traceId);
112
125
 
113
126
  replaceObsSessionRuntimeState({
114
127
  sessionId: normalizeString(options.sessionId),
@@ -117,10 +130,23 @@ export function startObsSessionRuntimeState(options: StartObsSessionRuntimeState
117
130
  llmCalls: 0,
118
131
  toolCalls: 0,
119
132
  costUsd: 0,
120
- traceLink,
133
+ ...traceLinkState,
121
134
  });
122
135
  }
123
136
 
137
+ function createObsSessionTraceLinkState(
138
+ config: ObservMeConfig | undefined,
139
+ traceId: string | undefined,
140
+ ): ObsSessionTraceLinkState {
141
+ if (!config || !traceId) return {};
142
+
143
+ try {
144
+ return { traceLink: buildGrafanaTraceLink(config, traceId) };
145
+ } catch (error) {
146
+ return { traceLinkError: sanitizeDiagnosticText(readDiagnosticMessage(error)) };
147
+ }
148
+ }
149
+
124
150
  export function updateObsSessionRuntimeState(patch: ObsSessionRuntimeStatePatch): void {
125
151
  if (patch.sessionId !== undefined) runtimeSessionState.sessionId = normalizeString(patch.sessionId);
126
152
  if (patch.traceId !== undefined) runtimeSessionState.traceId = normalizeTraceId(patch.traceId);
@@ -129,6 +155,7 @@ export function updateObsSessionRuntimeState(patch: ObsSessionRuntimeStatePatch)
129
155
  if (patch.toolCalls !== undefined) runtimeSessionState.toolCalls = normalizeCount(patch.toolCalls);
130
156
  if (patch.costUsd !== undefined) runtimeSessionState.costUsd = normalizeCost(patch.costUsd);
131
157
  if (patch.traceLink !== undefined) runtimeSessionState.traceLink = normalizeString(patch.traceLink);
158
+ if (patch.traceLinkError !== undefined) runtimeSessionState.traceLinkError = normalizeString(patch.traceLinkError);
132
159
  }
133
160
 
134
161
  export function recordObsSessionTurn(count = 1): void {
@@ -152,20 +179,6 @@ export function clearObsSessionRuntimeState(): void {
152
179
  replaceObsSessionRuntimeState(createEmptyObsSessionRuntimeState());
153
180
  }
154
181
 
155
- export function buildObsSessionTraceLink(traceId: string | undefined, traceUrlTemplate: string | undefined): string | undefined {
156
- const template = normalizeString(traceUrlTemplate);
157
- if (!traceId || !template) return undefined;
158
-
159
- if (template.includes("{{traceId}}")) return template.replaceAll("{{traceId}}", encodeURIComponent(traceId));
160
- if (template.includes("${traceId}")) return template.replaceAll("${traceId}", encodeURIComponent(traceId));
161
- if (template.includes("{traceId}")) return template.replaceAll("{traceId}", encodeURIComponent(traceId));
162
- if (template.includes("$traceId")) return template.replaceAll("$traceId", encodeURIComponent(traceId));
163
- if (template.includes("%TRACE_ID%")) return template.replaceAll("%TRACE_ID%", encodeURIComponent(traceId));
164
- if (template.includes("__TRACE_ID__")) return template.replaceAll("__TRACE_ID__", encodeURIComponent(traceId));
165
- if (template.includes("...")) return template.replaceAll("...", encodeURIComponent(traceId));
166
- return undefined;
167
- }
168
-
169
182
  class ObsSessionCommand {
170
183
  readonly #options: RegisterObsSessionCommandOptions;
171
184
 
@@ -194,6 +207,7 @@ function replaceObsSessionRuntimeState(state: MutableObsSessionRuntimeState): vo
194
207
  runtimeSessionState.toolCalls = state.toolCalls;
195
208
  runtimeSessionState.costUsd = state.costUsd;
196
209
  runtimeSessionState.traceLink = state.traceLink;
210
+ runtimeSessionState.traceLinkError = state.traceLinkError;
197
211
  }
198
212
 
199
213
  function createEmptyObsSessionRuntimeState(): MutableObsSessionRuntimeState {
@@ -214,6 +228,7 @@ function normalizeObsSessionSnapshot(snapshot: ObsSessionSnapshot): ObsSessionSn
214
228
  toolCalls: normalizeCount(snapshot.toolCalls),
215
229
  costUsd: normalizeCost(snapshot.costUsd),
216
230
  traceLink: normalizeString(snapshot.traceLink),
231
+ traceLinkError: normalizeString(snapshot.traceLinkError),
217
232
  };
218
233
  }
219
234
 
@@ -246,14 +261,6 @@ function isObsSessionRequest(args: string): boolean {
246
261
  return isExactObsSubcommandRequest(args, OBS_SESSION_SUBCOMMAND);
247
262
  }
248
263
 
249
- async function notifySession(
250
- ctx: ObsSessionCommandContext,
251
- message: string,
252
- type: "info" | "warning" | "error",
253
- ): Promise<void> {
254
- await ctx.ui?.notify?.(message, type);
255
- }
256
-
257
264
  function formatError(error: unknown): string {
258
265
  return error instanceof Error ? error.message : String(error);
259
266
  }