@senad-d/observme 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.env.example +4 -0
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +36 -11
  4. package/dashboards/observme-agent-node-graphs.json +5 -5
  5. package/dashboards/observme-agents.json +169 -4
  6. package/dashboards/observme-alerts.yaml +16 -3
  7. package/dashboards/observme-overview.json +6 -6
  8. package/dashboards/observme-trace-journey.json +4 -4
  9. package/docs/agent-subagent-observability-requirements.md +19 -10
  10. package/docs/compatibility-matrix.md +21 -4
  11. package/docs/configuration.md +7 -2
  12. package/docs/extension-integration.md +20 -13
  13. package/docs/reference/03-pi-event-and-session-model.md +6 -6
  14. package/docs/reference/04-telemetry-semantic-conventions.md +39 -1
  15. package/docs/reference/05-otel-pipeline-and-collector.md +23 -10
  16. package/docs/reference/06-security-privacy-redaction.md +13 -6
  17. package/docs/reference/08-query-grafana-integration.md +30 -7
  18. package/docs/reference/09-dashboards-alerts-slos.md +132 -3
  19. package/docs/reference/10-testing-release-operations.md +44 -14
  20. package/docs/reference/11-deployment-runbooks.md +82 -5
  21. package/docs/reference/12-configuration-reference.md +48 -5
  22. package/docs/review-validation.md +17 -0
  23. package/examples/README.md +7 -2
  24. package/examples/collector.yaml +8 -8
  25. package/examples/integrations/subagent-runner.ts +8 -5
  26. package/examples/observme.yaml +3 -2
  27. package/package.json +14 -4
  28. package/src/commands/obs-agents.ts +17 -12
  29. package/src/commands/obs-backfill.ts +2 -2
  30. package/src/commands/obs-command-support.ts +2 -1
  31. package/src/commands/obs-cost.ts +15 -7
  32. package/src/commands/obs-health.ts +22 -2
  33. package/src/commands/obs-loki-summary.ts +4 -8
  34. package/src/commands/obs-session.ts +35 -28
  35. package/src/commands/obs-status.ts +56 -13
  36. package/src/commands/obs-tools.ts +19 -8
  37. package/src/commands/obs-trace.ts +9 -0
  38. package/src/commands/obs.ts +6 -1
  39. package/src/config/bootstrap-project-config.ts +50 -32
  40. package/src/config/defaults.ts +1 -2
  41. package/src/config/load-config.ts +270 -81
  42. package/src/config/project-paths.ts +318 -6
  43. package/src/config/query-limits.ts +10 -0
  44. package/src/config/schema.ts +18 -8
  45. package/src/config/transport-security.ts +107 -0
  46. package/src/config/validate.ts +88 -12
  47. package/src/extension.ts +2 -12
  48. package/src/integration.ts +6 -2
  49. package/src/otel/logs.ts +24 -13
  50. package/src/otel/metrics.ts +24 -13
  51. package/src/otel/otlp-endpoint.ts +41 -2
  52. package/src/otel/otlp-http-options.ts +11 -0
  53. package/src/otel/sdk.ts +155 -9
  54. package/src/otel/shutdown.ts +39 -11
  55. package/src/otel/traces.ts +34 -16
  56. package/src/pi/active-agent-lease.ts +101 -0
  57. package/src/pi/agent-tree-tracker.ts +14 -1
  58. package/src/pi/compatibility.ts +121 -0
  59. package/src/pi/event-handlers/agent-turn.ts +16 -9
  60. package/src/pi/event-handlers/lifecycle.ts +312 -106
  61. package/src/pi/event-handlers/llm.ts +17 -9
  62. package/src/pi/event-handlers/session-events.ts +53 -19
  63. package/src/pi/event-handlers/tool-bash.ts +21 -27
  64. package/src/pi/handler-internals.ts +16 -26
  65. package/src/pi/handler-runtime.ts +159 -54
  66. package/src/pi/handler-types.ts +40 -17
  67. package/src/pi/handlers.ts +12 -1
  68. package/src/pi/integration-api.ts +27 -15
  69. package/src/pi/session-correlation.ts +167 -0
  70. package/src/pi/subagent-spawn.ts +164 -31
  71. package/src/privacy/redact.ts +574 -68
  72. package/src/privacy/secret-patterns.ts +6 -1
  73. package/src/query/grafana-readiness.ts +18 -2
  74. package/src/query/grafana-transport.ts +150 -27
  75. package/src/query/grafana-url.ts +36 -0
  76. package/src/query/grafana.ts +4 -136
  77. package/src/query/loki.ts +2 -4
  78. package/src/query/prometheus.ts +8 -10
  79. package/src/query/tempo.ts +2 -4
  80. package/src/query/trace-link.ts +186 -0
  81. package/src/safety/display-bounds.ts +84 -0
  82. package/src/semconv/metrics.ts +7 -0
  83. package/src/semconv/values.ts +2 -0
