@senad-d/observme 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +24 -8
  3. package/docs/agent-subagent-observability-requirements.md +4 -3
  4. package/docs/compatibility-matrix.md +14 -3
  5. package/docs/configuration.md +2 -2
  6. package/docs/extension-integration.md +20 -13
  7. package/docs/reference/03-pi-event-and-session-model.md +6 -6
  8. package/docs/reference/04-telemetry-semantic-conventions.md +1 -0
  9. package/docs/reference/06-security-privacy-redaction.md +13 -6
  10. package/docs/reference/08-query-grafana-integration.md +30 -7
  11. package/docs/reference/11-deployment-runbooks.md +21 -0
  12. package/docs/reference/12-configuration-reference.md +30 -4
  13. package/docs/review-validation.md +10 -0
  14. package/examples/integrations/subagent-runner.ts +8 -5
  15. package/examples/observme.yaml +2 -2
  16. package/package.json +12 -4
  17. package/src/commands/obs-agents.ts +17 -12
  18. package/src/commands/obs-backfill.ts +2 -2
  19. package/src/commands/obs-command-support.ts +2 -1
  20. package/src/commands/obs-cost.ts +15 -7
  21. package/src/commands/obs-health.ts +22 -2
  22. package/src/commands/obs-loki-summary.ts +4 -8
  23. package/src/commands/obs-session.ts +35 -28
  24. package/src/commands/obs-status.ts +56 -13
  25. package/src/commands/obs-tools.ts +19 -8
  26. package/src/commands/obs-trace.ts +9 -0
  27. package/src/commands/obs.ts +6 -1
  28. package/src/config/bootstrap-project-config.ts +49 -32
  29. package/src/config/defaults.ts +0 -2
  30. package/src/config/load-config.ts +270 -92
  31. package/src/config/project-paths.ts +318 -6
  32. package/src/config/query-limits.ts +10 -0
  33. package/src/config/schema.ts +9 -8
  34. package/src/config/transport-security.ts +107 -0
  35. package/src/config/validate.ts +68 -11
  36. package/src/extension.ts +2 -12
  37. package/src/integration.ts +6 -2
  38. package/src/otel/logs.ts +24 -13
  39. package/src/otel/metrics.ts +24 -13
  40. package/src/otel/otlp-endpoint.ts +41 -2
  41. package/src/otel/otlp-http-options.ts +11 -0
  42. package/src/otel/sdk.ts +155 -9
  43. package/src/otel/shutdown.ts +39 -11
  44. package/src/otel/traces.ts +34 -16
  45. package/src/pi/agent-tree-tracker.ts +14 -1
  46. package/src/pi/compatibility.ts +30 -0
  47. package/src/pi/event-handlers/agent-turn.ts +16 -9
  48. package/src/pi/event-handlers/lifecycle.ts +207 -75
  49. package/src/pi/event-handlers/llm.ts +17 -9
  50. package/src/pi/event-handlers/session-events.ts +53 -19
  51. package/src/pi/event-handlers/tool-bash.ts +21 -27
  52. package/src/pi/handler-internals.ts +16 -26
  53. package/src/pi/handler-runtime.ts +142 -55
  54. package/src/pi/handler-types.ts +30 -15
  55. package/src/pi/handlers.ts +12 -1
  56. package/src/pi/integration-api.ts +27 -15
  57. package/src/pi/session-correlation.ts +167 -0
  58. package/src/pi/subagent-spawn.ts +164 -31
  59. package/src/privacy/redact.ts +574 -68
  60. package/src/privacy/secret-patterns.ts +6 -1
  61. package/src/query/grafana-readiness.ts +18 -2
  62. package/src/query/grafana-transport.ts +150 -27
  63. package/src/query/grafana-url.ts +36 -0
  64. package/src/query/grafana.ts +4 -136
  65. package/src/query/loki.ts +2 -4
  66. package/src/query/prometheus.ts +8 -10
  67. package/src/query/tempo.ts +2 -4
  68. package/src/query/trace-link.ts +186 -0
  69. package/src/safety/display-bounds.ts +84 -0
  70. package/src/semconv/metrics.ts +1 -0
  71. package/src/semconv/values.ts +2 -0
@@ -15,8 +15,9 @@ observme:
15
15
  headers:
