@senad-d/observme 0.1.4 → 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.
- package/CHANGELOG.md +35 -0
- package/README.md +24 -8
- package/docs/agent-subagent-observability-requirements.md +4 -3
- package/docs/compatibility-matrix.md +11 -2
- package/docs/configuration.md +2 -2
- package/docs/extension-integration.md +20 -13
- package/docs/reference/03-pi-event-and-session-model.md +6 -6
- package/docs/reference/04-telemetry-semantic-conventions.md +1 -0
- package/docs/reference/06-security-privacy-redaction.md +13 -6
- package/docs/reference/08-query-grafana-integration.md +30 -7
- package/docs/reference/11-deployment-runbooks.md +21 -0
- package/docs/reference/12-configuration-reference.md +30 -4
- package/examples/integrations/subagent-runner.ts +8 -5
- package/examples/observme.yaml +2 -2
- package/package.json +12 -4
- package/src/commands/obs-agents.ts +17 -12
- package/src/commands/obs-backfill.ts +2 -2
- package/src/commands/obs-command-support.ts +2 -1
- package/src/commands/obs-cost.ts +15 -7
- package/src/commands/obs-health.ts +22 -2
- package/src/commands/obs-loki-summary.ts +4 -8
- package/src/commands/obs-session.ts +35 -28
- package/src/commands/obs-status.ts +56 -13
- package/src/commands/obs-tools.ts +19 -8
- package/src/commands/obs-trace.ts +9 -0
- package/src/commands/obs.ts +6 -1
- package/src/config/bootstrap-project-config.ts +49 -32
- package/src/config/defaults.ts +0 -2
- package/src/config/load-config.ts +270 -92
- package/src/config/project-paths.ts +318 -6
- package/src/config/query-limits.ts +10 -0
- package/src/config/schema.ts +9 -8
- package/src/config/transport-security.ts +107 -0
- package/src/config/validate.ts +68 -11
- package/src/extension.ts +2 -12
- package/src/integration.ts +6 -2
- package/src/otel/logs.ts +24 -13
- package/src/otel/metrics.ts +24 -13
- package/src/otel/otlp-endpoint.ts +41 -2
- package/src/otel/otlp-http-options.ts +11 -0
- package/src/otel/sdk.ts +155 -9
- package/src/otel/shutdown.ts +39 -11
- package/src/otel/traces.ts +34 -16
- package/src/pi/agent-tree-tracker.ts +14 -1
- package/src/pi/compatibility.ts +121 -0
- package/src/pi/event-handlers/agent-turn.ts +16 -9
- package/src/pi/event-handlers/lifecycle.ts +207 -75
- package/src/pi/event-handlers/llm.ts +17 -9
- package/src/pi/event-handlers/session-events.ts +53 -19
- package/src/pi/event-handlers/tool-bash.ts +21 -27
- package/src/pi/handler-internals.ts +16 -26
- package/src/pi/handler-runtime.ts +138 -51
- package/src/pi/handler-types.ts +30 -15
- package/src/pi/handlers.ts +12 -1
- package/src/pi/integration-api.ts +27 -15
- package/src/pi/session-correlation.ts +167 -0
- package/src/pi/subagent-spawn.ts +164 -31
- package/src/privacy/redact.ts +574 -68
- package/src/privacy/secret-patterns.ts +6 -1
- package/src/query/grafana-readiness.ts +18 -2
- package/src/query/grafana-transport.ts +150 -27
- package/src/query/grafana-url.ts +36 -0
- package/src/query/grafana.ts +4 -136
- package/src/query/loki.ts +2 -4
- package/src/query/prometheus.ts +8 -10
- package/src/query/tempo.ts +2 -4
- package/src/query/trace-link.ts +186 -0
- package/src/safety/display-bounds.ts +84 -0
- package/src/semconv/metrics.ts +1 -0
- 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`
|
|
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
|
-
-
|
|
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
|
|
|
@@ -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
|
|
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:
|
|
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"]):
|
|
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";
|
package/examples/observme.yaml
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ObservMe: a Pi extension that instruments Pi agent sessions and exports OpenTelemetry traces, metrics, and logs to an external observability stack (OTel Collector, Grafana Tempo/Loki/Prometheus).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -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
|
+
"minimumVersion": "0.80.5",
|
|
80
|
+
"releaseTestedVersion": "0.80.6",
|
|
81
|
+
"supportedRange": ">=0.80.5 <0.81.0"
|
|
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": "
|
|
92
|
-
"@earendil-works/pi-coding-agent": "
|
|
99
|
+
"@earendil-works/pi-ai": "0.80.6",
|
|
100
|
+
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
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": "
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 {
|
package/src/commands/obs-cost.ts
CHANGED
|
@@ -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
|
|
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)
|
|
125
|
-
|
|
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
|
|
199
|
+
return normalizeObsBackendLabel(value) ?? "unknown";
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
function normalizeOptionalString(value: string | undefined): string | undefined {
|
|
194
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
if (!normalized) return undefined;
|
|
145
|
-
if (normalized.length <= maximumDisplayValueLength) return normalized;
|
|
146
|
-
return `${normalized.slice(0, maximumDisplayValueLength - 1)}…`;
|
|
142
|
+
return normalizeObsBackendLabel(value);
|
|
147
143
|
}
|
|
148
144
|
|
|
149
145
|
function renderObsLokiLogSummaryRow(row: ObsLokiLogSummaryRow): string {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ObservMeConfig } from "../config/schema.ts";
|
|
3
|
+
import { readDiagnosticMessage, sanitizeDiagnosticText } from "../diagnostics/sanitize.ts";
|
|
4
|
+
import { buildGrafanaTraceLink } from "../query/trace-link.ts";
|
|
2
5
|
import { completeObsSubcommand, isExactObsSubcommandRequest } from "./obs-args.ts";
|
|
6
|
+
import { notifyObsCommand } from "./obs-command-support.ts";
|
|
3
7
|
|
|
4
8
|
export interface ObsSessionCommandContext {
|
|
5
9
|
readonly ui: {
|
|
@@ -15,6 +19,7 @@ export interface ObsSessionSnapshot {
|
|
|
15
19
|
readonly toolCalls: number;
|
|
16
20
|
readonly costUsd: number;
|
|
17
21
|
readonly traceLink?: string;
|
|
22
|
+
readonly traceLinkError?: string;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export interface ObsSessionRuntimeStatePatch {
|
|
@@ -25,12 +30,13 @@ export interface ObsSessionRuntimeStatePatch {
|
|
|
25
30
|
readonly toolCalls?: number;
|
|
26
31
|
readonly costUsd?: number;
|
|
27
32
|
readonly traceLink?: string;
|
|
33
|
+
readonly traceLinkError?: string;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export interface StartObsSessionRuntimeStateOptions {
|
|
31
37
|
readonly sessionId?: string;
|
|
32
38
|
readonly traceId?: string;
|
|
33
|
-
readonly
|
|
39
|
+
readonly config?: ObservMeConfig;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export type ObsSessionProvider = (ctx: ObsSessionCommandContext) => Promise<ObsSessionSnapshot> | ObsSessionSnapshot;
|
|
@@ -47,6 +53,12 @@ interface MutableObsSessionRuntimeState {
|
|
|
47
53
|
toolCalls: number;
|
|
48
54
|
costUsd: number;
|
|
49
55
|
traceLink?: string;
|
|
56
|
+
traceLinkError?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ObsSessionTraceLinkState {
|
|
60
|
+
readonly traceLink?: string;
|
|
61
|
+
readonly traceLinkError?: string;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
const OBS_COMMAND_NAME = "obs";
|
|
@@ -71,15 +83,15 @@ export async function handleObsSessionCommand(
|
|
|
71
83
|
options: RegisterObsSessionCommandOptions = {},
|
|
72
84
|
): Promise<void> {
|
|
73
85
|
if (!isObsSessionRequest(args)) {
|
|
74
|
-
await
|
|
86
|
+
await notifyObsCommand(ctx, "Usage: /obs session", "warning");
|
|
75
87
|
return;
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
try {
|
|
79
91
|
const snapshot = await resolveObsSessionSnapshot(ctx, options);
|
|
80
|
-
await
|
|
92
|
+
await notifyObsCommand(ctx, renderObsSession(snapshot), snapshot.traceLinkError ? "warning" : "info");
|
|
81
93
|
} catch (error) {
|
|
82
|
-
await
|
|
94
|
+
await notifyObsCommand(ctx, `ObservMe session unavailable: ${formatError(error)}`, "error");
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -103,12 +115,13 @@ export function renderObsSession(snapshot: ObsSessionSnapshot): string {
|
|
|
103
115
|
];
|
|
104
116
|
|
|
105
117
|
if (normalized.traceLink) lines.push(`Open trace: ${normalized.traceLink}`);
|
|
118
|
+
else if (normalized.traceLinkError) lines.push(`Trace link unavailable: ${normalized.traceLinkError}`);
|
|
106
119
|
return lines.join("\n");
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
export function startObsSessionRuntimeState(options: StartObsSessionRuntimeStateOptions): void {
|
|
110
123
|
const traceId = normalizeTraceId(options.traceId);
|
|
111
|
-
const
|
|
124
|
+
const traceLinkState = createObsSessionTraceLinkState(options.config, traceId);
|
|
112
125
|
|
|
113
126
|
replaceObsSessionRuntimeState({
|
|
114
127
|
sessionId: normalizeString(options.sessionId),
|
|
@@ -117,10 +130,23 @@ export function startObsSessionRuntimeState(options: StartObsSessionRuntimeState
|
|
|
117
130
|
llmCalls: 0,
|
|
118
131
|
toolCalls: 0,
|
|
119
132
|
costUsd: 0,
|
|
120
|
-
|
|
133
|
+
...traceLinkState,
|
|
121
134
|
});
|
|
122
135
|
}
|
|
123
136
|
|
|
137
|
+
function createObsSessionTraceLinkState(
|
|
138
|
+
config: ObservMeConfig | undefined,
|
|
139
|
+
traceId: string | undefined,
|
|
140
|
+
): ObsSessionTraceLinkState {
|
|
141
|
+
if (!config || !traceId) return {};
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
return { traceLink: buildGrafanaTraceLink(config, traceId) };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return { traceLinkError: sanitizeDiagnosticText(readDiagnosticMessage(error)) };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
124
150
|
export function updateObsSessionRuntimeState(patch: ObsSessionRuntimeStatePatch): void {
|
|
125
151
|
if (patch.sessionId !== undefined) runtimeSessionState.sessionId = normalizeString(patch.sessionId);
|
|
126
152
|
if (patch.traceId !== undefined) runtimeSessionState.traceId = normalizeTraceId(patch.traceId);
|
|
@@ -129,6 +155,7 @@ export function updateObsSessionRuntimeState(patch: ObsSessionRuntimeStatePatch)
|
|
|
129
155
|
if (patch.toolCalls !== undefined) runtimeSessionState.toolCalls = normalizeCount(patch.toolCalls);
|
|
130
156
|
if (patch.costUsd !== undefined) runtimeSessionState.costUsd = normalizeCost(patch.costUsd);
|
|
131
157
|
if (patch.traceLink !== undefined) runtimeSessionState.traceLink = normalizeString(patch.traceLink);
|
|
158
|
+
if (patch.traceLinkError !== undefined) runtimeSessionState.traceLinkError = normalizeString(patch.traceLinkError);
|
|
132
159
|
}
|
|
133
160
|
|
|
134
161
|
export function recordObsSessionTurn(count = 1): void {
|
|
@@ -152,20 +179,6 @@ export function clearObsSessionRuntimeState(): void {
|
|
|
152
179
|
replaceObsSessionRuntimeState(createEmptyObsSessionRuntimeState());
|
|
153
180
|
}
|
|
154
181
|
|
|
155
|
-
export function buildObsSessionTraceLink(traceId: string | undefined, traceUrlTemplate: string | undefined): string | undefined {
|
|
156
|
-
const template = normalizeString(traceUrlTemplate);
|
|
157
|
-
if (!traceId || !template) return undefined;
|
|
158
|
-
|
|
159
|
-
if (template.includes("{{traceId}}")) return template.replaceAll("{{traceId}}", encodeURIComponent(traceId));
|
|
160
|
-
if (template.includes("${traceId}")) return template.replaceAll("${traceId}", encodeURIComponent(traceId));
|
|
161
|
-
if (template.includes("{traceId}")) return template.replaceAll("{traceId}", encodeURIComponent(traceId));
|
|
162
|
-
if (template.includes("$traceId")) return template.replaceAll("$traceId", encodeURIComponent(traceId));
|
|
163
|
-
if (template.includes("%TRACE_ID%")) return template.replaceAll("%TRACE_ID%", encodeURIComponent(traceId));
|
|
164
|
-
if (template.includes("__TRACE_ID__")) return template.replaceAll("__TRACE_ID__", encodeURIComponent(traceId));
|
|
165
|
-
if (template.includes("...")) return template.replaceAll("...", encodeURIComponent(traceId));
|
|
166
|
-
return undefined;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
182
|
class ObsSessionCommand {
|
|
170
183
|
readonly #options: RegisterObsSessionCommandOptions;
|
|
171
184
|
|
|
@@ -194,6 +207,7 @@ function replaceObsSessionRuntimeState(state: MutableObsSessionRuntimeState): vo
|
|
|
194
207
|
runtimeSessionState.toolCalls = state.toolCalls;
|
|
195
208
|
runtimeSessionState.costUsd = state.costUsd;
|
|
196
209
|
runtimeSessionState.traceLink = state.traceLink;
|
|
210
|
+
runtimeSessionState.traceLinkError = state.traceLinkError;
|
|
197
211
|
}
|
|
198
212
|
|
|
199
213
|
function createEmptyObsSessionRuntimeState(): MutableObsSessionRuntimeState {
|
|
@@ -214,6 +228,7 @@ function normalizeObsSessionSnapshot(snapshot: ObsSessionSnapshot): ObsSessionSn
|
|
|
214
228
|
toolCalls: normalizeCount(snapshot.toolCalls),
|
|
215
229
|
costUsd: normalizeCost(snapshot.costUsd),
|
|
216
230
|
traceLink: normalizeString(snapshot.traceLink),
|
|
231
|
+
traceLinkError: normalizeString(snapshot.traceLinkError),
|
|
217
232
|
};
|
|
218
233
|
}
|
|
219
234
|
|
|
@@ -246,14 +261,6 @@ function isObsSessionRequest(args: string): boolean {
|
|
|
246
261
|
return isExactObsSubcommandRequest(args, OBS_SESSION_SUBCOMMAND);
|
|
247
262
|
}
|
|
248
263
|
|
|
249
|
-
async function notifySession(
|
|
250
|
-
ctx: ObsSessionCommandContext,
|
|
251
|
-
message: string,
|
|
252
|
-
type: "info" | "warning" | "error",
|
|
253
|
-
): Promise<void> {
|
|
254
|
-
await ctx.ui?.notify?.(message, type);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
264
|
function formatError(error: unknown): string {
|
|
258
265
|
return error instanceof Error ? error.message : String(error);
|
|
259
266
|
}
|