@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
@@ -144,7 +144,7 @@
144
144
  "type": "prometheus",
145
145
  "uid": "prometheus"
146
146
  },
147
- "description": "Current ObservMe agent runtimes from the active-agent gauge. Zero can mean healthy idle or an absent gauge; check Export Health before treating an unexpected zero as healthy.",
147
+ "description": "Canonical leased-active fleet total; Prometheus summary values are aggregate and are not filtered to the selected workflow. Clean shutdown reaches zero after export/scrape propagation; after a crash, SIGKILL, cancelled job, or runner loss, the leased total converges to zero within the default 60-second lease plus one Prometheus scrape even if raw claims stay cached. Zero can also mean healthy idle or a failed-closed lease, so check Export Health. /obs agents is current-process child state, not fleet liveness.",
148
148
  "fieldConfig": {
149
149
  "defaults": {
150
150
  "color": {
@@ -195,7 +195,7 @@
195
195
  "uid": "prometheus"
196
196
  },
197
197
  "editorMode": "code",
198
- "expr": "(sum(observme_active_agents) or vector(0))",
198
+ "expr": "sum(max by (observme_instance_id) ((observme_active_agents > 0) and on (observme_instance_id) ((observme_agent_lease_expires_unixtime_seconds > time()) and (observme_agent_lease_expires_unixtime_seconds <= time() + 305)))) or vector(0)",
199
199
  "instant": true,
200
200
  "range": false,
201
201
  "refId": "A"
@@ -1005,7 +1005,7 @@
1005
1005
  "type": "prometheus",
1006
1006
  "uid": "prometheus"
1007
1007
  },
1008
- "description": "Shape of the agent tree over time without using workflow or agent IDs as metric labels. Constant reference lines mirror the depth=5 and fan-out=20 alert thresholds.",
1008
+ "description": "Aggregate fleet shape over time, not per-workflow truth: the leased-active role series is canonical current activity, while tree histograms summarize all matching telemetry without workflow or agent IDs as metric labels. Clean shutdown removes leased activity after export/scrape propagation; after an ungraceful exit, leased activity converges to zero within the default 60-second lease plus one Prometheus scrape while raw claims may remain cached. /obs agents is local current-process child state. Constant lines mirror the depth=5 and fan-out=20 alert thresholds.",
1009
1009
  "fieldConfig": {
1010
1010
  "defaults": {
1011
1011
  "color": {
@@ -1083,7 +1083,7 @@
1083
1083
  "uid": "prometheus"
1084
1084
  },
1085
1085
  "editorMode": "code",
1086
- "expr": "sum(observme_active_agents) by (agent_role)",
1086
+ "expr": "sum by (agent_role) (max by (observme_instance_id, agent_role) ((observme_active_agents > 0) and on (observme_instance_id) ((observme_agent_lease_expires_unixtime_seconds > time()) and (observme_agent_lease_expires_unixtime_seconds <= time() + 305)))) or on() vector(0)",
1087
1087
  "legendFormat": "active {{agent_role}}",
1088
1088
  "range": true,
1089
1089
  "refId": "A"
@@ -565,6 +565,7 @@ agent.run.failed
565
565
  agent.spawn.started
566
566
  agent.spawn.completed
567
567
  agent.spawn.failed
568
+ agent.spawn.cancelled
568
569
  agent.wait.started
569
570
  agent.wait.completed
570
571
  agent.join.started
@@ -647,27 +648,35 @@ histogram_quantile(0.95, sum(rate(observme_subagent_spawn_duration_ms_bucket[$__
647
648
 
648
649
  ```text
649
650
  observme_active_agents
651
+ observme_agent_lease_expires_unixtime_seconds
650
652
  observme_agent_tree_depth
651
653
  observme_agent_tree_width
652
654
  observme_agent_fanout_count
653
655
  ```
654
656
 
655
- Expected labels:
656
-
657
- ```text
658
- agent_role
659
- subagent_depth
660
- ```
657
+ The active claim and lease carry the bounded `agent_role` and `environment` dimensions plus generated resource-instance identity normalized by the Collector to `observme_instance_id`. They do not currently carry `subagent_depth`; never derive a replacement depth label from workflow, session, logical-agent, trace, span, or job-run IDs. The tree histograms retain their documented bounded tree dimensions.
661
658
 
662
659
  Dashboard examples:
663
660
 
664
661
  ```promql
665
- sum(observme_active_agents) by (agent_role, subagent_depth)
662
+ sum by (agent_role) (
663
+ max by (observme_instance_id, agent_role) (
664
+ (observme_active_agents > 0)
665
+ and on (observme_instance_id)
666
+ (
667
+ (observme_agent_lease_expires_unixtime_seconds > time())
668
+ and
669
+ (observme_agent_lease_expires_unixtime_seconds <= time() + 305)
670
+ )
671
+ )
672
+ ) or on() vector(0)
666
673
  histogram_quantile(0.95, sum(rate(observme_agent_tree_depth_bucket[$__rate_interval])) by (subagent_depth, le))
667
674
  histogram_quantile(0.95, sum(rate(observme_agent_tree_width_bucket[$__rate_interval])) by (subagent_depth, le))
668
675
  histogram_quantile(0.95, sum(rate(observme_agent_fanout_count_bucket[$__rate_interval])) by (subagent_depth, le))
669
676
  ```
670
677
 
678
+ Use the canonical total, role, environment, diagnostics, and migration guidance in [`reference/09-dashboards-alerts-slos.md` §1.3](reference/09-dashboards-alerts-slos.md#13-active-agent-leased-state-contract); raw active-claim sums are not current-liveness queries after ungraceful termination.
679
+
671
680
  ### Lineage health metrics
672
681
 
673
682
  ```text
@@ -825,9 +834,9 @@ Minimum adapter behavior:
825
834
  1. Request the API with `requestObservMeIntegration(pi)`.
826
835
  2. Before spawning child Pi, call `startSubagent` with spawn type/reason and safe command metadata.
827
836
  3. Pass the returned `env` into `child_process.spawn`.
828
- 4. On launcher success, call `completeSubagent` with child status `active`.
829
- 5. On launcher error/abort/failure, call `failSubagent` or complete with cancelled status.
830
- 6. Around blocking waits, call `startWait` and `endWait`.
837
+ 4. On launcher error before the child runs, call `failSubagent`.
838
+ 5. Around blocking waits, call `startWait` and `endWait`.
839
+ 6. When the child reaches a terminal state, call `completeSubagent` once with matching `completed`, `failed`, or `cancelled` status/outcome fields.
831
840
  7. When child output is collected, call `startJoin` and `endJoin` with child status.
832
841
  8. Ensure the child command loads ObservMe as an extension/package.
833
842
  9. Ensure the child ObservMe runtime receives the complete propagated parent context.
@@ -1,8 +1,14 @@
1
1
  # ObservMe Compatibility Matrix
2
2
 
3
- Last updated: 2026-07-07
3
+ Last updated: 2026-07-14
4
4
 
5
- This matrix records the ObservMe runtime and observability-stack versions that have been exercised or pinned for the current release line. A row marked **validated** has been covered by a named local or CI validation command. A row marked **pinned, pending integration** is present in the reference configuration but must be promoted to validated after the integration tests in tasks 48-50 exercise it.
5
+ This matrix records the ObservMe runtime and observability-stack versions exercised or pinned for the current release line. A component is marked **validated** only when the named command actually exercised it; configured CI targets remain distinct from completed local or workflow evidence.
6
+
7
+ ## Pi version policy
8
+
9
+ ObservMe 0.1.x supports `@earendil-works/pi-coding-agent` `>=0.80.5 <0.81.0`. The Pi-mandated core package peers remain in `peerDependencies` with `"*"`, while release development dependencies are pinned exactly to 0.80.6 and CI installs both the 0.80.5 minimum and the 0.80.6 release-resolved target explicitly. Extension startup checks Pi's exported `VERSION` and required APIs before registering any handler or command.
10
+
11
+ The 0.80.5 minimum is evidence-based: its exported extension contract includes `session_info_changed`, which ObservMe requires for live rename metadata; 0.80.2 does not. Pi's current extension overloads do not include `bashExecution`, `model_change`, or `thinking_level_change`. `bashExecution` is an `AgentMessage` role observed through `message_end`, while model and thinking changes are session entry types represented at runtime by `model_select` and `thinking_level_select`; ObservMe no longer registers those three non-event names.
6
12
 
7
13
  ## Validation record
8
14
 
@@ -12,12 +18,23 @@ This matrix records the ObservMe runtime and observability-stack versions that h
12
18
  | 2026-07-07 | Local Docker 29.5.2 with Collector Contrib 0.104.0 | `npm run test:integration:collector` | Starts a local debug-exporter Collector container, exports representative ObservMe traces/metrics/logs, and asserts default-disabled content capture is absent. |
13
19
  | 2026-07-07 | Local Docker 29.5.2 with the pinned Grafana stack | `npm run test:integration:grafana-stack` | Starts `observability-stack/` services, exports representative ObservMe telemetry, queries Tempo by trace ID and lineage attributes, queries Loki by session ID, queries Prometheus token totals, and validates Grafana dashboard provisioning imports. |
14
20
  | Continuous integration target | GitHub Actions `ubuntu-latest`, Node.js 22.19.0 | `npm ci --ignore-scripts` then `npm run validate` | CI target from `.github/workflows/ci.yml`; record the workflow run URL here after each release validation. |
21
+ | Pi compatibility CI matrix (configured 2026-07-14) | GitHub Actions `ubuntu-latest`, Node.js 22.19.0; exact Pi 0.80.5 and 0.80.6 matrix targets | Exact Pi install, version assertion, then `npm run validate:pi-compatibility` | Compile-time event fixtures, compatibility diagnostics, package install, handler/lifecycle smoke, and a real Pi RPC runtime are exercised separately at the declared minimum and release-resolved versions. |
22
+ | 2026-07-14 | Local Node.js v26.0.0, npm 11.12.1; exact `@earendil-works/pi-coding-agent` and `@earendil-works/pi-ai` 0.80.5 | Exact version assertion then `npm run validate:pi-compatibility` | Passed typecheck, typed event contracts, compatibility diagnostics, packaged install, handler/lifecycle smoke, and real Pi RPC runtime at the declared minimum. |
23
+ | 2026-07-14 | Local Node.js v26.0.0, npm 11.12.1; exact `@earendil-works/pi-coding-agent` and `@earendil-works/pi-ai` 0.80.6 | Exact version assertion then `npm run validate:pi-compatibility` | Passed the same compatibility suite at the release-resolved version. |
24
+ | Lease cancellation CI profile (configured 2026-07-14) | GitHub-hosted Linux `ubuntu-latest` (exact `ImageOS`/`ImageVersion` recorded in the Actions runner log); Node.js 22.19.0; Collector Contrib 0.104.0; Prometheus 2.53.1; Grafana 11.1.0 | `npm run test:active-agent-lease:contracts` then `npm run test:integration:active-agent-lease` | The bounded `lease-cancellation` job validates clean shutdown, `SIGTERM`, and cancellation-oriented `SIGKILL` convergence against the pinned Collector and Prometheus without secrets or a Grafana dependency. Grafana 11.1.0 is the recorded dashboard compatibility baseline validated separately by `npm run test:integration:grafana-stack`. `if: always()` cleanup is best effort; lease expiry is the asserted correctness mechanism. |
25
+ | 2026-07-14 | Local Node.js v26.0.0, npm 11.12.1 | `npm run validate` | Passed 493 unit/contract tests plus typecheck, ESLint, formatting, script, package-content, packaged-install, handler, Pi lifecycle, and Pi runtime checks for the lease release candidate. |
26
+ | 2026-07-14 | Local Docker 29.5.2, Compose 5.1.4, Collector Contrib 0.104.0 | `npm run test:integration:collector`; Compose config; pinned Collector `validate` | Passed representative OTLP export, Compose interpolation, and the shipped Collector config validation. |
27
+ | 2026-07-14 | Local Docker 29.5.2, Collector Contrib 0.104.0, Prometheus 2.53.1 | `npm run test:integration:active-agent-lease` | Passed clean shutdown, `SIGTERM`, and cancellation-oriented `SIGKILL`; the raw claim remained cached while leased activity converged without Collector restart. Labeled containers and the network were absent after cleanup. |
28
+ | 2026-07-14 | Local Docker 29.5.2 with the pinned Grafana stack | `npm run test:integration:grafana-stack` | Unrelated pre-existing broad-stack blocker remains: this run timed out with an empty Prometheus total-token vector. That token path does not validate active-agent leases. Cleanup removed all integration containers and networks; focused lease evidence is unaffected. |
29
+ | 2026-07-14 | npm package dry run for `@senad-d/observme` 0.1.3 | `npm run pack:dry-run` | Passed with 124 files; metric source, dashboards, alerts, examples, target docs, and `skills/observme-docs/SKILL.md` were present. |
30
+
31
+ Active-agent lease interpretation additionally requires producer and Prometheus wall clocks to remain within 5 seconds. GitHub-hosted runners satisfy this expectation. Self-hosted runners are supported only with reliable NTP or equivalent synchronization; clock health must be monitored separately from the ObservMe version matrix.
15
32
 
16
33
  ## Runtime and library matrix
17
34
 
18
35
  | Component | Version tested or pinned | Source of truth | Status | Evidence and notes |
19
36
  | --- | --- | --- | --- | --- |
20
- | Pi package API | `@earendil-works/pi-coding-agent` 0.80.3, `@earendil-works/pi-ai` 0.80.3 | `package-lock.json` | Validated locally | `npm run validate` typechecks the Pi extension API imports and runs smoke harnesses against registered extension handlers. |
37
+ | Pi package API | Supported: `@earendil-works/pi-coding-agent` >=0.80.5 <0.81.0; release-resolved `@earendil-works/pi-coding-agent` 0.80.6 and `@earendil-works/pi-ai` 0.80.6 | `package.json` `observmeCompatibility`, exact dev dependencies, `package-lock.json`, `.github/workflows/ci.yml` | Validated locally at minimum and release-resolved versions; exact CI matrix configured | `npm run validate:pi-compatibility` typechecks exported event contracts, tests pre-registration diagnostics, installs the package, runs handler/lifecycle smoke, and launches the real Pi RPC runtime. |
21
38
  | Node.js | Local: 26.0.0; CI target/minimum: 22.19.0 | `node --version`, `package.json` `engines.node`, `.github/workflows/ci.yml` | Validated locally for 26.0.0; CI target pinned at 22.19.0 | Local validation ran on 26.0.0. CI is configured to validate on 22.19.0, which is also the minimum supported Node.js version. |
22
39
  | OpenTelemetry JS package set | `@opentelemetry/api` 1.9.1; `@opentelemetry/api-logs` 0.220.0; OTLP proto exporters 0.220.0; `@opentelemetry/resources` 2.9.0; `@opentelemetry/sdk-logs` 0.220.0; `@opentelemetry/sdk-metrics` 2.9.0; `@opentelemetry/sdk-trace-base` 2.9.0; `@opentelemetry/sdk-trace-node` 2.9.0 | `package.json`, `package-lock.json` | Validated locally | `npm run validate` includes the OTEL lifecycle/exporter unit tests under `test/otel-*.test.mjs`. |
23
40
 
@@ -33,7 +50,7 @@ This matrix records the ObservMe runtime and observability-stack versions that h
33
50
 
34
51
  ## Update rules
35
52
 
36
- - Update this file whenever `package-lock.json`, `.github/workflows/ci.yml`, or `observability-stack/docker-compose.yml` changes a tracked version.
53
+ - Update this file whenever `package.json` `observmeCompatibility`, `package-lock.json`, `.github/workflows/ci.yml`, or `observability-stack/docker-compose.yml` changes a tracked version.
37
54
  - Add a new validation-record row whenever a release candidate is validated locally, in CI, or against the Docker Compose stack.
38
55
  - Do not mark backend components as validated unless a command actually started or queried that component.
39
56
  - Keep workflow IDs, session IDs, agent IDs, trace IDs, and other high-cardinality values out of this document.
@@ -18,12 +18,15 @@ The standard Pi distribution currently resolves this to `.pi/observme.yaml`. Obs
18
18
 
19
19
  Edit the project config file (`.pi/observme.yaml` in the standard distribution) where you run Pi:
20
20
 
21
- - `otlp.endpoint` and `otlp.signalEndpoints` — your OpenTelemetry Collector URLs.
21
+ - `otlp.endpoint` and `otlp.signalEndpoints` — absolute HTTP(S) OpenTelemetry Collector URLs. Base endpoints may include an intentional path; ObservMe appends one `/v1/{signal}` suffix with URL pathname semantics. Signal-specific endpoints must already end in the matching signal path. Keep credentials in `otlp.headers`; endpoint userinfo, unresolved placeholders, queries, and fragments are rejected.
22
22
  - `resource.attributes` — service name, project name, tenant, and deployment environment labels.
23
23
  - `capture` — whether prompts, responses, thinking, tool data, bash data, and file paths are exported. For local debugging, set only the specific fields you need to `true`.
24
24
  - `privacy` — redaction, unsafe-capture acknowledgement, insecure transport, hash salt env var, and path handling. Keep `redactionEnabled: true`; set `allowUnsafeCapture: true` only when you intentionally accept unredacted sensitive-content export from this trusted project. Live telemetry and `/obs backfill` use the same policy: disabled capture omits content, enabled redaction redacts then truncates, redaction failures drop content, and `redactionEnabled: false` with `allowUnsafeCapture: true` exports raw truncated content.
25
25
  - `query.grafana` — Grafana URL, datasource UIDs, TLS, and IPv4 transport settings for `/obs` query commands.
26
- - `query.links.traceUrlTemplate` — the Grafana Explore trace-link template used by `/obs trace` and `/obs link`.
26
+ - `query.links.traceUrlTemplate` — the canonical Grafana Explore trace-link template used by `/obs session`, `/obs trace`, and `/obs link`; use `{traceId}`, `{{traceId}}`, `${traceId}`, or `%TRACE_ID%`, or keep the generated `...` structured fallback.
27
+ - `metrics.activeAgentLeaseDurationMillis` — how long the last exported active-agent lease remains valid. The default is `60000` ms, the supported range is `10000`–`300000` ms, and the value must be at least `(2 * metrics.exportIntervalMillis) + 5000` ms. `OBSERVME_ACTIVE_AGENT_LEASE_DURATION_MS` overrides YAML through the normal precedence rules.
28
+
29
+ The shipped dashboards and alerts combine a positive `observme_active_agents` lifecycle claim with a current lease; raw active-claim sums are diagnostic only after an ungraceful exit. Clean shutdown reaches zero after normal export/scrape propagation. Crash, `SIGKILL`, forced GitHub Actions cancellation, or runner loss converges within the lease plus up to 5 seconds of supported clock skew and one Prometheus scrape/evaluation interval, without restarting the Collector. Keep producer and Prometheus clocks synchronized within 5 seconds; GitHub-hosted runners meet this expectation, while self-hosted runners require reliable NTP or an equivalent time service.
27
30
 
28
31
  Keep credentials out of YAML. Reference environment variables such as `OBSERVME_OTLP_TOKEN`, `OBSERVME_GRAFANA_TOKEN`, `OBSERVME_GRAFANA_PASSWORD`, and `OBSERVME_HASH_SALT`, then set those values in the shell or a trusted project `.env` file. When any content capture flag is enabled with `privacy.redactionEnabled: true`, `OBSERVME_HASH_SALT` must be set before the event occurs; otherwise content capture fails closed and emits `redaction.failed` diagnostics instead of conversation rows.
29
32
 
@@ -45,3 +48,5 @@ Invalid or unsafe merged configuration falls back to safe defaults. `/obs status
45
48
  - [Example configurations](../examples/README.md)
46
49
  - [Security, privacy, and redaction](reference/06-security-privacy-redaction.md)
47
50
  - [Grafana and `/obs` validation flow](validation-flow.md)
51
+ - [Canonical active-agent queries and raw-query migration](reference/09-dashboards-alerts-slos.md#131-canonical-active-agent-promql)
52
+ - [Active-lease deployment and troubleshooting runbook](reference/11-deployment-runbooks.md)
@@ -58,9 +58,9 @@ Use this order for each child process:
58
58
  1. Request the ObservMe integration API.
59
59
  2. Call `startSubagent()` immediately before launching the child.
60
60
  3. Pass the returned `env` as the child process environment without logging it.
61
- 4. Call `completeSubagent(..., { childStatus: "active" })` when the launcher successfully creates the child.
62
- 5. Call `failSubagent()` when the launcher fails before the child is active.
63
- 6. Call `startWait()`/`endWait()` around time spent waiting for child completion.
61
+ 4. Call `failSubagent()` only when the launcher fails before the child is running.
62
+ 5. Call `startWait()`/`endWait()` around time spent waiting for child completion.
63
+ 6. Call `completeSubagent()` once with the matching terminal `childStatus` and `outcome` (`completed`, `failed`, or `cancelled`).
64
64
  7. Call `startJoin()`/`endJoin()` when collecting the terminal child status or result.
65
65
 
66
66
  ```typescript
@@ -78,12 +78,9 @@ if (!started?.ok) {
78
78
  return;
79
79
  }
80
80
 
81
+ let child;
81
82
  try {
82
- await launchChildPi({ env: started.env });
83
- observme.completeSubagent(started.spawnId, {
84
- childAgentId: started.childAgentId,
85
- childStatus: "active",
86
- });
83
+ child = await launchChildPi({ env: started.env });
87
84
  } catch (error) {
88
85
  observme.failSubagent(started.spawnId, {
89
86
  childAgentId: started.childAgentId,
@@ -91,6 +88,13 @@ try {
91
88
  });
92
89
  throw error;
93
90
  }
91
+
92
+ const result = await waitForChildPi(child);
93
+ observme.completeSubagent(started.spawnId, {
94
+ childAgentId: started.childAgentId,
95
+ childStatus: result.status,
96
+ outcome: result.status,
97
+ });
94
98
  ```
95
99
 
96
100
  Do not put raw tasks, prompts, command lines, environment values, child output, or private paths in `errorClass`, `spawnReason`, or other bounded fields.
@@ -101,7 +105,7 @@ Do not put raw tasks, prompts, command lines, environment values, child output,
101
105
  | --- | --- |
102
106
  | `getContext()` | Read the current workflow, root/parent/current agent, role, depth, session, and trace identifiers for local orchestration correlation. These are high-cardinality values and must not become metric labels. |
103
107
  | `startSubagent(options)` | Starts `pi.agent.spawn`, records spawn metrics/logs, creates bounded parent tree state, and returns a sanitized propagation environment. |
104
- | `completeSubagent(spawnId, options)` | Ends a successful launcher operation. Use child status `active` when the child is running; use `completed` only when launch and child completion are intentionally represented together. |
108
+ | `completeSubagent(spawnId, options)` | Ends the active child lifecycle with one coherent `completed`, `failed`, or `cancelled` status/outcome pair. |
105
109
  | `failSubagent(spawnId, options)` | Ends a launcher failure and records bounded failure telemetry. |
106
110
  | `startWait(options)` / `endWait(id, options)` | Measures time the parent is blocked on a child or dependency. |
107
111
  | `startJoin(options)` / `endJoin(id, options)` | Measures result collection and records child failure propagation or confirmed parent recovery. |
@@ -111,16 +115,16 @@ Do not put raw tasks, prompts, command lines, environment values, child output,
111
115
  | Option | Values and meaning |
112
116
  | --- | --- |
113
117
  | `spawnId` | Optional caller-generated safe ID; omit to let ObservMe generate it. |
114
- | `childAgentId` | Optional bounded parent-side placeholder; this is not propagated as the child's real agent ID. |
118
+ | `childAgentId` | Optional bounded parent-side placeholder; this is not propagated as the child's real agent ID and must remain unique while its active or terminal node is retained. |
115
119
  | `command` / `args` | Used only to create a salted command fingerprint when configured; raw values are not exported. Omit them if the launcher cannot safely provide them. |
116
120
  | `spawnType` | `command`, `tool`, `extension`, or `unknown`. |
117
121
  | `spawnReason` | `delegated_task`, `parallel_search`, `review`, `tool_wrapper`, or `unknown`. |
118
122
  | `toolCallId` | Optional high-cardinality trace/log correlation when a tool initiated the spawn. |
119
123
  | `env` | Base child environment. ObservMe removes stale lineage/W3C keys and returns the replacement environment. |
120
124
 
121
- Runtime callers are validated even when JavaScript bypasses the TypeScript types. Caller-provided lifecycle identifiers must match `[A-Za-z0-9._:-]{1,128}`. Commands and individual arguments are capped at 4096 characters, argument lists at 256 items, and environment objects at 4096 entries. Durations must be finite, non-negative milliseconds. Invalid or duplicate active operations return a failure without replacing an existing span.
125
+ Runtime callers are validated even when JavaScript bypasses the TypeScript types. Caller-provided lifecycle identifiers must match `[A-Za-z0-9._:-]{1,128}`. Commands and individual arguments are capped at 4096 characters, argument lists at 256 items, and environment objects at 4096 entries. Durations must be finite, non-negative milliseconds. Invalid or duplicate active operations return a failure without replacing an existing span. Child placeholders, including generated placeholders, are collision-checked before span, tree, metric, or propagation state is created; do not reuse a terminal placeholder while its bounded tree node remains retained.
122
126
 
123
- Completion and wait/join methods use bounded child states (`starting`, `active`, `completed`, `failed`, `cancelled`, `orphaned`), join states (`waiting`, `completed`, `failed`, `cancelled`, `timeout`, `unknown`), and wait reasons (`dependency`, `rate_limit`, `child_running`, `unknown`). `failurePropagated=false` on a completed join confirms that the parent recovered from a failed child.
127
+ Completion accepts only terminal child states (`completed`, `failed`, `cancelled`), and any supplied outcome must match. Wait/join methods use bounded child states (`starting`, `active`, `completed`, `failed`, `cancelled`, `orphaned`), join states (`waiting`, `completed`, `failed`, `cancelled`, `timeout`, `unknown`), and wait reasons (`dependency`, `rate_limit`, `child_running`, `unknown`). `failurePropagated=false` on a completed join confirms that the parent recovered from a failed child.
124
128
 
125
129
  All mutation methods return a discriminated result. Handle these reasons without crashing Pi:
126
130
 
@@ -129,7 +133,10 @@ All mutation methods return a discriminated result. Handle these reasons without
129
133
  | `session_unavailable` | ObservMe is loaded but no telemetry session is active. |
130
134
  | `invalid_request` | An identifier, enum, duration, command/argument field, or environment shape is invalid or oversized. |
131
135
  | `spawn_already_exists` / `wait_already_exists` / `join_already_exists` | The requested lifecycle identifier is already active. Generate a unique identifier or finish the active operation; do not overwrite it. |
136
+ | `child_agent_already_exists` | The requested or generated child placeholder belongs to an active spawn or retained tree node. Generate a unique child identifier; do not reuse terminal placeholders. |
132
137
  | `spawn_not_found` / `wait_not_found` / `join_not_found` | The lifecycle handle is absent or has already ended. |
138
+ | `child_agent_mismatch` | The supplied child ID does not match the child stored for the active spawn. |
139
+ | `invalid_terminal_transition` | Terminal status/outcome fields contradict each other or would rewrite an existing terminal tree state. |
133
140
  | `operation_failed` | ObservMe could not safely complete the operation. |
134
141
 
135
142
  Do not retry a completed lifecycle handle blindly; repeated completion can otherwise hide an orchestration-state bug.
@@ -183,7 +190,7 @@ The integration API is transport-agnostic. A launcher can use a local subprocess
183
190
 
184
191
  - passes the returned environment unchanged to the child Pi process;
185
192
  - does not serialize the envelope or raw task into telemetry or captured logs;
186
- - reports launcher completion/failure separately from child completion;
193
+ - reports launcher failure separately from child completion, failure, or cancellation;
187
194
  - records wait and join around the transport's actual blocking and result-collection boundaries;
188
195
  - guarantees the child loads a compatible ObservMe extension;
189
196
  - cleans up temporary files, pipes, sessions, containers, or remote resources deterministically.
@@ -105,7 +105,7 @@ Optional telemetry:
105
105
 
106
106
  ### `custom` entry
107
107
 
108
- Extension state entry created through `pi.appendEntry()`. It does not participate in LLM context. ObservMe should append a custom entry only for explicitly enabled minimal correlation state; by default it should not append custom entries.
108
+ Extension state entry created through `pi.appendEntry()`. It does not participate in LLM context. When `agent.writeCorrelationEntry` is explicitly enabled, ObservMe appends at most one `observme.correlation` entry per active branch after telemetry startup succeeds. The versioned data contains only bounded workflow/agent lineage identifiers and depth. Startup scans `ctx.sessionManager.getBranch()` from newest to oldest, restores the latest valid entry, ignores corrupt entries and entries on abandoned branches, and skips an idempotent reload write. By default ObservMe does not append custom entries.
109
109
 
110
110
  ### `custom_message` entry
111
111
 
@@ -244,12 +244,12 @@ The derived id must be scoped by session id and agent run id and must not be use
244
244
 
245
245
  On extension startup in an existing session:
246
246
 
247
- 1. Read current session header if available.
247
+ 1. Read the current session header from `ctx.sessionManager` if available.
248
248
  2. Set `pi.session.id`, `pi.workflow.id`, `pi.agent.id`, and resource attributes.
249
- 3. Reconstruct optional workflow/agent lineage only from trusted environment variables or an explicitly enabled minimal custom correlation entry.
250
- 4. Do not replay old telemetry by default.
251
- 5. Optionally support `replayOnStart: true` for backfill, but it must be off by default.
252
- 6. If replay is enabled, mark spans/logs with `observme.replayed=true`.
249
+ 3. When `agent.writeCorrelationEntry` is enabled, reconstruct optional workflow/agent lineage only from the latest validated `observme.correlation` custom entry on `ctx.sessionManager.getBranch()`.
250
+ 4. Ignore malformed entries and entries outside the active branch without exporting their values or changing LLM context.
251
+ 5. After successful startup, append the minimal custom entry only when the active branch does not already contain the same validated correlation.
252
+ 6. Never replay historical telemetry automatically. Historical export requires the explicit, confirmed `/obs backfill` command, which marks each emitted record with `observme.replayed=true`.
253
253
 
254
254
  ## 11. Local Session Reading Rules
255
255
 
@@ -503,9 +503,46 @@ observme_handler_duration_ms
503
503
 
504
504
  ```text
505
505
  observme_active_spans
506
- observme_active_agents
506
+ observme_active_agents # compatibility lifecycle claim
507
+ observme_agent_lease_expires_unixtime_seconds # asynchronous gauge, unit s
507
508
  ```
508
509
 
510
+ #### 12.4.1 Active-agent lease and clock contract
511
+
512
+ This section is the single source of truth for active-agent lease constants and semantics. Runtime configuration, metric metadata, PromQL, dashboards, alerts, examples, and tests must reference these values rather than define different local windows.
513
+
514
+ | Contract item | Required value |
515
+ |---|---|
516
+ | Lease metric | `observme_agent_lease_expires_unixtime_seconds` |
517
+ | OTel instrument and unit | Asynchronous gauge, unit `s` |
518
+ | Observation value | `(wallClockUnixMillis + metrics.activeAgentLeaseDurationMillis) / 1000`; fractional Unix seconds are valid |
519
+ | Configuration | `metrics.activeAgentLeaseDurationMillis`; `OBSERVME_ACTIVE_AGENT_LEASE_DURATION_MS` is its environment override |
520
+ | Default lease | `60000` ms (60 seconds) |
521
+ | Inclusive duration bounds | `10000` ms through `300000` ms (10 seconds through 5 minutes) |
522
+ | Export relationship | Lease duration must be at least `(2 * metrics.exportIntervalMillis) + 5000` ms |
523
+ | Supported wall-clock skew | Producer and Prometheus clocks must differ by no more than 5 seconds |
524
+ | Future-timestamp horizon | A lease later than `time() + 305` seconds is pathological and must not count active |
525
+
526
+ `observme_active_agents` remains the clean lifecycle claim. An agent is operationally active only when its claim is positive and the matching lease is finite, later than Prometheus `time()`, and no later than the future-timestamp horizon. Missing, expired, malformed, non-finite, or pathologically future leases fail closed. Lease renewal occurs once per SDK metric collection while the session is active, including otherwise idle sessions; it does not depend on turns, tool calls, or a background heartbeat timer.
527
+
528
+ On clean shutdown, deactivate the lease before the final metric force-flush and record the matching `observme_active_agents` decrement. This makes the leased result zero after normal export/scrape propagation without waiting for expiry. If shutdown telemetry cannot run, the last exported lease is allowed to expire. With synchronized clocks, an ungraceful exit converges to zero within the configured lease plus one Prometheus scrape/evaluation interval. Across the full supported positive-skew budget, the measurable worst-case bound is the configured lease plus 5 seconds plus one scrape/evaluation interval; for the defaults and a 15-second scrape interval, that is at most 80 seconds.
529
+
530
+ The only per-runtime Prometheus join and deduplication key is the Collector-normalized `observme_instance_id` resource label. ObservMe generates its UUID independently for each telemetry session and never derives it from prompts, responses, content, paths, commands, GitHub run IDs, sessions, workflows, or logical agents. `service.instance.id` may carry the same resource value for OTel interoperability, but active-agent PromQL joins on `observme_instance_id` and deduplicates repeated exporter/scrape representations with `max by (observme_instance_id)`. The lease observation itself may carry only existing bounded metric labels; the instance identity comes from resource-to-metric conversion and must not become a dashboard variable or legend.
531
+
532
+ Lifecycle and failure behavior is fixed as follows:
533
+
534
+ | Condition | Required leased-active behavior |
535
+ |---|---|
536
+ | Clean exit | Deactivate before final flush; the active claim becomes zero without waiting for lease expiry. |
537
+ | `SIGTERM` | Use the clean path when Pi delivers shutdown; otherwise use the same expiry behavior as an abrupt exit. |
538
+ | Crash, `SIGKILL`, cancelled GitHub job, or runner/power loss | No cleanup is required for correctness; the last lease expires within the documented convergence bound. |
539
+ | Duplicate session start, reload, or resume | Deactivate and flush the replaced session before activating its replacement. If replacement cleanup is interrupted, the old instance lease expires independently. |
540
+ | Startup failure or partial runtime construction | Never leave an activated observable callback; dispose any registered callback. |
541
+ | Collector or OTLP outage | Cached positive claims stop counting when their last delivered leases expire. A still-running producer becomes active again after export recovers and a fresh lease arrives. Backend unavailability itself remains an unknown/no-query state. |
542
+ | Clock skew or clock movement | A producer clock ahead can delay expiry and one behind can fail closed early. Clocks outside the supported skew are unsupported; non-finite observations are omitted and values beyond the future horizon do not count. GitHub-hosted runners satisfy the synchronized-clock expectation; self-hosted runners must use reliable time synchronization. |
543
+
544
+ A renewable lease is a bounded liveness signal, not proof that the operating-system process is reachable at the exact query instant. `observme_active_agents` remains emitted with its existing name and clean-lifecycle behavior for compatibility, so the change is additive under the semantic-convention versioning policy and does not require a major convention-version increment. Consumers that need crash-safe live counts must migrate from raw active-claim sums to the leased definition.
545
+
509
546
  ### 12.5 Lifecycle recording points
510
547
 
511
548
  - Record `observme_tool_result_size_chars` once from the finalized `tool_execution_end` result, using the same bounded tool labels as tool-call metrics.
@@ -665,6 +702,7 @@ agent.run.failed
665
702
  agent.spawn.started
666
703
  agent.spawn.completed
667
704
  agent.spawn.failed
705
+ agent.spawn.cancelled
668
706
  agent.wait.started
669
707
  agent.wait.completed
670
708
  agent.join.started
@@ -47,6 +47,7 @@ metrics:
47
47
  enabled: true
48
48
  exportIntervalMillis: 15000
49
49
  exportTimeoutMillis: 3000
50
+ activeAgentLeaseDurationMillis: 60000
50
51
 
51
52
  logs:
52
53
  enabled: true
@@ -58,6 +59,8 @@ logs:
58
59
 
59
60
  The OpenTelemetry JS OTLP HTTP exporters default to signal-specific paths such as `http://localhost:4318/v1/traces`, `http://localhost:4318/v1/metrics`, and `/v1/logs`. Treat `otlp.endpoint` as a base endpoint in ObservMe config, but pass signal-specific URLs to SDK exporters that require an explicit `url`.
60
61
 
62
+ The default active-agent lease is 60 seconds and renews on each 15-second metric collection. Supported lease values are 10 seconds through 5 minutes and must be at least twice the export interval plus 5 seconds. Keep the producer and Prometheus clocks within 5 seconds; otherwise a lease may fail closed early or remain valid longer than intended. The exact contract is frozen in [`04-telemetry-semantic-conventions.md` §12.4.1](04-telemetry-semantic-conventions.md#1241-active-agent-lease-and-clock-contract).
63
+
61
64
  ## 4. Trace Context and Workflow/Agent-Lineage Propagation
62
65
 
63
66
  When ObservMe launches or wraps a subagent process, the parent runtime should propagate both standard trace context and ObservMe-specific lineage:
@@ -118,10 +121,10 @@ service:
118
121
 
119
122
  ## 6. Production Collector for Grafana Stack
120
123
 
121
- This configuration routes traces to Tempo, logs to Loki, and metrics to a Prometheus-compatible remote write endpoint such as Mimir. Use a Collector distribution that contains every configured component. The minimal debug example works with the core Collector, while `prometheusremotewrite`, `probabilistic_sampler`, and `tail_sampling` are commonly deployed from the Collector Contrib distribution or a vendor distribution such as Grafana Alloy.
124
+ This configuration routes traces to Tempo, logs to Loki, and metrics to a Prometheus scrape endpoint. Use a Collector distribution that contains every configured component. The minimal debug example works with the core Collector, while `prometheus`, `prometheusremotewrite`, `probabilistic_sampler`, and `tail_sampling` are commonly deployed from the Collector Contrib distribution or a vendor distribution such as Grafana Alloy.
122
125
 
123
126
  ```yaml
124
- # Production-oriented Collector reference for Tempo, Loki, and Mimir.
127
+ # Production-oriented Collector reference for Tempo, Loki, and Prometheus.
125
128
  # Verify that your Collector distribution contains every configured component,
126
129
  # replace backend endpoints/security for your deployment, and never add raw
127
130
  # content or high-cardinality execution identifiers to metric labels.
@@ -220,14 +223,14 @@ exporters:
220
223
  retry_on_failure:
221
224
  enabled: true
222
225
 
223
- prometheusremotewrite/mimir:
224
- endpoint: http://mimir:9009/api/v1/push
226
+ prometheus:
227
+ endpoint: 0.0.0.0:8889
228
+ # Exporter-wide stale-series/cardinality cleanup for gauges, counters, and
229
+ # histograms. Five minutes exceeds ObservMe's default one-minute active-agent
230
+ # lease; lease-aware PromQL, not expiration, determines liveness.
231
+ metric_expiration: 5m
225
232
  resource_to_telemetry_conversion:
226
233
  enabled: true
227
- sending_queue:
228
- enabled: true
229
- retry_on_failure:
230
- enabled: true
231
234
 
232
235
  debug:
233
236
  verbosity: basic
@@ -243,7 +246,7 @@ service:
243
246
  metrics:
244
247
  receivers: [otlp]
245
248
  processors: [memory_limiter, resource/observme, resource/drop_high_cardinality_metric_attrs, batch]
246
- exporters: [prometheusremotewrite/mimir]
249
+ exporters: [prometheus]
247
250
 
248
251
  logs:
249
252
  receivers: [otlp]
@@ -251,9 +254,19 @@ service:
251
254
  exporters: [otlphttp/loki]
252
255
  ```
253
256
 
257
+ ### 6.1 Prometheus exporter expiration policy
258
+
259
+ The shipped local and production scrape-exporter examples set `metric_expiration: 5m`. This exporter-level setting applies uniformly to every Prometheus-exported gauge, counter, and histogram; it cannot be scoped only to active-agent metrics. Its only purpose is stale-series and cardinality cleanup: it bounds the Collector memory used by abandoned metric streams and eventually lets Prometheus mark an expired series stale after the exporter stops exposing it. Lease-aware PromQL remains the active-agent correctness mechanism, even while the Collector still exposes a cached positive `observme_active_agents` value.
260
+
261
+ Five minutes comfortably exceeds the default 60-second active-agent lease and 15-second SDK metric export interval. If either interval is customized, keep `metric_expiration` longer than the lease and allow enough additional time for expected export delays and scrape gaps. A shorter value cleans abandoned streams sooner but can evict gauges, counters, and histograms during a temporary producer outage; a longer value consumes Collector memory and preserves abandoned cardinality for longer. It must never be shortened merely to make active-agent totals converge.
262
+
263
+ Apply a Collector configuration change through the deployment's supported reload mechanism or restart the Collector. A restart clears the Prometheus exporter's in-memory series cache, but neither reload nor restart deletes samples already stored in Prometheus; historical samples remain subject to Prometheus retention and deletion policy. Restart is never required merely to make an expired active-agent lease stop counting. `metric_expiration` belongs only to the metrics scrape exporter, so traces and logs are unaffected. Remote-write deployments do not use this scrape-exporter cache setting and must apply their backend's series-retention policy, while continuing to use the same lease-aware active-agent queries.
264
+
265
+ When migrating, deploy a version that emits `observme_agent_lease_expires_unixtime_seconds`, verify the Collector preserves the generated `observme_instance_id`, then replace every current-activity raw sum in dashboards, recording rules, and alerts with the canonical queries in [`09-dashboards-alerts-slos.md` §1.3.1](09-dashboards-alerts-slos.md#131-canonical-active-agent-promql). Keep raw and expired claims only as clearly labeled diagnostics. Missing leases intentionally fail closed, so do not switch a mixed-version fleet until the lease metric is visible for the producers that should count.
266
+
254
267
  The Collector attribute processor is defense in depth for log attributes only. It must not be the only redaction layer, and it does not sanitize arbitrary log bodies; ObservMe must redact or drop sensitive content before export. The traces pipeline intentionally keeps `pi.llm.prompt.redacted`, `pi.llm.response.redacted`, and `pi.llm.thinking.redacted` so Tempo can display redacted content after explicit capture is enabled. Old telemetry that an earlier Collector dropped cannot be recovered; generate new LLM events after updating the Collector.
255
268
 
256
- Keep high-cardinality lineage attributes on traces/logs, but remove them from the metrics pipeline unless the organization explicitly accepts the cardinality. The bundled local Loki pipeline promotes `pi.agent.id` and `pi.agent.run.id` as log labels so the LLM Conversations dashboard can filter captured chat content by agent and run. Prometheus resource-to-telemetry conversion is safe only after the metrics pipeline drops `pi.workflow.id`, `pi.agent.id`, `pi.session.id`, trace IDs, and spawn IDs; the remaining generated `service.instance.id`/`observme.instance.id` labels keep concurrent ObservMe metric streams distinct so session counters can be summed accurately.
269
+ Keep high-cardinality lineage attributes on traces/logs, but remove them from the metrics pipeline unless the organization explicitly accepts the cardinality. The bundled local Loki pipeline promotes `pi.agent.id` and `pi.agent.run.id` as log labels so the LLM Conversations dashboard can filter captured chat content by agent and run. Prometheus resource-to-telemetry conversion is safe only after the metrics pipeline drops `pi.workflow.id`, `pi.agent.id`, `pi.session.id`, trace IDs, and spawn IDs. Preserve the generated `service.instance.id`/`observme.instance.id` resource identity in the metrics pipeline so lease-aware queries can join and deduplicate concurrent ObservMe metric streams without exposing workflow, session, logical-agent, trace, span, or job-run identifiers as labels.
257
270
 
258
271
  ## 7. Direct-to-Backend Development Mode
259
272
 
@@ -105,7 +105,7 @@ GitHub token: (gh[pousr]_[A-Za-z0-9_]{36,}|github_pat_[A-Za-z0-9_
105
105
  OpenAI-like key: sk-[A-Za-z0-9_-]{20,}
106
106
  Anthropic-like key: sk-ant-[A-Za-z0-9_-]{20,}
107
107
  Slack token: xox[baprs]-[A-Za-z0-9-]{10,}
108
- Private key block: -----BEGIN [A-Z ]*PRIVATE KEY-----
108
+ Private key block: complete uppercase labels ending in PRIVATE KEY (including PKCS#8, encrypted, RSA, and EC)
109
109
  Password assignment: (?i)(password|passwd|pwd)\s*[:=]\s*[^\s]+
110
110
  API key assignment: (?i)(api[_-]?key|token|secret|client[_-]?secret)\s*[:=]\s*[^\s]+
111
111
  URL credentials: [a-z][a-z0-9+.-]*://[^\s:/?#]+:[^\s@/]+@
@@ -113,6 +113,8 @@ URL credentials: [a-z][a-z0-9+.-]*://[^\s:/?#]+:[^\s@/]+@
113
113
 
114
114
  These patterns are a minimum safety net, not a complete secret scanner. Keep them covered by tests and allow organizations to add custom regexes for proprietary credential formats.
115
115
 
116
+ Private-key matching consumes a `PRIVATE KEY` label with an optional, bounded uppercase prefix, its body, and a same-label `END` marker with a 1,000,000-character body scan bound. This covers PKCS#8 (`PRIVATE KEY`), `ENCRYPTED PRIVATE KEY`, `RSA PRIVATE KEY`, `EC PRIVATE KEY`, and other uppercase private-key labels without accepting public-key labels. The shared input-size guard drops larger captured values before secret matching. If a supported private-key marker has no matching footer because it is missing, truncated, or mislabeled, redaction fails closed by replacing everything from that marker through the end of the captured value. `PUBLIC KEY` and `RSA PUBLIC KEY` blocks are not private-key matches.
117
+
116
118
  Matched values are replaced with:
117
119
 
118
120
  ```text
@@ -228,7 +230,7 @@ Example:
228
230
 
229
231
  The Collector cannot recover content dropped by an older configuration; generate new LLM events after updating the Collector and dashboards. Do not use raw chat text in Loki or Tempo query strings.
230
232
 
231
- ## 11. Authentication
233
+ ## 11. Authentication and Credential-Free Endpoint URLs
232
234
 
233
235
  ObservMe supports OTLP headers:
234
236
 
@@ -238,7 +240,9 @@ otlp:
238
240
  Authorization: "Bearer ${OBSERVME_OTLP_TOKEN}"
239
241
  ```
240
242
 
241
- Secrets must be read from environment variables or secure runtime config, not hardcoded in extension source.
243
+ Grafana base URLs must be absolute HTTP(S) URLs without embedded username or password components. Configure Grafana authentication only through `query.grafana.token` or the complete `query.grafana.username` and `query.grafana.password` pair. A credential-bearing base URL is rejected during configuration validation and query readiness, before either Grafana transport can perform network I/O. Diagnostics report only the safe `embedded_credentials` failure class and dedicated setting names; they do not render the rejected URL or credential values.
244
+
245
+ Secrets must be read from environment variables or secure runtime config, not hardcoded in extension source or embedded in endpoint URLs.
242
246
 
243
247
  ## 12. TLS
244
248
 
@@ -246,12 +250,12 @@ Production default:
246
250
 
247
251
  ```yaml
248
252
  otlp:
253
+ endpoint: https://otel-collector.example.com:4318
249
254
  tls:
250
- enabled: true
251
255
  insecureSkipVerify: false
252
256
  ```
253
257
 
254
- Development may use insecure local endpoints only when explicitly configured.
258
+ The endpoint URL scheme selects TLS. The OTLP exporters explicitly keep certificate verification enabled unless `otlp.tls.insecureSkipVerify: true` is configured. Grafana query transport follows the same rule through `query.grafana.tls.insecureSkipVerify`. Production rejects either verification bypass, and any plain HTTP endpoint, unless `privacy.allowInsecureTransport: true` records the explicit insecure-transport acknowledgement. Development may use local HTTP or self-signed certificates, but the insecure setting remains visible in `/obs status` and `/obs health`.
255
259
 
256
260
  ## 13. Tenant Isolation
257
261
 
@@ -281,10 +285,13 @@ Do not log secrets while reporting security failures.
281
285
 
282
286
  ## 15. Safe Config Validation
283
287
 
288
+ Trusted project config, project `.env`, and starter-config creation share one filesystem boundary. Their lexical paths and canonical targets must remain inside the stable canonical project root. Existing symlinks are supported only when they resolve to files or directories inside that root; out-of-root, dangling, replaced, or unverifiable paths fail closed. Reads and starter writes use identity-verified file handles and recheck containment so a concurrent file, ancestor, or root change cannot substitute an external target. A rejected project source is not loaded, starter creation does not overwrite an existing file, and diagnostics expose only a bounded failure class rather than canonical or external path details.
289
+
284
290
  Reject config if:
285
291
 
286
292
  - capture is enabled but redaction is disabled, unless `allowUnsafeCapture: true`
287
- - OTLP endpoint is plain HTTP in production, unless `allowInsecureTransport: true`
293
+ - an OTLP or Grafana endpoint is plain HTTP in production, unless `allowInsecureTransport: true`
294
+ - OTLP or Grafana certificate verification is bypassed in production, unless `allowInsecureTransport: true`
288
295
  - metric labels include forbidden high-cardinality fields such as workflow IDs, session IDs, or agent IDs
289
296
  - propagated workflow or agent lineage values are malformed, too long, or contain unsafe characters
290
297
  - queue sizes exceed configured memory limits
@@ -43,9 +43,10 @@ query:
43
43
 
44
44
  Authentication and local transport behavior:
45
45
 
46
- - Prefer `query.grafana.token` for Grafana service-account or bearer tokens.
46
+ - Keep `query.grafana.url` credential-free: it must be an absolute HTTP(S) base URL with no embedded username or password component.
47
+ - Configure authentication only with `query.grafana.token` or the dedicated `query.grafana.username` and `query.grafana.password` settings. Prefer the token for Grafana service-account or bearer authentication.
47
48
  - If the token is blank or an unresolved environment placeholder, ObservMe may use `query.grafana.username` plus `query.grafana.password` as a local-development Basic auth fallback.
48
- - Query-backed commands fail fast before backend calls when Grafana auth is unresolved/missing/incomplete, `query.grafana.url` is invalid, or a required datasource UID is blank.
49
+ - Query-backed commands fail fast before backend calls when Grafana auth is unresolved/missing/incomplete, `query.grafana.url` is invalid or contains credentials, or a required datasource UID is blank. Credential-bearing URLs report the safe `embedded_credentials` class without rendering the URL or credential values.
49
50
  - `401` and `403` responses are surfaced as Grafana authentication failures without printing token or password values.
50
51
  - Browser login cookies are irrelevant because `/obs` commands call the Grafana API directly from the Pi process.
51
52
 
@@ -71,7 +72,7 @@ query:
71
72
  preferIPv4: true
72
73
  ```
73
74
 
74
- For the bundled `observability-stack/`, the supported command path is nginx HTTPS at `https://observability.local`; the default stack does not publish Grafana on `localhost:3000`. Create a Grafana service-account token with Viewer access for read-only datasource queries and export it as `OBSERVME_GRAFANA_TOKEN`, or export/set `OBSERVME_GRAFANA_PASSWORD` from `observability-stack/secrets/grafana_admin_password` for local Basic auth. Extension environment values can come from system environment variables before Pi starts, or from a trusted project `.env` copied from `.env.example`; system variables override `.env`. The local self-signed certificate requires `query.grafana.tls.insecureSkipVerify=true`, and `query.grafana.transport.preferIPv4=true` avoids DNS stalls when `observability.local` resolves to IPv6 first. 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`.
75
+ For the bundled `observability-stack/`, the supported command path is nginx HTTPS at `https://observability.local`; the default stack does not publish Grafana on `localhost:3000`. Create a Grafana service-account token with Viewer access for read-only datasource queries and export it as `OBSERVME_GRAFANA_TOKEN`, or export/set `OBSERVME_GRAFANA_PASSWORD` from `observability-stack/secrets/grafana_admin_password` for local Basic auth. Extension environment values can come from system environment variables before Pi starts, or from a trusted project `.env` copied from `.env.example`; system variables override `.env`. The local self-signed certificate requires `query.grafana.tls.insecureSkipVerify=true`, and `query.grafana.transport.preferIPv4=true` avoids DNS stalls when `observability.local` resolves to IPv6 first. Production configurations using the TLS bypass must also set `privacy.allowInsecureTransport=true` as an explicit acknowledgement; trusting the production CA remains preferred. 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`.
75
76
 
76
77
  ## 4. Commands
77
78
 
@@ -84,6 +85,9 @@ Output:
84
85
  ```text
85
86
  ObservMe: enabled
86
87
  OTLP endpoint: https://otel.example.com:4318
88
+ OTLP transport security: TLS certificate verification enabled
89
+ Grafana URL: https://grafana.example.com/
90
+ Grafana transport security: TLS certificate verification enabled
87
91
  Traces: enabled
88
92
  Metrics: enabled
89
93
  Logs: enabled
@@ -97,6 +101,8 @@ Last export error: none
97
101
  Checks Collector and Grafana reachability, and reports Grafana query auth/config readiness before datasource calls.
98
102
 
99
103
  ```text
104
+ Collector transport security: TLS certificate verification enabled
105
+ Grafana transport security: TLS certificate verification enabled
100
106
  Collector: reachable
101
107
  Grafana: reachable
102
108
  Tempo datasource: ok
@@ -216,13 +222,22 @@ Only available if Loki receives `pi.session.id` as structured metadata or label.
216
222
 
217
223
  ## 5. Trace Links
218
224
 
219
- If trace id is available:
225
+ `/obs session`, `/obs trace`, and `/obs link` use the same pure trace-link builder, so one effective config and trace ID always produce one canonical URL. Custom absolute HTTP(S) templates support these trace-ID placeholders:
220
226
 
221
- ```text
222
- https://grafana.example.com/explore?schemaVersion=1&panes=...
227
+ - `{traceId}`
228
+ - `{{traceId}}` or `{{ traceId }}`
229
+ - `${traceId}`
230
+ - `%TRACE_ID%`
231
+
232
+ The matching optional Tempo datasource placeholders are `{tempoDatasourceUid}`, `{{tempoDatasourceUid}}` (whitespace is allowed), `${tempoDatasourceUid}`, and `%TEMPO_DATASOURCE_UID%`. Trace IDs and datasource UIDs are URL-encoded before substitution.
233
+
234
+ An empty template or a template containing `...` selects the structured Grafana Explore fallback. The fallback uses `query.grafana.url`, preserves any path prefix, and constructs the current `schemaVersion=1&panes=...` Tempo query with `query.grafana.datasourceUids.tempo`. This is the behavior used by the generated starter:
235
+
236
+ ```yaml
237
+ traceUrlTemplate: https://observability.local/explore?left=...
223
238
  ```
224
239
 
225
- ObservMe should support configurable URL templates because Grafana URL encoding changes across versions and organizations.
240
+ Unsupported placeholders, non-HTTP(S) URLs, credential-bearing URLs, and malformed or oversized templates produce one bounded actionable diagnostic; commands do not silently omit a link or apply different substitutions.
226
241
 
227
242
  ## 6. Grafana Query Abstraction
228
243
 
@@ -258,6 +273,14 @@ query:
258
273
  maxAgents: 20
259
274
  ```
260
275
 
276
+ Each query result-count setting accepts integers from 1 through 100. Runtime query clients clamp programmatically supplied values to the same ceiling before adding a backend request limit or selecting parsed results.
277
+
278
+ ### Bounded command display
279
+
280
+ Prometheus label keys and values used for `/obs` display are normalized before they enter command snapshots. Control and line-separator characters become spaces, repeated whitespace collapses, and each label is limited to 96 UTF-16 code units. Truncated labels retain a valid Unicode prefix and end with `…`.
281
+
282
+ `/obs cost` renders at most 20 rows. `/obs tools` renders at most 20 call rows and 20 failure rows. Omitted rows are reported explicitly. `/obs agents` retains its 10-row recent-child display limit. Every final `/obs` notification is normalized to remove terminal controls other than intended line feeds and is limited to 64 rows and 8,192 UTF-16 code units. A notification that reaches either hard limit ends with `… output truncated`. These display bounds do not change backend queries, metric identity, or emitted telemetry.
283
+
261
284
  ## 8. Dependency Direction
262
285
 
263
286
  ObservMe telemetry emission must not depend on Grafana query availability.