@@ -1,9 +1,21 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import type { LoadSessionConfigOptions, SessionConfigDiagnostics, SessionConfigEffectiveSource } from "../config/load-config.ts";
2
+ import type {
3
+ LoadSessionConfigOptions,
4
+ SessionConfigDiagnostics,
5
+ SessionConfigEffectiveSource,
6
+ SessionConfigEnvFileStatus,
7
+ SessionConfigEnvironmentStatus,
8
+ SessionConfigProjectStatus,
9
+ } from "../config/load-config.ts";
3
10
  import { loadSessionConfigWithDiagnostics } from "../config/load-config.ts";
4
11
  import type { CaptureConfig, ObservMeConfig } from "../config/schema.ts";
12
+ import {
13
+ describeGrafanaTransportSecurity,
14
+ describeOtlpTransportSecurity,
15
+ } from "../config/transport-security.ts";
5
16
  import { getGrafanaQueryReadiness } from "../query/grafana-readiness.ts";
6
17
  import { completeObsSubcommand, isExactObsSubcommandRequest } from "./obs-args.ts";
18
+ import { notifyObsCommand } from "./obs-command-support.ts";
7
19
  import { sanitizeObsDiagnosticText } from "./obs-diagnostics.ts";
8
20
 
9
21
  export interface ObsStatusCommandContext {
@@ -93,15 +105,15 @@ export async function handleObsStatusCommand(
93
105
  options: RegisterObsStatusCommandOptions = {},
94
106
  ): Promise<void> {
95
107
  if (!isObsStatusRequest(args)) {
96
- await notifyStatus(ctx, "Usage: /obs status", "warning");
108
+ await notifyObsCommand(ctx, "Usage: /obs status", "warning");
97
109
  return;
98
110
  }
99
111
 
100
112
  try {
101
113
  const snapshot = await resolveObsStatusSnapshot(ctx, options);
102
- await notifyStatus(ctx, renderObsStatus(snapshot), "info");
114
+ await notifyObsCommand(ctx, renderObsStatus(snapshot), "info");
103
115
  } catch (error) {
104
- await notifyStatus(ctx, `ObservMe status unavailable: ${sanitizeObsDiagnosticText(formatError(error))}`, "error");
116
+ await notifyObsCommand(ctx, `ObservMe status unavailable: ${sanitizeObsDiagnosticText(formatError(error))}`, "error");
105
117
  }
106
118
  }
107
119
 
@@ -129,8 +141,10 @@ export function renderObsStatus(snapshot: ObsStatusSnapshot): string {
129
141
  const lines = [
130
142
  `ObservMe: ${formatEnabled(config.enabled)}`,
131
143
  `OTLP endpoint: ${formatSafeConfiguredEndpoint(config.otlp.endpoint)}`,
144
+ `OTLP transport security: ${describeOtlpTransportSecurity(config)}`,
132
145
  ...formatConfigDiagnosticsLines(snapshot.configDiagnostics),
133
146
  `Grafana URL: ${formatSafeConfiguredUrl(config.query.grafana.url)}`,
147
+ `Grafana transport security: ${describeGrafanaTransportSecurity(config)}`,
134
148
  `Grafana query readiness: ${formatGrafanaQueryReadiness(config)}`,
135
149
  `Traces: ${formatEnabled(signalEnabled(config, config.traces.enabled))}`,
136
150
  `Metrics: ${formatEnabled(signalEnabled(config, config.metrics.enabled))}`,
@@ -237,10 +251,6 @@ function isObsStatusRequest(args: string): boolean {
237
251
  return isExactObsSubcommandRequest(args, OBS_STATUS_SUBCOMMAND, { allowEmpty: true });
238
252
  }
239
253
 
240
- async function notifyStatus(ctx: ObsStatusCommandContext, message: string, type: "info" | "warning" | "error"): Promise<void> {
241
- await ctx.ui?.notify?.(message, type);
242
- }
243
-
244
254
  function formatCaptureLines(capture: CaptureConfig): string[] {
245
255
  return [
246
256
  `Prompt capture: ${formatEnabled(capture.prompts)}`,
@@ -259,7 +269,10 @@ function formatConfigDiagnosticsLines(diagnostics: SessionConfigDiagnostics | un
259
269
 
260
270
  return [
261
271
  `Config source: ${formatConfigEffectiveSource(diagnostics.effectiveSource)}`,
262
- `Project config: ${formatProjectConfigStatus(diagnostics)}`,
272
+ ...formatGlobalConfigStatus(diagnostics),
273
+ `Project config: ${formatProjectConfigStatus(diagnostics.projectConfigStatus)}`,
274
+ ...formatEnvFileStatus(diagnostics.envFileStatus),
275
+ ...formatEnvironmentStatus(diagnostics.environmentStatus),
263
276
  ...formatConfigRejectionLines(diagnostics),
264
277
  ];
265
278
  }
@@ -281,13 +294,43 @@ function formatConfigEffectiveSource(source: SessionConfigEffectiveSource): stri
281
294
  return "defaults";
282
295
  }
283
296
 
284
- function formatProjectConfigStatus(diagnostics: SessionConfigDiagnostics): string {
285
- if (diagnostics.projectConfigStatus === "loaded") return "loaded (trusted .pi/observme.yaml)";
286
- if (diagnostics.projectConfigStatus === "skipped_untrusted") {
297
+ function formatGlobalConfigStatus(diagnostics: SessionConfigDiagnostics): string[] {
298
+ if (!diagnostics.globalConfigStatus) return [];
299
+ return [`Global config: ${formatFileSourceStatus(diagnostics.globalConfigStatus, "global config")}`];
300
+ }
301
+
302
+ function formatProjectConfigStatus(status: SessionConfigProjectStatus): string {
303
+ if (status === "loaded") return "loaded (trusted .pi/observme.yaml)";
304
+ if (status === "skipped_untrusted") {
287
305
  return "skipped (project is untrusted; safe defaults/global/env only)";
288
306
  }
307
+ if (status === "missing") return "missing (trusted project has no .pi/observme.yaml)";
308
+ return formatFileSourceStatus(status, "trusted .pi/observme.yaml");
309
+ }
310
+
311
+ function formatEnvFileStatus(status: SessionConfigEnvFileStatus | undefined): string[] {
312
+ if (!status) return [];
313
+ if (status === "skipped_untrusted") return ["Project .env: skipped (project is untrusted)"];
314
+ if (status === "skipped_disabled") return ["Project .env: skipped (loading is disabled)"];
315
+ return [`Project .env: ${formatFileSourceStatus(status, "trusted project .env")}`];
316
+ }
317
+
318
+ function formatEnvironmentStatus(status: SessionConfigEnvironmentStatus | undefined): string[] {
319
+ if (!status) return [];
320
+ if (status === "loaded") return ["Process environment: loaded"];
321
+ if (status === "rejected") return ["Process environment: rejected (malformed supported override)"];
322
+ return ["Process environment: no ObservMe values"];
323
+ }
289
324
 
290
- return "missing (trusted project has no .pi/observme.yaml)";
325
+ function formatFileSourceStatus(
326
+ status: Exclude<SessionConfigProjectStatus, "skipped_untrusted">,
327
+ label: string,
328
+ ): string {
329
+ if (status === "loaded") return "loaded";
330
+ if (status === "missing") return "missing";
331
+ if (status === "malformed") return `ignored (${label} is malformed)`;
332
+ if (status === "unreadable") return `ignored (${label} is unreadable)`;
333
+ return `ignored (${label} was structurally rejected)`;
291
334
  }
292
335
 
293
336
  function formatGrafanaQueryReadiness(config: ObservMeConfig): string {
@@ -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, isExactObsSubcommandRequest } from "./obs-args.ts";
7
12
  import { loadObsCommandConfig, notifyObsCommand } from "./obs-command-support.ts";
8
13
  import { appendObsRecoveryHint, formatObsCommandFailure } from "./obs-diagnostics.ts";
@@ -126,22 +131,29 @@ export async function getObsToolsSnapshot(
126
131
  export function renderObsTools(snapshot: ObsToolsSnapshot): string {
127
132
  const calls = snapshot.calls.map(normalizeObsToolCallRow).filter(isObsToolCallRow);
128
133
  const failures = snapshot.failures.map(normalizeObsToolFailureRow).filter(isObsToolFailureRow);
129
- const lines = [`Tool calls by tool (last ${snapshot.window})`];
134
+ const callSelection = selectObsCommandRows(calls);
135
+ const failureSelection = selectObsCommandRows(failures);
136
+ const window = normalizeObsBackendLabel(snapshot.window) ?? OBS_TOOLS_WINDOW;
137
+ const lines = [`Tool calls by tool (last ${window})`];
130
138
 
131
139
  if (calls.length === 0) {
132
140
  lines.push(appendObsRecoveryHint("No tool call metrics found.", OBS_TOOLS_NO_CALLS_NEXT_ACTION));
133
141
  } else {
134
- lines.push(...calls.map(renderObsToolCallRow));
142
+ lines.push(...callSelection.rows.map(renderObsToolCallRow));
143
+ if (callSelection.omittedCount > 0) lines.push(`… ${callSelection.omittedCount} tool call row(s) omitted`);
135
144
  }
136
145
 
137
- lines.push(`Tool failures by tool/error (last ${snapshot.window})`);
146
+ lines.push(`Tool failures by tool/error (last ${window})`);
138
147
  if (failures.length === 0) {
139
148
  lines.push(appendObsRecoveryHint("No tool failure metrics found.", OBS_TOOLS_NO_FAILURES_NEXT_ACTION));
140
149
  } else {
141
- lines.push(...failures.map(renderObsToolFailureRow));
150
+ lines.push(...failureSelection.rows.map(renderObsToolFailureRow));
151
+ if (failureSelection.omittedCount > 0) {
152
+ lines.push(`… ${failureSelection.omittedCount} tool failure row(s) omitted`);
153
+ }
142
154
  }
143
155
 
144
- return lines.join("\n");
156
+ return boundObsCommandOutput(lines.join("\n"));
145
157
  }
146
158
 
147
159
  class ObsToolsCommand {
@@ -231,12 +243,11 @@ function parseRatePerSecond(value: string | number | undefined): number | undefi
231
243
  }
232
244
 
233
245
  function normalizeMetricLabel(value: string | undefined): string {
234
- return normalizeOptionalString(value) ?? "unknown";
246
+ return normalizeObsBackendLabel(value) ?? "unknown";
235
247
  }
236
248
 
237
249
  function normalizeOptionalString(value: string | undefined): string | undefined {
238
- const trimmed = value?.trim();
239
- return trimmed || undefined;
250
+ return normalizeObsBackendLabel(value);
240
251
  }
241
252
 
242
253
  function renderObsToolCallRow(row: ObsToolCallRow): string {
@@ -92,6 +92,8 @@ const OBS_COMMAND_NAME = "obs";
92
92
  const OBS_TRACE_SUBCOMMAND = "trace";
93
93
  const OBS_TRACE_USAGE = "Usage: /obs trace [--last-turn|--session <session-id>]";
94
94
  const OBS_TRACE_TEMPO_ERROR_NEXT_ACTION = "run /obs health and verify query.grafana.url, Grafana credentials, and the Tempo datasource UID.";
95
+ const OBS_TRACE_LINK_CONFIG_ERROR_NEXT_ACTION =
96
+ "set query.links.traceUrlTemplate to a supported trace-id placeholder or the ellipsis fallback, then verify query.grafana.url.";
95
97
  const OBS_TRACE_SESSION_ERROR_NEXT_ACTION = "wait for a Pi turn or query a generated session id with /obs trace --session <session-id>.";
96
98
  const OBS_TRACE_NOT_FOUND_NEXT_ACTION = "check the session id, wait for trace export, then verify Tempo datasource with /obs health.";
97
99
  const OBS_TRACE_ACTIVE_SESSION_NOTE = "Trace visibility: active sessions may show ended child spans before the root pi.session span; the root is exported after session_shutdown.";
@@ -398,6 +400,9 @@ export function resolveObsTraceDiagnostic(error: unknown): ObsCommandRecoveryHin
398
400
  const message = readObsDiagnosticMessage(error);
399
401
 
400
402
  if (isObsTraceSessionErrorMessage(message)) return { subsystem: "Session trace", nextAction: OBS_TRACE_SESSION_ERROR_NEXT_ACTION };
403
+ if (isObsTraceLinkConfigErrorMessage(message)) {
404
+ return { subsystem: "Grafana trace link", nextAction: OBS_TRACE_LINK_CONFIG_ERROR_NEXT_ACTION };
405
+ }
401
406
  if (isObsTraceNotFoundMessage(message)) return { subsystem: "Tempo", nextAction: OBS_TRACE_NOT_FOUND_NEXT_ACTION };
402
407
  return { subsystem: "Tempo", nextAction: OBS_TRACE_TEMPO_ERROR_NEXT_ACTION };
403
408
  }
@@ -406,6 +411,10 @@ function isObsTraceSessionErrorMessage(message: string): boolean {
406
411
  return /No current ObservMe session trace|No last-turn ObservMe trace|Unsafe ObservMe session id/u.test(message);
407
412
  }
408
413
 
414
+ function isObsTraceLinkConfigErrorMessage(message: string): boolean {
415
+ return /Grafana trace link configuration is invalid/u.test(message);
416
+ }
417
+
409
418
  function isObsTraceNotFoundMessage(message: string): boolean {
410
419
  return /No trace was found/u.test(message);
411
420
  }
@@ -22,6 +22,7 @@ import { handleObsToolsCommand } from "./obs-tools.ts";
22
22
  import type { ObsTraceCommandContext, RegisterObsTraceCommandOptions } from "./obs-trace.ts";
23
23
  import { handleObsTraceCommand } from "./obs-trace.ts";
24
24
  import { completeObsSubcommands, firstObsCommandToken, obsUsageWithError } from "./obs-args.ts";
25
+ import { notifyObsCommand } from "./obs-command-support.ts";
25
26
 
26
27
  export interface ObsCommandContext
27
28
  extends ObsStatusCommandContext,
@@ -153,7 +154,11 @@ export async function handleObsCommand(
153
154
  return;
154
155
  }
155
156
 
156
- await ctx.ui?.notify?.(obsUsageWithError(OBS_ROOT_USAGE, subcommand ? `Unknown subcommand: ${subcommand}.` : undefined), "warning");
157
+ await notifyObsCommand(
158
+ ctx,
159
+ obsUsageWithError(OBS_ROOT_USAGE, subcommand ? `Unknown subcommand: ${subcommand}.` : undefined),
160
+ "warning",
161
+ );
157
162
  }
158
163
 
159
164
  export function getObsRootCommandArgumentCompletions(prefix: string): Array<{ value: string; label: string }> | null {
@@ -1,9 +1,18 @@
1
1
  import { CONFIG_DIR_NAME, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
2
- import { mkdir, writeFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
2
+ import type {
3
+ ExtensionContext,
4
+ SessionStartEvent,
5
+ } from "@earendil-works/pi-coding-agent";
4
6
  import { readDiagnosticMessage, sanitizeDiagnosticText } from "../diagnostics/sanitize.ts";
5
7
  import { RESOURCE_ATTRIBUTES } from "../semconv/attributes.ts";
6
- import { resolveProjectLocalFilePath } from "./project-paths.ts";
8
+ import {
9
+ createCanonicalProjectLocalFileExclusively,
10
+ resolveProjectLocalFilePath,
11
+ } from "./project-paths.ts";
12
+ import type {
13
+ ProjectLocalFileOperationHooks,
14
+ ProjectLocalFilePathOptions,
15
+ } from "./project-paths.ts";
7
16
 
8
17
  export type ProjectConfigBootstrapStatus = "created" | "exists" | "skipped_untrusted";
9
18
  export type ProjectConfigNotifyLevel = "info" | "warning" | "error";
@@ -30,14 +39,15 @@ export interface EnsureProjectConfigOptions {
30
39
  readonly configDirName?: string;
31
40
  readonly cwd?: string;
32
41
  readonly isProjectTrusted?: boolean | (() => boolean | Promise<boolean>);
42
+ readonly projectFileOperationHooks?: ProjectLocalFileOperationHooks;
33
43
  }
34
44
 
35
45
  export type EnsureProjectConfig = (options: EnsureProjectConfigOptions) => Promise<ProjectConfigBootstrapResult>;
36
46
 
37
- type ProjectConfigBootstrapHandler = (event: unknown, ctx: ProjectConfigBootstrapContext) => Promise<void> | void;
47
+ type ProjectConfigBootstrapHandler = (event: SessionStartEvent, ctx: ExtensionContext) => Promise<void> | void;
38
48
 
39
49
  interface ProjectConfigBootstrapPiApi {
40
- on: (eventName: string, handler: ProjectConfigBootstrapHandler) => void;
50
+ on: (eventName: "session_start", handler: ProjectConfigBootstrapHandler) => void;
41
51
  }
42
52
 
43
53
  const observmeYamlFileName = "observme.yaml";
@@ -46,7 +56,6 @@ export const PROJECT_OBSERVME_YAML_TEMPLATE = `observme:
46
56
  enabled: true
47
57
  environment: development
48
58
  tenant: local-dev
49
- replayOnStart: false
50
59
 
51
60
  otlp:
52
61
  endpoint: http://localhost:4318
@@ -54,7 +63,6 @@ export const PROJECT_OBSERVME_YAML_TEMPLATE = `observme:
54
63
  timeoutMs: 3000
55
64
  headers: {}
56
65
  tls:
57
- enabled: false
58
66
  insecureSkipVerify: false
59
67
  signalEndpoints:
60
68
  traces: http://localhost:4318/v1/traces
@@ -86,6 +94,8 @@ export const PROJECT_OBSERVME_YAML_TEMPLATE = `observme:
86
94
  propagateTraceContext: true
87
95
  propagateToSubagents: true
88
96
  capabilityEnv: OBSERVME_AGENT_CAPABILITY
97
+ # Opt in to one validated, branch-local custom entry for reload/resume recovery.
98
+ # This entry never participates in LLM context.
89
99
  writeCorrelationEntry: false
90
100
 
91
101
  traces:
@@ -101,6 +111,7 @@ export const PROJECT_OBSERVME_YAML_TEMPLATE = `observme:
101
111
  enabled: true
102
112
  exportIntervalMillis: 15000
103
113
  exportTimeoutMillis: 3000
114
+ activeAgentLeaseDurationMillis: 60000
104
115
 
105
116
  logs:
106
117
  enabled: true
@@ -191,11 +202,16 @@ export function registerProjectConfigBootstrap(
191
202
  export async function ensureProjectObservMeConfig(
192
203
  options: EnsureProjectConfigOptions = {},
193
204
  ): Promise<ProjectConfigBootstrapResult> {
194
- const configPath = resolveProjectObservMeConfigPath(options);
205
+ const pathOptions = createProjectObservMeConfigPathOptions(options);
206
+ const configPath = resolveProjectLocalFilePath(pathOptions);
195
207
  const projectTrusted = await resolveBootstrapProjectTrust(options.isProjectTrusted);
196
208
 
197
209
  if (!projectTrusted) return { path: configPath, status: "skipped_untrusted" };
198
- return createProjectObservMeConfigFile(configPath);
210
+ return createProjectObservMeConfigFile(
211
+ configPath,
212
+ pathOptions,
213
+ options.projectFileOperationHooks,
214
+ );
199
215
  }
200
216
 
201
217
  function createProjectConfigBootstrapHandler(
@@ -253,30 +269,40 @@ async function notifyProjectConfigBootstrapFailed(ctx: ProjectConfigBootstrapCon
253
269
  await ctx.ui?.notify?.(`ObservMe could not create the project config file: ${formatError(error)}`, "warning");
254
270
  }
255
271
 
256
- async function createProjectObservMeConfigFile(configPath: string): Promise<ProjectConfigBootstrapResult> {
257
- return withFileMutationQueue(configPath, createProjectObservMeConfigFileInQueue.bind(undefined, configPath));
272
+ async function createProjectObservMeConfigFile(
273
+ configPath: string,
274
+ pathOptions: ProjectLocalFilePathOptions,
275
+ hooks: ProjectLocalFileOperationHooks | undefined,
276
+ ): Promise<ProjectConfigBootstrapResult> {
277
+ return withFileMutationQueue(
278
+ configPath,
279
+ createProjectObservMeConfigFileInQueue.bind(undefined, configPath, pathOptions, hooks),
280
+ );
258
281
  }
259
282
 
260
- async function createProjectObservMeConfigFileInQueue(configPath: string): Promise<ProjectConfigBootstrapResult> {
261
- await mkdir(dirname(configPath), { recursive: true });
262
-
263
- try {
264
- await writeFile(configPath, PROJECT_OBSERVME_YAML_TEMPLATE, { encoding: "utf8", flag: "wx" });
265
- return { path: configPath, status: "created" };
266
- } catch (error) {
267
- if (isFileAlreadyExistsError(error)) return { path: configPath, status: "exists" };
268
- throw error;
269
- }
283
+ async function createProjectObservMeConfigFileInQueue(
284
+ configPath: string,
285
+ pathOptions: ProjectLocalFilePathOptions,
286
+ hooks: ProjectLocalFileOperationHooks | undefined,
287
+ ): Promise<ProjectConfigBootstrapResult> {
288
+ const status = await createCanonicalProjectLocalFileExclusively(
289
+ pathOptions,
290
+ PROJECT_OBSERVME_YAML_TEMPLATE,
291
+ hooks,
292
+ );
293
+ return { path: configPath, status };
270
294
  }
271
295
 
272
- function resolveProjectObservMeConfigPath(options: EnsureProjectConfigOptions): string {
273
- return resolveProjectLocalFilePath({
296
+ function createProjectObservMeConfigPathOptions(
297
+ options: EnsureProjectConfigOptions,
298
+ ): ProjectLocalFilePathOptions {
299
+ return {
274
300
  cwd: options.cwd,
275
301
  configDirName: options.configDirName,
276
302
  defaultConfigDirName: CONFIG_DIR_NAME,
277
303
  fileName: observmeYamlFileName,
278
304
  inputLabel: "project config path",
279
- });
305
+ };
280
306
  }
281
307
 
282
308
  async function resolveBootstrapProjectTrust(
@@ -287,14 +313,6 @@ async function resolveBootstrapProjectTrust(
287
313
  return false;
288
314
  }
289
315
 
290
- function isFileAlreadyExistsError(error: unknown): boolean {
291
- return isErrorWithCode(error) && error.code === "EEXIST";
292
- }
293
-
294
- function isErrorWithCode(error: unknown): error is NodeJS.ErrnoException {
295
- return typeof error === "object" && error !== null && "code" in error;
296
- }
297
-
298
316
  function formatError(error: unknown): string {
299
317
  return sanitizeDiagnosticText(readDiagnosticMessage(error));
300
318
  }
@@ -5,7 +5,6 @@ export const defaultObservMeConfig = {
5
5
  enabled: true,
6
6
  environment: "production",
7
7
  tenant: "platform",
8
- replayOnStart: false,
9
8
  otlp: {
10
9
  endpoint: "https://otel-collector.example.com:4318",
11
10
  protocol: "http/protobuf",
@@ -14,7 +13,6 @@ export const defaultObservMeConfig = {
14
13
  Authorization: "Bearer ${OBSERVME_OTLP_TOKEN}",
15
14
  },
16
15
  tls: {
17
- enabled: true,
18
16
  insecureSkipVerify: false,
19
17
  },
20
18
  },
@@ -60,6 +58,7 @@ export const defaultObservMeConfig = {
60
58
  enabled: true,
61
59
  exportIntervalMillis: 15000,
62
60
  exportTimeoutMillis: 3000,
61
+ activeAgentLeaseDurationMillis: 60000,
63
62
  },
64
63
  logs: {
65
64
  enabled: true,