16
16
  Authorization: "Bearer ${OBSERVME_OTLP_TOKEN}"
17
17
  tls:
18
- enabled: true
19
18
  insecureSkipVerify: false
19
+ # The endpoint scheme selects HTTP or HTTPS. HTTPS certificate verification
20
+ # stays enabled unless insecureSkipVerify is explicitly acknowledged.
20
21
  # Derived SDK URLs for OTLP/HTTP exporters:
21
22
  # traces: https://otel-collector.example.com:4318/v1/traces
22
23
  # metrics: https://otel-collector.example.com:4318/v1/metrics
@@ -135,6 +136,16 @@ observme:
135
136
  flushTimeoutMs: 3000
136
137
  ```
137
138
 
139
+ `otlp.endpoint` and every `otlp.signalEndpoints` value must be an absolute HTTP(S) URL without userinfo, unresolved placeholders, a query, or a fragment; credentials belong in `otlp.headers`. The base endpoint may contain an intentional path, to which ObservMe appends exactly one `/v1/{signal}` suffix using URL pathname semantics. Explicit signal endpoints must already end in their matching `/v1/traces`, `/v1/metrics`, or `/v1/logs` path.
140
+
141
+ `query.grafana.url` must be an absolute HTTP(S) base URL without a username or password component. Authentication belongs only in `query.grafana.token` or the complete `query.grafana.username` and `query.grafana.password` pair. Credential-bearing base URLs fail configuration validation and query/transport preflight before network I/O. Diagnostics identify the bounded `embedded_credentials` failure class without rendering the rejected URL or either credential component.
142
+
143
+ `otlp.tls.enabled` is intentionally unsupported and rejected as an unknown setting: the `http://` or `https://` endpoint scheme is the single source of truth for whether TLS is used. The retained `otlp.tls.insecureSkipVerify` option is passed to every OTLP HTTP exporter as its certificate-verification behavior.
144
+
145
+ `query.links.traceUrlTemplate` is shared by `/obs session`, `/obs trace`, and `/obs link`. Absolute HTTP(S) templates support `{traceId}`, `{{traceId}}` (with optional inner whitespace), `${traceId}`, and `%TRACE_ID%`; optional Tempo UID placeholders use the matching `{tempoDatasourceUid}`, `{{tempoDatasourceUid}}`, `${tempoDatasourceUid}`, and `%TEMPO_DATASOURCE_UID%` forms. An empty template or one containing `...` selects the structured Grafana Explore fallback built from `query.grafana.url` and `query.grafana.datasourceUids.tempo`. Unsupported placeholders and invalid URLs fail with a bounded diagnostic.
146
+
147
+ `query.maxLogs`, `query.maxTraces`, `query.maxMetricSeries`, and `query.maxAgents` accept integers from 1 through 100. Query clients enforce the same upper bound at runtime before issuing backend requests.
148
+
138
149
  ## 2. Environment Variables
139
150
 
