@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.
- package/.env.example +4 -0
- package/CHANGELOG.md +48 -0
- package/README.md +36 -11
- package/dashboards/observme-agent-node-graphs.json +5 -5
- package/dashboards/observme-agents.json +169 -4
- package/dashboards/observme-alerts.yaml +16 -3
- package/dashboards/observme-overview.json +6 -6
- package/dashboards/observme-trace-journey.json +4 -4
- package/docs/agent-subagent-observability-requirements.md +19 -10
- package/docs/compatibility-matrix.md +21 -4
- package/docs/configuration.md +7 -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 +39 -1
- package/docs/reference/05-otel-pipeline-and-collector.md +23 -10
- package/docs/reference/06-security-privacy-redaction.md +13 -6
- package/docs/reference/08-query-grafana-integration.md +30 -7
- package/docs/reference/09-dashboards-alerts-slos.md +132 -3
- package/docs/reference/10-testing-release-operations.md +44 -14
- package/docs/reference/11-deployment-runbooks.md +82 -5
- package/docs/reference/12-configuration-reference.md +48 -5
- package/docs/review-validation.md +17 -0
- package/examples/README.md +7 -2
- package/examples/collector.yaml +8 -8
- package/examples/integrations/subagent-runner.ts +8 -5
- package/examples/observme.yaml +3 -2
- package/package.json +14 -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 +50 -32
- package/src/config/defaults.ts +1 -2
- package/src/config/load-config.ts +270 -81
- package/src/config/project-paths.ts +318 -6
- package/src/config/query-limits.ts +10 -0
- package/src/config/schema.ts +18 -8
- package/src/config/transport-security.ts +107 -0
- package/src/config/validate.ts +88 -12
- 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/active-agent-lease.ts +101 -0
- 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 +312 -106
- 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 +159 -54
- package/src/pi/handler-types.ts +40 -17
- 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 +7 -0
- package/src/semconv/values.ts +2 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { open } from "node:fs/promises";
|
|
2
2
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
SessionShutdownEvent,
|
|
6
|
+
SessionStartEvent,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3
8
|
import {
|
|
4
9
|
clearObsAgentsRuntimeState,
|
|
5
10
|
startObsAgentsRuntimeState,
|
|
@@ -25,10 +30,10 @@ import {
|
|
|
25
30
|
import type { ObservMeConfig } from "../../config/schema.ts";
|
|
26
31
|
import { emitUnsafeCaptureWarning, normalizeConfigRejectionDiagnostic } from "../../config/validate.ts";
|
|
27
32
|
import { EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE } from "../../constants.ts";
|
|
28
|
-
import
|
|
33
|
+
import { ObservMeOtelStartupError } from "../../otel/sdk.ts";
|
|
34
|
+
import type { BoundedOtelOperationResult, OtelOperationSettlement } from "../../otel/shutdown.ts";
|
|
29
35
|
import {
|
|
30
36
|
AGENT_LINEAGE_ATTRIBUTES,
|
|
31
|
-
COMMON_SPAN_ATTRIBUTES,
|
|
32
37
|
CONFIG_ATTRIBUTES,
|
|
33
38
|
LOG_ATTRIBUTES,
|
|
34
39
|
SESSION_ATTRIBUTES,
|
|
@@ -55,7 +60,6 @@ import {
|
|
|
55
60
|
readSpanId,
|
|
56
61
|
readSpanTraceId,
|
|
57
62
|
readString,
|
|
58
|
-
readUnknown,
|
|
59
63
|
resolveModelId,
|
|
60
64
|
resolveModelProvider,
|
|
61
65
|
resolveSessionFilePath,
|
|
@@ -71,15 +75,19 @@ import {
|
|
|
71
75
|
startSessionTelemetry,
|
|
72
76
|
workflowFailed,
|
|
73
77
|
} from "../handler-runtime.ts";
|
|
78
|
+
import {
|
|
79
|
+
appendSessionCorrelationEntry,
|
|
80
|
+
readLatestSessionCorrelation,
|
|
81
|
+
} from "../session-correlation.ts";
|
|
74
82
|
import type { HandlerRegistrar, SerializedLifecycleQueue } from "../handler-runtime.ts";
|
|
75
83
|
import type {
|
|
76
84
|
AttributeMap,
|
|
77
|
-
Handler,
|
|
78
85
|
HandlerSessionState,
|
|
79
86
|
LoadSessionConfig,
|
|
80
87
|
MinimalSessionCorrelation,
|
|
81
88
|
ObservMeHandlerContext,
|
|
82
89
|
ObservMeTelemetrySession,
|
|
90
|
+
PiHandler,
|
|
83
91
|
RegisterHandlersOptions,
|
|
84
92
|
SessionConfigLoadResult,
|
|
85
93
|
SessionRecoveryHeader,
|
|
@@ -108,23 +116,33 @@ export function buildSessionAttributes(
|
|
|
108
116
|
config: ObservMeConfig,
|
|
109
117
|
lineage: ObservMeTelemetrySession["lineage"],
|
|
110
118
|
recovery?: StartupRecoveryState,
|
|
119
|
+
initialThinkingLevel?: unknown,
|
|
111
120
|
): AttributeMap {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
121
|
+
const sessionManager = ctx.sessionManager;
|
|
122
|
+
const managerHeader = normalizeSessionHeader(sessionManager?.getHeader());
|
|
123
|
+
const header = managerHeader ?? recovery?.header;
|
|
124
|
+
const cwd = sessionManager?.getCwd() ?? header?.cwd ?? ctx.cwd ?? process.cwd();
|
|
125
|
+
const sessionId = sessionManager?.getSessionId() ?? header?.id ?? resolveSessionId(event, ctx, lineage);
|
|
126
|
+
const parentSessionId = header?.parentSession ?? lineage.parentSessionId;
|
|
127
|
+
const sessionFile = sessionManager?.getSessionFile() ?? recovery?.sessionFile ?? resolveSessionFilePath(event, ctx);
|
|
128
|
+
const sessionName = sessionManager
|
|
129
|
+
? sessionManager.getSessionName() ?? "unknown"
|
|
130
|
+
: readString(event, "sessionName") ?? readString(event, "name") ?? "unknown";
|
|
131
|
+
const persisted = sessionManager
|
|
132
|
+
? sessionFile !== undefined
|
|
133
|
+
: readBoolean(event, "persisted") ?? recovery?.resumed ?? false;
|
|
116
134
|
|
|
117
135
|
return withoutUndefinedAttributes({
|
|
118
136
|
[SESSION_ATTRIBUTES.PI_SESSION_ID]: sessionId,
|
|
119
|
-
[SESSION_ATTRIBUTES.PI_SESSION_NAME]:
|
|
137
|
+
[SESSION_ATTRIBUTES.PI_SESSION_NAME]: sessionName,
|
|
120
138
|
[SESSION_ATTRIBUTES.PI_SESSION_CWD_HASH]: hashValue(cwd, config),
|
|
121
139
|
[SESSION_ATTRIBUTES.PI_SESSION_PARENT_SESSION_HASH]: parentSessionId ? hashValue(parentSessionId, config) : "",
|
|
122
|
-
[SESSION_ATTRIBUTES.PI_SESSION_PERSISTED]:
|
|
140
|
+
[SESSION_ATTRIBUTES.PI_SESSION_PERSISTED]: persisted,
|
|
123
141
|
[SESSION_ATTRIBUTES.PI_SESSION_FILE_HASH]: sessionFile ? hashValue(sessionFile, config) : "",
|
|
124
|
-
[SESSION_ATTRIBUTES.PI_SESSION_VERSION]: readString(
|
|
125
|
-
[SESSION_ATTRIBUTES.PI_MODEL_PROVIDER_CURRENT]: resolveModelProvider(
|
|
126
|
-
[SESSION_ATTRIBUTES.PI_MODEL_ID_CURRENT]: resolveModelId(
|
|
127
|
-
[SESSION_ATTRIBUTES.PI_THINKING_LEVEL_CURRENT]: resolveThinkingLevel(
|
|
142
|
+
[SESSION_ATTRIBUTES.PI_SESSION_VERSION]: readString(header, "version") ?? "unknown",
|
|
143
|
+
[SESSION_ATTRIBUTES.PI_MODEL_PROVIDER_CURRENT]: resolveModelProvider(ctx),
|
|
144
|
+
[SESSION_ATTRIBUTES.PI_MODEL_ID_CURRENT]: resolveModelId(ctx),
|
|
145
|
+
[SESSION_ATTRIBUTES.PI_THINKING_LEVEL_CURRENT]: resolveThinkingLevel(initialThinkingLevel),
|
|
128
146
|
...buildCommonSessionSpanAttributes(sessionId, config, lineage),
|
|
129
147
|
});
|
|
130
148
|
}
|
|
@@ -153,7 +171,7 @@ function createSessionStartHandler(
|
|
|
153
171
|
loadConfigFn: LoadSessionConfig,
|
|
154
172
|
startTelemetryFn: StartSessionTelemetry,
|
|
155
173
|
options: RegisterHandlersOptions,
|
|
156
|
-
):
|
|
174
|
+
): PiHandler<"session_start"> {
|
|
157
175
|
return handleSessionStart.bind(undefined, state, loadConfigFn, startTelemetryFn, options);
|
|
158
176
|
}
|
|
159
177
|
|
|
@@ -162,19 +180,29 @@ async function handleSessionStart(
|
|
|
162
180
|
loadConfigFn: LoadSessionConfig,
|
|
163
181
|
startTelemetryFn: StartSessionTelemetry,
|
|
164
182
|
options: RegisterHandlersOptions,
|
|
165
|
-
event:
|
|
166
|
-
ctx:
|
|
183
|
+
event: SessionStartEvent,
|
|
184
|
+
ctx: ExtensionContext,
|
|
167
185
|
): Promise<void> {
|
|
186
|
+
if (!(await resolvePendingTelemetryCleanupBeforeStart(state, ctx))) return;
|
|
187
|
+
|
|
168
188
|
const previousSession = state.session;
|
|
169
|
-
if (previousSession
|
|
189
|
+
if (previousSession && !(await shutDownPreviousSessionBeforeDuplicateStart(previousSession, ctx, state))) return;
|
|
170
190
|
|
|
171
191
|
await ensureProjectConfigForHandler(options, ctx);
|
|
172
192
|
const loadedConfig = await loadSessionConfigForHandler(loadConfigFn, options, ctx);
|
|
173
193
|
const config = loadedConfig.config;
|
|
174
|
-
|
|
194
|
+
updateObsStatusRuntimeState({ config, configDiagnostics: loadedConfig.diagnostics });
|
|
195
|
+
clearObsStatusExportError();
|
|
196
|
+
|
|
197
|
+
if (!config.enabled) {
|
|
198
|
+
clearDisabledTelemetryRuntimeState(state);
|
|
199
|
+
await clearExtensionStatus(ctx);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
175
202
|
|
|
203
|
+
await emitUnsafeCaptureWarning(config, ctx);
|
|
176
204
|
const recovery = await resolveStartupRecovery(event, ctx, config, options);
|
|
177
|
-
const recoveryCorrelation = recovery.customCorrelation
|
|
205
|
+
const recoveryCorrelation = recovery.customCorrelation;
|
|
178
206
|
const lineage = createAgentLineageContext({
|
|
179
207
|
config,
|
|
180
208
|
env: buildRecoveryLineageEnv(config, recoveryCorrelation, options.env),
|
|
@@ -183,55 +211,78 @@ async function handleSessionStart(
|
|
|
183
211
|
options.requireCompleteParentEnvelope ?? (options.trustedParentContext === true && recoveryCorrelation === undefined),
|
|
184
212
|
failOpenInvalidPropagation: true,
|
|
185
213
|
});
|
|
186
|
-
|
|
214
|
+
let session: ObservMeTelemetrySession;
|
|
215
|
+
try {
|
|
216
|
+
session = await startTelemetryFn({
|
|
217
|
+
config,
|
|
218
|
+
lineage,
|
|
219
|
+
now: options.now,
|
|
220
|
+
wallClockNow: options.wallClockNow,
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
await handleTelemetryStartupFailure(state, ctx, error);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
187
227
|
session.now = options.now ?? session.now ?? monotonicNowMs;
|
|
188
|
-
updateObsStatusRuntimeState({ config: session.config, configDiagnostics: loadedConfig.diagnostics });
|
|
189
|
-
clearObsStatusExportError();
|
|
190
228
|
state.session = session;
|
|
191
|
-
const attributes = buildSessionAttributes(event, ctx, session.config, lineage, recovery);
|
|
192
|
-
const labels = metricLabels(session.config, lineage);
|
|
193
|
-
|
|
194
|
-
session.sessionAttributes = attributes;
|
|
195
|
-
const traceParent = resolveSessionTraceParent(lineage);
|
|
196
|
-
session.sessionSpan = startActiveRootSpan(session, SPAN_NAMES.PI_SESSION, attributes, "session", traceParent);
|
|
197
|
-
emitConfigRejectionDiagnostic(session, loadedConfig.diagnostics, ctx);
|
|
198
|
-
recordSessionTracePropagationFailure(session, traceParent);
|
|
199
|
-
startObsSessionRuntimeState({
|
|
200
|
-
sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
|
|
201
|
-
traceId: readSpanTraceId(session.sessionSpan),
|
|
202
|
-
traceUrlTemplate: session.config.query.links.traceUrlTemplate,
|
|
203
|
-
});
|
|
204
|
-
startObsAgentsRuntimeState({
|
|
205
|
-
lineage,
|
|
206
|
-
agentTree: session.agentTree,
|
|
207
|
-
sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
|
|
208
|
-
traceId: readSpanTraceId(session.sessionSpan),
|
|
209
|
-
});
|
|
210
|
-
session.workflowStartedAtMs = session.now();
|
|
211
|
-
session.metrics.sessionsStarted.add(1, labels);
|
|
212
|
-
session.metrics.activeAgents.add(1, labels);
|
|
213
|
-
session.activeAgentRecorded = true;
|
|
214
|
-
session.sessionSpan.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, attributes);
|
|
215
|
-
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, attributes);
|
|
216
|
-
if (recovery.resumed && session.config.replayOnStart) emitStartupReplayTelemetry(session, attributes);
|
|
217
229
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
try {
|
|
231
|
+
const attributes = buildSessionAttributes(
|
|
232
|
+
event,
|
|
233
|
+
ctx,
|
|
234
|
+
session.config,
|
|
235
|
+
lineage,
|
|
236
|
+
recovery,
|
|
237
|
+
options.getThinkingLevel?.(),
|
|
238
|
+
);
|
|
239
|
+
const labels = metricLabels(session.config, lineage);
|
|
240
|
+
|
|
241
|
+
session.sessionAttributes = attributes;
|
|
242
|
+
const traceParent = resolveSessionTraceParent(lineage);
|
|
243
|
+
session.sessionSpan = startActiveRootSpan(session, SPAN_NAMES.PI_SESSION, attributes, "session", traceParent);
|
|
244
|
+
emitConfigRejectionDiagnostic(session, loadedConfig.diagnostics, ctx);
|
|
245
|
+
recordSessionTracePropagationFailure(session, traceParent);
|
|
246
|
+
startObsSessionRuntimeState({
|
|
247
|
+
sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
|
|
248
|
+
traceId: readSpanTraceId(session.sessionSpan),
|
|
249
|
+
config: session.config,
|
|
250
|
+
});
|
|
251
|
+
startObsAgentsRuntimeState({
|
|
252
|
+
lineage,
|
|
253
|
+
agentTree: session.agentTree,
|
|
254
|
+
sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
|
|
255
|
+
traceId: readSpanTraceId(session.sessionSpan),
|
|
256
|
+
});
|
|
257
|
+
session.workflowStartedAtMs = session.now();
|
|
258
|
+
session.metrics.sessionsStarted.add(1, labels);
|
|
259
|
+
session.sessionSpan.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, attributes);
|
|
260
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, attributes);
|
|
261
|
+
|
|
262
|
+
if (isRootWorkflow(lineage)) {
|
|
263
|
+
session.metrics.workflowsStarted.add(1, labels);
|
|
264
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.WORKFLOW_STARTED, attributes);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE);
|
|
268
|
+
activateSessionActiveAgent(session, labels);
|
|
269
|
+
if (session.config.agent.writeCorrelationEntry) {
|
|
270
|
+
appendSessionCorrelationEntry(options.appendEntry, lineage, recovery.customCorrelation);
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
await cleanUpFailedSessionStart(session, ctx, state);
|
|
274
|
+
throw error;
|
|
221
275
|
}
|
|
222
|
-
|
|
223
|
-
await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE);
|
|
224
|
-
state.session = session;
|
|
225
276
|
}
|
|
226
277
|
|
|
227
|
-
function createSessionShutdownHandler(state: HandlerSessionState):
|
|
278
|
+
function createSessionShutdownHandler(state: HandlerSessionState): PiHandler<"session_shutdown"> {
|
|
228
279
|
return handleSessionShutdown.bind(undefined, state);
|
|
229
280
|
}
|
|
230
281
|
|
|
231
282
|
async function handleSessionShutdown(
|
|
232
283
|
state: HandlerSessionState,
|
|
233
|
-
event:
|
|
234
|
-
ctx:
|
|
284
|
+
event: SessionShutdownEvent,
|
|
285
|
+
ctx: ExtensionContext,
|
|
235
286
|
): Promise<void> {
|
|
236
287
|
const session = state.session;
|
|
237
288
|
if (!session) return;
|
|
@@ -247,8 +298,13 @@ async function resolveStartupRecovery(
|
|
|
247
298
|
): Promise<StartupRecoveryState> {
|
|
248
299
|
const sessionFile = resolveSessionFilePath(event, ctx);
|
|
249
300
|
const readHeader = options.readSessionHeader ?? readSessionHeaderFromFile;
|
|
250
|
-
|
|
251
|
-
|
|
301
|
+
let header: SessionRecoveryHeader | undefined;
|
|
302
|
+
if (ctx.sessionManager) {
|
|
303
|
+
header = normalizeSessionHeader(ctx.sessionManager.getHeader());
|
|
304
|
+
} else if (sessionFile) {
|
|
305
|
+
header = await readHeader(sessionFile);
|
|
306
|
+
}
|
|
307
|
+
const customCorrelation = config.agent.writeCorrelationEntry ? readActiveBranchCorrelation(ctx) : undefined;
|
|
252
308
|
|
|
253
309
|
return {
|
|
254
310
|
resumed: isExistingSessionStart(event),
|
|
@@ -258,16 +314,6 @@ async function resolveStartupRecovery(
|
|
|
258
314
|
};
|
|
259
315
|
}
|
|
260
316
|
|
|
261
|
-
function emitStartupReplayTelemetry(session: ObservMeTelemetrySession, attributes: AttributeMap): void {
|
|
262
|
-
const replayAttributes = {
|
|
263
|
-
...attributes,
|
|
264
|
-
[COMMON_SPAN_ATTRIBUTES.OBSERVME_REPLAYED]: true,
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
session.sessionSpan?.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, replayAttributes);
|
|
268
|
-
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, replayAttributes);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
317
|
function buildRecoveryLineageEnv(
|
|
272
318
|
config: ObservMeConfig,
|
|
273
319
|
correlation: MinimalSessionCorrelation | undefined,
|
|
@@ -282,19 +328,28 @@ function buildRecoveryLineageEnv(
|
|
|
282
328
|
...definedEnvValue(config.agent.parentIdEnv, correlation.parentAgentId),
|
|
283
329
|
...definedEnvValue(config.agent.rootIdEnv, correlation.rootAgentId),
|
|
284
330
|
...definedEnvValue(config.agent.parentSessionIdEnv, correlation.parentSessionId),
|
|
285
|
-
...definedEnvValue(config.agent.depthEnv,
|
|
331
|
+
...definedEnvValue(config.agent.depthEnv, recoveryPropagationDepth(correlation)),
|
|
286
332
|
...definedEnvValue(config.agent.spawnIdEnv, correlation.spawnId),
|
|
287
333
|
...definedEnvValue(config.agent.capabilityEnv, correlation.capability),
|
|
288
334
|
};
|
|
289
335
|
}
|
|
290
336
|
|
|
337
|
+
function recoveryPropagationDepth(correlation: MinimalSessionCorrelation): string | undefined {
|
|
338
|
+
if (correlation.depth === undefined) return undefined;
|
|
339
|
+
const parentDepth = correlation.parentAgentId ? Math.max(0, correlation.depth - 1) : correlation.depth;
|
|
340
|
+
return String(parentDepth);
|
|
341
|
+
}
|
|
342
|
+
|
|
291
343
|
function definedEnvValue(name: string, value: string | undefined): NodeJS.ProcessEnv {
|
|
292
344
|
return value === undefined || value === "" ? {} : { [name]: value };
|
|
293
345
|
}
|
|
294
346
|
|
|
295
|
-
function
|
|
296
|
-
|
|
297
|
-
|
|
347
|
+
function readActiveBranchCorrelation(ctx: ObservMeHandlerContext): StartupRecoveryState["customCorrelation"] {
|
|
348
|
+
try {
|
|
349
|
+
return readLatestSessionCorrelation(ctx.sessionManager?.getBranch());
|
|
350
|
+
} catch {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
298
353
|
}
|
|
299
354
|
|
|
300
355
|
function normalizeSessionHeader(value: unknown): SessionRecoveryHeader | undefined {
|
|
@@ -307,27 +362,9 @@ function normalizeSessionHeader(value: unknown): SessionRecoveryHeader | undefin
|
|
|
307
362
|
timestamp: readString(value, "timestamp"),
|
|
308
363
|
cwd: readString(value, "cwd"),
|
|
309
364
|
parentSession: readString(value, "parentSession"),
|
|
310
|
-
correlation: normalizeMinimalCorrelation(readUnknown(value, "observmeCorrelation") ?? readUnknown(value, "correlation")),
|
|
311
365
|
});
|
|
312
366
|
}
|
|
313
367
|
|
|
314
|
-
function normalizeMinimalCorrelation(value: unknown): MinimalSessionCorrelation | undefined {
|
|
315
|
-
if (!isRecord(value)) return undefined;
|
|
316
|
-
|
|
317
|
-
const correlation = withoutUndefinedObjectValues({
|
|
318
|
-
workflowId: readString(value, "workflowId"),
|
|
319
|
-
agentId: readString(value, "agentId"),
|
|
320
|
-
parentAgentId: readString(value, "parentAgentId"),
|
|
321
|
-
rootAgentId: readString(value, "rootAgentId"),
|
|
322
|
-
parentSessionId: readString(value, "parentSessionId"),
|
|
323
|
-
depth: readInteger(value, "depth"),
|
|
324
|
-
spawnId: readString(value, "spawnId"),
|
|
325
|
-
capability: readString(value, "capability"),
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
return Object.keys(correlation).length === 0 ? undefined : correlation;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
368
|
function withoutUndefinedObjectValues<T extends Record<string, unknown>>(value: T): T {
|
|
332
369
|
const definedEntries: Array<[string, unknown]> = [];
|
|
333
370
|
for (const entry of Object.entries(value)) {
|
|
@@ -388,19 +425,84 @@ async function shutDownPreviousSessionBeforeDuplicateStart(
|
|
|
388
425
|
session: ObservMeTelemetrySession,
|
|
389
426
|
ctx: ObservMeHandlerContext,
|
|
390
427
|
state: HandlerSessionState,
|
|
391
|
-
): Promise<
|
|
428
|
+
): Promise<boolean> {
|
|
392
429
|
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_DUPLICATE_START, buildDuplicateSessionStartAttributes(session));
|
|
393
430
|
|
|
394
431
|
try {
|
|
395
|
-
await shutDownTelemetrySession(session, duplicateSessionStartShutdownEvent(), ctx, state);
|
|
432
|
+
const result = await shutDownTelemetrySession(session, duplicateSessionStartShutdownEvent(), ctx, state);
|
|
433
|
+
if (otelCleanupCompleted(result)) return true;
|
|
434
|
+
notifyPendingTelemetryCleanup(ctx);
|
|
435
|
+
return false;
|
|
396
436
|
} catch (error) {
|
|
397
437
|
recordDuplicateSessionStartShutdownError(session, error);
|
|
398
438
|
clearObsSessionRuntimeState();
|
|
399
439
|
clearObsAgentsRuntimeState();
|
|
400
440
|
state.session = undefined;
|
|
441
|
+
state.pendingCleanup = session;
|
|
442
|
+
notifyPendingTelemetryCleanup(ctx);
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function resolvePendingTelemetryCleanupBeforeStart(
|
|
448
|
+
state: HandlerSessionState,
|
|
449
|
+
ctx: ObservMeHandlerContext,
|
|
450
|
+
): Promise<boolean> {
|
|
451
|
+
const pendingSession = state.pendingCleanup;
|
|
452
|
+
if (!pendingSession) return true;
|
|
453
|
+
|
|
454
|
+
const result = await recordControllerOperationResult(pendingSession, "shutdown");
|
|
455
|
+
if (otelCleanupCompleted(result)) {
|
|
456
|
+
if (state.pendingCleanup === pendingSession) state.pendingCleanup = undefined;
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
retainUnresolvedTelemetryCleanup(state, pendingSession, result);
|
|
461
|
+
notifyPendingTelemetryCleanup(ctx);
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function retainUnresolvedTelemetryCleanup(
|
|
466
|
+
state: HandlerSessionState,
|
|
467
|
+
session: ObservMeTelemetrySession,
|
|
468
|
+
result: BoundedOtelOperationResult | undefined,
|
|
469
|
+
): void {
|
|
470
|
+
if (!result || otelCleanupCompleted(result)) return;
|
|
471
|
+
|
|
472
|
+
state.pendingCleanup = session;
|
|
473
|
+
void result.settlement?.then(releaseSettledTelemetryCleanup.bind(undefined, state, session));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function releaseSettledTelemetryCleanup(
|
|
477
|
+
state: HandlerSessionState,
|
|
478
|
+
session: ObservMeTelemetrySession,
|
|
479
|
+
settlement: OtelOperationSettlement,
|
|
480
|
+
): void {
|
|
481
|
+
if (!otelCleanupCompleted(settlement) || state.pendingCleanup !== session) return;
|
|
482
|
+
state.pendingCleanup = undefined;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function otelCleanupCompleted(
|
|
486
|
+
result: Pick<BoundedOtelOperationResult, "completed" | "timedOut" | "error">,
|
|
487
|
+
): boolean {
|
|
488
|
+
return result.completed && !result.timedOut && !result.error;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function notifyPendingTelemetryCleanup(ctx: ObservMeHandlerContext): void {
|
|
492
|
+
if (ctx.hasUI === false || !ctx.ui?.notify) return;
|
|
493
|
+
|
|
494
|
+
const message = "ObservMe telemetry startup was deferred because prior OTEL shutdown cleanup is still unresolved.";
|
|
495
|
+
try {
|
|
496
|
+
void Promise.resolve(ctx.ui.notify(message, "warning")).catch(ignorePendingCleanupDiagnosticError);
|
|
497
|
+
} catch {
|
|
498
|
+
return;
|
|
401
499
|
}
|
|
402
500
|
}
|
|
403
501
|
|
|
502
|
+
function ignorePendingCleanupDiagnosticError(): undefined {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
|
|
404
506
|
function buildDuplicateSessionStartAttributes(session: ObservMeTelemetrySession): AttributeMap {
|
|
405
507
|
return withoutUndefinedAttributes({
|
|
406
508
|
[LOG_ATTRIBUTES.PI_SESSION_ID]: readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
|
|
@@ -429,38 +531,142 @@ function recordDuplicateSessionStartShutdownError(session: ObservMeTelemetrySess
|
|
|
429
531
|
);
|
|
430
532
|
}
|
|
431
533
|
|
|
534
|
+
async function cleanUpFailedSessionStart(
|
|
535
|
+
session: ObservMeTelemetrySession,
|
|
536
|
+
ctx: ObservMeHandlerContext,
|
|
537
|
+
state: HandlerSessionState,
|
|
538
|
+
): Promise<void> {
|
|
539
|
+
const labels = metricLabels(session.config, session.lineage);
|
|
540
|
+
let shutdownResult: BoundedOtelOperationResult | undefined;
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
deactivateSessionActiveAgent(session, labels);
|
|
544
|
+
endAllActiveSpans(session);
|
|
545
|
+
endActiveSpan(session, session.sessionSpan);
|
|
546
|
+
await clearExtensionStatus(ctx);
|
|
547
|
+
await recordControllerOperationResult(session, "flush");
|
|
548
|
+
shutdownResult = await recordControllerOperationResult(session, "shutdown");
|
|
549
|
+
} finally {
|
|
550
|
+
disposeSessionActiveAgentLease(session);
|
|
551
|
+
clearTelemetrySessionRuntimeState(session, state);
|
|
552
|
+
retainUnresolvedTelemetryCleanup(state, session, shutdownResult);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
432
556
|
async function shutDownTelemetrySession(
|
|
433
557
|
session: ObservMeTelemetrySession,
|
|
434
558
|
event: unknown,
|
|
435
559
|
ctx: ObservMeHandlerContext,
|
|
436
560
|
state: HandlerSessionState,
|
|
437
|
-
): Promise<
|
|
561
|
+
): Promise<BoundedOtelOperationResult> {
|
|
438
562
|
const labels = metricLabels(session.config, session.lineage);
|
|
439
563
|
const shutdownAttributes = buildShutdownAttributes(event, session);
|
|
440
564
|
const failed = workflowFailed(event);
|
|
565
|
+
let shutdownResult: BoundedOtelOperationResult | undefined;
|
|
441
566
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
567
|
+
try {
|
|
568
|
+
deactivateSessionActiveAgent(session, labels);
|
|
569
|
+
session.metrics.sessionsShutdown.add(1, labels);
|
|
570
|
+
recordWorkflowShutdownTelemetry(session, shutdownAttributes, failed, labels);
|
|
571
|
+
endAllActiveSpans(session);
|
|
572
|
+
session.sessionSpan?.addEvent(LOG_EVENT_NAMES.SESSION_SHUTDOWN, shutdownAttributes);
|
|
573
|
+
if (failed) session.sessionSpan?.setStatus({ code: SpanStatusCode.ERROR });
|
|
574
|
+
endActiveSpan(session, session.sessionSpan);
|
|
575
|
+
await clearExtensionStatus(ctx);
|
|
576
|
+
await recordControllerOperationResult(session, "flush");
|
|
577
|
+
shutdownResult = await recordControllerOperationResult(session, "shutdown");
|
|
578
|
+
return shutdownResult;
|
|
579
|
+
} finally {
|
|
580
|
+
disposeSessionActiveAgentLease(session);
|
|
581
|
+
clearTelemetrySessionRuntimeState(session, state);
|
|
582
|
+
retainUnresolvedTelemetryCleanup(state, session, shutdownResult);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function activateSessionActiveAgent(
|
|
587
|
+
session: ObservMeTelemetrySession,
|
|
588
|
+
labels: Record<string, string>,
|
|
589
|
+
): void {
|
|
590
|
+
if (session.activeAgentRecorded) return;
|
|
591
|
+
|
|
592
|
+
session.metrics.activeAgents.add(1, labels);
|
|
593
|
+
session.activeAgentRecorded = true;
|
|
594
|
+
session.activeAgentLease?.activate();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function deactivateSessionActiveAgent(
|
|
598
|
+
session: ObservMeTelemetrySession,
|
|
599
|
+
labels: Record<string, string>,
|
|
600
|
+
): void {
|
|
601
|
+
session.activeAgentLease?.deactivate();
|
|
602
|
+
if (!session.activeAgentRecorded) return;
|
|
603
|
+
|
|
604
|
+
session.activeAgentRecorded = false;
|
|
605
|
+
session.metrics.activeAgents.add(-1, labels);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function disposeSessionActiveAgentLease(session: ObservMeTelemetrySession): void {
|
|
609
|
+
session.activeAgentLease?.dispose();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function clearTelemetrySessionRuntimeState(
|
|
613
|
+
session: ObservMeTelemetrySession,
|
|
614
|
+
state: HandlerSessionState,
|
|
615
|
+
): void {
|
|
452
616
|
clearObsSessionRuntimeState();
|
|
453
617
|
clearObsAgentsRuntimeState();
|
|
618
|
+
if (state.session === session) state.session = undefined;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function clearDisabledTelemetryRuntimeState(state: HandlerSessionState): void {
|
|
454
622
|
state.session = undefined;
|
|
623
|
+
clearObsSessionRuntimeState();
|
|
624
|
+
clearObsAgentsRuntimeState();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function handleTelemetryStartupFailure(
|
|
628
|
+
state: HandlerSessionState,
|
|
629
|
+
ctx: ObservMeHandlerContext,
|
|
630
|
+
error: unknown,
|
|
631
|
+
): Promise<void> {
|
|
632
|
+
clearDisabledTelemetryRuntimeState(state);
|
|
633
|
+
if (!(error instanceof ObservMeOtelStartupError)) throw error;
|
|
634
|
+
|
|
635
|
+
recordObsStatusExportResult({ operation: "startup", error });
|
|
636
|
+
await clearExtensionStatus(ctx);
|
|
637
|
+
notifyTelemetryStartupFailure(ctx, error.message);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function notifyTelemetryStartupFailure(ctx: ObservMeHandlerContext, message: string): void {
|
|
641
|
+
if (ctx.hasUI === false || !ctx.ui?.notify) return;
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
void Promise.resolve(ctx.ui.notify(message, "error")).catch(ignoreStartupDiagnosticError);
|
|
645
|
+
} catch {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function ignoreStartupDiagnosticError(): undefined {
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function clearExtensionStatus(ctx: ObservMeHandlerContext): Promise<void> {
|
|
655
|
+
try {
|
|
656
|
+
await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, undefined);
|
|
657
|
+
} catch {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
455
660
|
}
|
|
456
661
|
|
|
457
662
|
async function recordControllerOperationResult(
|
|
458
663
|
session: ObservMeTelemetrySession,
|
|
459
664
|
operation: BoundedOtelOperationResult["operation"],
|
|
460
|
-
): Promise<
|
|
665
|
+
): Promise<BoundedOtelOperationResult> {
|
|
461
666
|
const result = await runControllerOperation(session, operation);
|
|
462
667
|
recordObsStatusExportResult(result);
|
|
463
668
|
recordExportOperationResult(session, result);
|
|
669
|
+
return result;
|
|
464
670
|
}
|
|
465
671
|
|
|
466
672
|
async function runControllerOperation(
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
2
|
+
import type {
|
|
3
|
+
BeforeProviderRequestEvent,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
6
|
import { recordObsSessionLlmCall } from "../../commands/obs-session.ts";
|
|
3
7
|
import { LOG_EVENT_NAMES } from "../../semconv/metrics.ts";
|
|
4
8
|
import { SPAN_NAMES } from "../../semconv/spans.ts";
|
|
@@ -26,7 +30,7 @@ import {
|
|
|
26
30
|
startActiveChildSpan,
|
|
27
31
|
} from "../handler-internals.ts";
|
|
28
32
|
import type { HandlerRegistrar } from "../handler-runtime.ts";
|
|
29
|
-
import type {
|
|
33
|
+
import type { HandlerSessionState, PiEvent, PiHandler } from "../handler-types.ts";
|
|
30
34
|
import { recordBashExecution } from "./tool-bash.ts";
|
|
31
35
|
|
|
32
36
|
export function registerLlmHandlers(registrar: HandlerRegistrar, state: HandlerSessionState): void {
|
|
@@ -35,14 +39,14 @@ export function registerLlmHandlers(registrar: HandlerRegistrar, state: HandlerS
|
|
|
35
39
|
registrar.add("message_end", createMessageEndHandler(state));
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
function createBeforeProviderRequestHandler(state: HandlerSessionState):
|
|
42
|
+
function createBeforeProviderRequestHandler(state: HandlerSessionState): PiHandler<"before_provider_request"> {
|
|
39
43
|
return handleBeforeProviderRequest.bind(undefined, state);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
function handleBeforeProviderRequest(
|
|
43
47
|
state: HandlerSessionState,
|
|
44
|
-
event:
|
|
45
|
-
ctx:
|
|
48
|
+
event: BeforeProviderRequestEvent,
|
|
49
|
+
ctx: ExtensionContext,
|
|
46
50
|
): void {
|
|
47
51
|
const session = state.session;
|
|
48
52
|
if (!session) return;
|
|
@@ -61,14 +65,14 @@ function handleBeforeProviderRequest(
|
|
|
61
65
|
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.LLM_REQUEST_STARTED, attributes);
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
function createAfterProviderResponseHandler(state: HandlerSessionState):
|
|
68
|
+
function createAfterProviderResponseHandler(state: HandlerSessionState): PiHandler<"after_provider_response"> {
|
|
65
69
|
return handleAfterProviderResponse.bind(undefined, state);
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
function handleAfterProviderResponse(
|
|
69
73
|
state: HandlerSessionState,
|
|
70
|
-
event:
|
|
71
|
-
_ctx:
|
|
74
|
+
event: PiEvent<"after_provider_response">,
|
|
75
|
+
_ctx: ExtensionContext,
|
|
72
76
|
): void {
|
|
73
77
|
const session = state.session;
|
|
74
78
|
if (!session) return;
|
|
@@ -77,11 +81,15 @@ function handleAfterProviderResponse(
|
|
|
77
81
|
span?.setAttributes(buildLlmResponseAttributes(event));
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
function createMessageEndHandler(state: HandlerSessionState):
|
|
84
|
+
function createMessageEndHandler(state: HandlerSessionState): PiHandler<"message_end"> {
|
|
81
85
|
return handleMessageEnd.bind(undefined, state);
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
function handleMessageEnd(
|
|
88
|
+
function handleMessageEnd(
|
|
89
|
+
state: HandlerSessionState,
|
|
90
|
+
event: PiEvent<"message_end">,
|
|
91
|
+
_ctx: ExtensionContext,
|
|
92
|
+
): void {
|
|
85
93
|
const session = state.session;
|
|
86
94
|
if (!session) return;
|
|
87
95
|
|