140
151
  ```text
@@ -211,7 +222,8 @@ Rules:
211
222
  - Continue a valid W3C parent context explicitly on the child `pi.session` span. If trusted lineage has no usable continuation, start a new trace and add a validated parent span link when metadata exists, otherwise emit the documented propagation-failure log/counter fallback.
212
223
  - When `propagateTraceContext` is true, propagate W3C `traceparent`/`tracestate` to child processes launched by ObservMe-aware subagent wrappers.
213
224
  - When `workflow.enabled` is true, propagate `OBSERVME_WORKFLOW_ID` to child processes and report depth/fan-out/orphan metrics.
214
- - `writeCorrelationEntry` may append a minimal `custom` session entry for recovery, but it must remain disabled by default and must never use `custom_message`.
225
+ - `writeCorrelationEntry` defaults to `false`. When enabled, successful startup appends at most one versioned `observme.correlation` `custom` entry to the active branch. Reload/resume/fork recovery scans only `ctx.sessionManager.getBranch()`, accepts the latest valid bounded lineage entry, ignores corrupt or abandoned-branch data, and does not duplicate an unchanged entry. ObservMe never uses `custom_message`, so this state cannot enter LLM context.
226
+ - Historical telemetry is never replayed automatically. Use the explicit, confirmed `/obs backfill` command. The removed `replayOnStart` YAML field is rejected as unknown, and `OBSERVME_REPLAY_ON_START` is no longer recognized; neither can emit a synthetic duplicate startup record.
215
227
 
216
228
  ## 4. Config Precedence
217
229
 
@@ -224,6 +236,17 @@ Rules:
224
236
 
225
237
  Copy `.env.example` to `.env` for project-local extension variables, or export the same `OBSERVME_*` names in the shell before starting Pi. System environment variables override `.env` values, and `.env` must never be committed. If redacted content capture is enabled, set `OBSERVME_HASH_SALT` in the shell or trusted project `.env`; missing salts make capture fail closed.
226
238
 
239
+ ### 4.1 Project-Local Path and Symlink Policy
240
+
241
+ Project config, project `.env`, and starter-config creation use the same fail-closed filesystem policy:
242
+
243
+ - Pi must mark the project trusted before ObservMe reads or creates a project-local file.
244
+ - The lexical path and resolved canonical target must both remain inside one stable canonical project root.
245
+ - Existing file or directory symlinks are supported when their targets remain inside that root. Out-of-root, dangling, or unverifiable symlinks are rejected.
246
+ - Reads and creates use opened file handles whose file identity, root identity, and canonical containment are verified before bytes are consumed or written. Concurrent replacement of a file, ancestor, or project root therefore rejects the operation instead of following the substituted target.
247
+ - Normal missing in-root paths remain supported. Starter creation is exclusive and idempotent, never overwrites an existing file, and removes an unverified empty target if post-open validation fails.
248
+ - Rejected project sources use the bounded `config_source_rejected` classification. Warnings and status diagnostics do not include canonical targets, external paths, file contents, or environment values; the rejected layer is skipped and other accepted layers continue to apply.
249
+
227
250
  Automatic project starter file:
228
251
 
229
252
  - On `session_start`, the extension creates `<cwd>/<CONFIG_DIR_NAME>/observme.yaml` (`<cwd>/.pi/observme.yaml` in the standard distribution) when the project is trusted and the file is missing.
@@ -307,7 +330,7 @@ Rules:
307
330
  - When the token is blank or an unresolved placeholder, a resolved `query.grafana.username` and `query.grafana.password` are sent as Basic auth for local development.
308
331
  - Query-backed commands and `/obs health` must fail fast before Grafana calls when Grafana auth is unresolved/missing/incomplete, `query.grafana.url` is invalid, or a required datasource UID is blank.
309
332
  - `/obs health` must report Grafana `401`/`403` responses as authentication failures and must not print token or password values.
310
- - `tls.insecureSkipVerify=true` is only for local self-signed certificates; production should trust the CA instead.
333
+ - `query.grafana.tls.insecureSkipVerify=true` is only for local self-signed certificates; production requires `privacy.allowInsecureTransport=true` as an explicit acknowledgement and should trust the CA instead.
311
334
  - `transport.preferIPv4=true` uses Node's local HTTP(S) transport with IPv4 DNS lookup for Grafana calls.
312
335
  - Provisioned datasource UIDs are `tempo`, `loki`, and `prometheus`; Loki selectors use normalized labels such as `service_name`, `pi_session_id`, `event_name`, and `event_category` for ObservMe data.
313
336
 
@@ -335,11 +358,14 @@ ObservMe must display a warning when unsafe capture is active. Redacted capture
335
358
  Reject config when:
336
359
 
337
360
  - `allowUnsafeCapture=false` and `redactionEnabled=false` while any content capture is true.
338
- - Production endpoint uses `http://` and `allowInsecureTransport` is not true.
361
+ - A production OTLP or Grafana endpoint uses `http://` and `privacy.allowInsecureTransport` is not true.
362
+ - `query.grafana.url` contains an embedded username or password instead of using the dedicated token or username/password settings.
363
+ - Production sets `otlp.tls.insecureSkipVerify=true` or `query.grafana.tls.insecureSkipVerify=true` and `privacy.allowInsecureTransport` is not true.
339
364
  - `otlp.protocol=http/protobuf` but a signal-specific exporter URL omits the required `/v1/traces`, `/v1/metrics`, or `/v1/logs` path.
340
365
  - `metrics.activeAgentLeaseDurationMillis` is fractional, non-numeric, below `10000`, above `300000`, or less than `(2 * metrics.exportIntervalMillis) + 5000`.
341
366
  - Metric labels include high-cardinality fields such as workflow IDs, session IDs, agent IDs, parent/child agent IDs, trace IDs, span IDs, entry IDs, spawn IDs, or spawn tool-call IDs. The generated `observme.instance.id` / `service.instance.id` remains a resource identity used by the Collector for the `observme_instance_id` lease join; it must not be configured as an execution-derived label.
342
367
  - Project-local config is read while `ctx.isProjectTrusted()` is false.
368
+ - A project config, project `.env`, or starter-config path escapes the canonical project root, changes identity during I/O, or cannot be verified safely.
343
369
  - Propagated workflow or agent lineage values are malformed, too long, or contain unsafe characters.
344
370
  - Queue sizes exceed configured memory guardrails.
345
371
 
@@ -93,6 +93,16 @@ This sanitized record covers the production active-agent lease release candidate
93
93
 
94
94
  Post-run cleanup check found no containers or networks labeled for the active-agent lease integration and no `observme-grafana-it-*` containers or networks. Release remains blocked on the unrelated broad Grafana-stack validation unless the release owner accepts the explicitly allowed pre-existing blocker; all lease-specific release criteria passed.
95
95
 
96
+ ## Pi 0.81.1 compatibility validation — 2026-07-22
97
+
98
+ This sanitized record covers the compatibility-policy correction against exact `@earendil-works/pi-coding-agent` and `@earendil-works/pi-ai` 0.81.1 development dependencies on local Node.js v26.0.0 and npm 11.12.1. No project, model, Grafana, or registry credentials were required or printed.
99
+
100
+ | Command | Result | Evidence and notes |
101
+ | --- | --- | --- |
102
+ | `npm run validate:pi-compatibility` | Pass | Source/test typechecks, compatibility and event-mapping tests, packaged-install smoke, handler and lifecycle smoke, and the real Pi RPC runtime passed against Pi 0.81.1. |
103
+ | `npm run validate` | Pass | ESLint, formatting, script checks, 618 unit/contract tests, package-content validation, packaged-install smoke, handler smoke, Pi lifecycle smoke, and real Pi runtime smoke passed. |
104
+ | `npm audit --omit=dev` | Pass | The production dependency tree reported no vulnerabilities. |
105
+
96
106
  ## Review-closure evidence categories
97
107
 
98
108
  - **Read-only/check-only** — commands that inspect, type-check, lint, audit, run tests, dry-run packaging, or smoke local deterministic fixtures without publishing or writing tracked files.
@@ -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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senad-d/observme",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",
@@ -67,12 +67,20 @@
67
67
  "smoke:handlers": "node scripts/smoke-handler-execution.mjs",
68
68
  "smoke:pi-lifecycle": "node scripts/smoke-pi-lifecycle.mjs",
69
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",
70
71
  "validate:grafana-obs": "node scripts/validate-grafana-obs-flow.mjs",
71
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",
72
73
  "pack:dry-run": "npm pack --dry-run --json",
73
74
  "check:pack": "node scripts/check-package-contents.mjs",
74
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"
75
76
  },
77
+ "observmeCompatibility": {
78
+ "pi": {
79
+ "earliestTestedVersion": "0.80.5",
80
+ "releaseTestedVersion": "0.81.1",
81
+ "runtimePolicy": "required-api-capabilities"
82
+ }
83
+ },
76
84
  "pi": {
77
85
  "extensions": [
78
86
  "./src/extension.ts"
@@ -88,8 +96,8 @@
88
96
  "typebox": "*"
89
97
  },
90
98
  "devDependencies": {
91
- "@earendil-works/pi-ai": "^0.80.2",
92
- "@earendil-works/pi-coding-agent": "^0.80.2",
99
+ "@earendil-works/pi-ai": "0.81.1",
100
+ "@earendil-works/pi-coding-agent": "0.81.1",
93
101
  "@eslint/js": "^10.0.1",
94
102
  "@types/node": "^26.1.1",
95
103
  "c8": "^11.0.0",
@@ -97,7 +105,7 @@
97
105
  "globals": "^17.7.0",
98
106
  "typebox": "^1.3.6",
99
107
  "typescript": "6.0.3",
100
- "typescript-eslint": "^8.63.0"
108
+ "typescript-eslint": "8.63.0"
101
109
  },
102
110
  "dependencies": {
103
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 {