@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
|
@@ -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();
|
|
175
196
|
|
|
197
|
+
if (!config.enabled) {
|
|
198
|
+
clearDisabledTelemetryRuntimeState(state);
|
|
199
|
+
await clearExtensionStatus(ctx);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
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,19 +211,31 @@ async function handleSessionStart(
|
|
|
183
211
|
options.requireCompleteParentEnvelope ?? (options.trustedParentContext === true && recoveryCorrelation === undefined),
|
|
184
212
|
failOpenInvalidPropagation: true,
|
|
185
213
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
|
|
192
227
|
session.now = options.now ?? session.now ?? monotonicNowMs;
|
|
193
228
|
state.session = session;
|
|
194
229
|
|
|
195
230
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
231
|
+
const attributes = buildSessionAttributes(
|
|
232
|
+
event,
|
|
233
|
+
ctx,
|
|
234
|
+
session.config,
|
|
235
|
+
lineage,
|
|
236
|
+
recovery,
|
|
237
|
+
options.getThinkingLevel?.(),
|
|
238
|
+
);
|
|
199
239
|
const labels = metricLabels(session.config, lineage);
|
|
200
240
|
|
|
201
241
|
session.sessionAttributes = attributes;
|
|
@@ -206,7 +246,7 @@ async function handleSessionStart(
|
|
|
206
246
|
startObsSessionRuntimeState({
|
|
207
247
|
sessionId: readString(attributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
|
|
208
248
|
traceId: readSpanTraceId(session.sessionSpan),
|
|
209
|
-
|
|
249
|
+
config: session.config,
|
|
210
250
|
});
|
|
211
251
|
startObsAgentsRuntimeState({
|
|
212
252
|
lineage,
|
|
@@ -218,29 +258,31 @@ async function handleSessionStart(
|
|
|
218
258
|
session.metrics.sessionsStarted.add(1, labels);
|
|
219
259
|
session.sessionSpan.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, attributes);
|
|
220
260
|
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, attributes);
|
|
221
|
-
if (recovery.resumed && session.config.replayOnStart) emitStartupReplayTelemetry(session, attributes);
|
|
222
261
|
|
|
223
262
|
if (isRootWorkflow(lineage)) {
|
|
224
263
|
session.metrics.workflowsStarted.add(1, labels);
|
|
225
264
|
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.WORKFLOW_STARTED, attributes);
|
|
226
265
|
}
|
|
227
266
|
|
|
228
|
-
|
|
267
|
+
ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, EXTENSION_STATUS_VALUE);
|
|
229
268
|
activateSessionActiveAgent(session, labels);
|
|
269
|
+
if (session.config.agent.writeCorrelationEntry) {
|
|
270
|
+
appendSessionCorrelationEntry(options.appendEntry, lineage, recovery.customCorrelation);
|
|
271
|
+
}
|
|
230
272
|
} catch (error) {
|
|
231
273
|
await cleanUpFailedSessionStart(session, ctx, state);
|
|
232
274
|
throw error;
|
|
233
275
|
}
|
|
234
276
|
}
|
|
235
277
|
|
|
236
|
-
function createSessionShutdownHandler(state: HandlerSessionState):
|
|
278
|
+
function createSessionShutdownHandler(state: HandlerSessionState): PiHandler<"session_shutdown"> {
|
|
237
279
|
return handleSessionShutdown.bind(undefined, state);
|
|
238
280
|
}
|
|
239
281
|
|
|
240
282
|
async function handleSessionShutdown(
|
|
241
283
|
state: HandlerSessionState,
|
|
242
|
-
event:
|
|
243
|
-
ctx:
|
|
284
|
+
event: SessionShutdownEvent,
|
|
285
|
+
ctx: ExtensionContext,
|
|
244
286
|
): Promise<void> {
|
|
245
287
|
const session = state.session;
|
|
246
288
|
if (!session) return;
|
|
@@ -256,8 +298,13 @@ async function resolveStartupRecovery(
|
|
|
256
298
|
): Promise<StartupRecoveryState> {
|
|
257
299
|
const sessionFile = resolveSessionFilePath(event, ctx);
|
|
258
300
|
const readHeader = options.readSessionHeader ?? readSessionHeaderFromFile;
|
|
259
|
-
|
|
260
|
-
|
|
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;
|
|
261
308
|
|
|
262
309
|
return {
|
|
263
310
|
resumed: isExistingSessionStart(event),
|
|
@@ -267,16 +314,6 @@ async function resolveStartupRecovery(
|
|
|
267
314
|
};
|
|
268
315
|
}
|
|
269
316
|
|
|
270
|
-
function emitStartupReplayTelemetry(session: ObservMeTelemetrySession, attributes: AttributeMap): void {
|
|
271
|
-
const replayAttributes = {
|
|
272
|
-
...attributes,
|
|
273
|
-
[COMMON_SPAN_ATTRIBUTES.OBSERVME_REPLAYED]: true,
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
session.sessionSpan?.addEvent(LOG_EVENT_NAMES.SESSION_STARTED, replayAttributes);
|
|
277
|
-
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_STARTED, replayAttributes);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
317
|
function buildRecoveryLineageEnv(
|
|
281
318
|
config: ObservMeConfig,
|
|
282
319
|
correlation: MinimalSessionCorrelation | undefined,
|
|
@@ -291,19 +328,28 @@ function buildRecoveryLineageEnv(
|
|
|
291
328
|
...definedEnvValue(config.agent.parentIdEnv, correlation.parentAgentId),
|
|
292
329
|
...definedEnvValue(config.agent.rootIdEnv, correlation.rootAgentId),
|
|
293
330
|
...definedEnvValue(config.agent.parentSessionIdEnv, correlation.parentSessionId),
|
|
294
|
-
...definedEnvValue(config.agent.depthEnv,
|
|
331
|
+
...definedEnvValue(config.agent.depthEnv, recoveryPropagationDepth(correlation)),
|
|
295
332
|
...definedEnvValue(config.agent.spawnIdEnv, correlation.spawnId),
|
|
296
333
|
...definedEnvValue(config.agent.capabilityEnv, correlation.capability),
|
|
297
334
|
};
|
|
298
335
|
}
|
|
299
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
|
+
|
|
300
343
|
function definedEnvValue(name: string, value: string | undefined): NodeJS.ProcessEnv {
|
|
301
344
|
return value === undefined || value === "" ? {} : { [name]: value };
|
|
302
345
|
}
|
|
303
346
|
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
347
|
+
function readActiveBranchCorrelation(ctx: ObservMeHandlerContext): StartupRecoveryState["customCorrelation"] {
|
|
348
|
+
try {
|
|
349
|
+
return readLatestSessionCorrelation(ctx.sessionManager?.getBranch());
|
|
350
|
+
} catch {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
307
353
|
}
|
|
308
354
|
|
|
309
355
|
function normalizeSessionHeader(value: unknown): SessionRecoveryHeader | undefined {
|
|
@@ -316,27 +362,9 @@ function normalizeSessionHeader(value: unknown): SessionRecoveryHeader | undefin
|
|
|
316
362
|
timestamp: readString(value, "timestamp"),
|
|
317
363
|
cwd: readString(value, "cwd"),
|
|
318
364
|
parentSession: readString(value, "parentSession"),
|
|
319
|
-
correlation: normalizeMinimalCorrelation(readUnknown(value, "observmeCorrelation") ?? readUnknown(value, "correlation")),
|
|
320
365
|
});
|
|
321
366
|
}
|
|
322
367
|
|
|
323
|
-
function normalizeMinimalCorrelation(value: unknown): MinimalSessionCorrelation | undefined {
|
|
324
|
-
if (!isRecord(value)) return undefined;
|
|
325
|
-
|
|
326
|
-
const correlation = withoutUndefinedObjectValues({
|
|
327
|
-
workflowId: readString(value, "workflowId"),
|
|
328
|
-
agentId: readString(value, "agentId"),
|
|
329
|
-
parentAgentId: readString(value, "parentAgentId"),
|
|
330
|
-
rootAgentId: readString(value, "rootAgentId"),
|
|
331
|
-
parentSessionId: readString(value, "parentSessionId"),
|
|
332
|
-
depth: readInteger(value, "depth"),
|
|
333
|
-
spawnId: readString(value, "spawnId"),
|
|
334
|
-
capability: readString(value, "capability"),
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
return Object.keys(correlation).length === 0 ? undefined : correlation;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
368
|
function withoutUndefinedObjectValues<T extends Record<string, unknown>>(value: T): T {
|
|
341
369
|
const definedEntries: Array<[string, unknown]> = [];
|
|
342
370
|
for (const entry of Object.entries(value)) {
|
|
@@ -397,19 +425,84 @@ async function shutDownPreviousSessionBeforeDuplicateStart(
|
|
|
397
425
|
session: ObservMeTelemetrySession,
|
|
398
426
|
ctx: ObservMeHandlerContext,
|
|
399
427
|
state: HandlerSessionState,
|
|
400
|
-
): Promise<
|
|
428
|
+
): Promise<boolean> {
|
|
401
429
|
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.SESSION_DUPLICATE_START, buildDuplicateSessionStartAttributes(session));
|
|
402
430
|
|
|
403
431
|
try {
|
|
404
|
-
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;
|
|
405
436
|
} catch (error) {
|
|
406
437
|
recordDuplicateSessionStartShutdownError(session, error);
|
|
407
438
|
clearObsSessionRuntimeState();
|
|
408
439
|
clearObsAgentsRuntimeState();
|
|
409
440
|
state.session = undefined;
|
|
441
|
+
state.pendingCleanup = session;
|
|
442
|
+
notifyPendingTelemetryCleanup(ctx);
|
|
443
|
+
return false;
|
|
410
444
|
}
|
|
411
445
|
}
|
|
412
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;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function ignorePendingCleanupDiagnosticError(): undefined {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
|
|
413
506
|
function buildDuplicateSessionStartAttributes(session: ObservMeTelemetrySession): AttributeMap {
|
|
414
507
|
return withoutUndefinedAttributes({
|
|
415
508
|
[LOG_ATTRIBUTES.PI_SESSION_ID]: readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_SESSION_ID),
|
|
@@ -444,6 +537,7 @@ async function cleanUpFailedSessionStart(
|
|
|
444
537
|
state: HandlerSessionState,
|
|
445
538
|
): Promise<void> {
|
|
446
539
|
const labels = metricLabels(session.config, session.lineage);
|
|
540
|
+
let shutdownResult: BoundedOtelOperationResult | undefined;
|
|
447
541
|
|
|
448
542
|
try {
|
|
449
543
|
deactivateSessionActiveAgent(session, labels);
|
|
@@ -451,10 +545,11 @@ async function cleanUpFailedSessionStart(
|
|
|
451
545
|
endActiveSpan(session, session.sessionSpan);
|
|
452
546
|
await clearExtensionStatus(ctx);
|
|
453
547
|
await recordControllerOperationResult(session, "flush");
|
|
454
|
-
await recordControllerOperationResult(session, "shutdown");
|
|
548
|
+
shutdownResult = await recordControllerOperationResult(session, "shutdown");
|
|
455
549
|
} finally {
|
|
456
550
|
disposeSessionActiveAgentLease(session);
|
|
457
551
|
clearTelemetrySessionRuntimeState(session, state);
|
|
552
|
+
retainUnresolvedTelemetryCleanup(state, session, shutdownResult);
|
|
458
553
|
}
|
|
459
554
|
}
|
|
460
555
|
|
|
@@ -463,10 +558,11 @@ async function shutDownTelemetrySession(
|
|
|
463
558
|
event: unknown,
|
|
464
559
|
ctx: ObservMeHandlerContext,
|
|
465
560
|
state: HandlerSessionState,
|
|
466
|
-
): Promise<
|
|
561
|
+
): Promise<BoundedOtelOperationResult> {
|
|
467
562
|
const labels = metricLabels(session.config, session.lineage);
|
|
468
563
|
const shutdownAttributes = buildShutdownAttributes(event, session);
|
|
469
564
|
const failed = workflowFailed(event);
|
|
565
|
+
let shutdownResult: BoundedOtelOperationResult | undefined;
|
|
470
566
|
|
|
471
567
|
try {
|
|
472
568
|
deactivateSessionActiveAgent(session, labels);
|
|
@@ -478,10 +574,12 @@ async function shutDownTelemetrySession(
|
|
|
478
574
|
endActiveSpan(session, session.sessionSpan);
|
|
479
575
|
await clearExtensionStatus(ctx);
|
|
480
576
|
await recordControllerOperationResult(session, "flush");
|
|
481
|
-
await recordControllerOperationResult(session, "shutdown");
|
|
577
|
+
shutdownResult = await recordControllerOperationResult(session, "shutdown");
|
|
578
|
+
return shutdownResult;
|
|
482
579
|
} finally {
|
|
483
580
|
disposeSessionActiveAgentLease(session);
|
|
484
581
|
clearTelemetrySessionRuntimeState(session, state);
|
|
582
|
+
retainUnresolvedTelemetryCleanup(state, session, shutdownResult);
|
|
485
583
|
}
|
|
486
584
|
}
|
|
487
585
|
|
|
@@ -520,6 +618,39 @@ function clearTelemetrySessionRuntimeState(
|
|
|
520
618
|
if (state.session === session) state.session = undefined;
|
|
521
619
|
}
|
|
522
620
|
|
|
621
|
+
function clearDisabledTelemetryRuntimeState(state: HandlerSessionState): void {
|
|
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
|
+
|
|
523
654
|
async function clearExtensionStatus(ctx: ObservMeHandlerContext): Promise<void> {
|
|
524
655
|
try {
|
|
525
656
|
await ctx.ui?.setStatus?.(EXTENSION_STATUS_KEY, undefined);
|
|
@@ -531,10 +662,11 @@ async function clearExtensionStatus(ctx: ObservMeHandlerContext): Promise<void>
|
|
|
531
662
|
async function recordControllerOperationResult(
|
|
532
663
|
session: ObservMeTelemetrySession,
|
|
533
664
|
operation: BoundedOtelOperationResult["operation"],
|
|
534
|
-
): Promise<
|
|
665
|
+
): Promise<BoundedOtelOperationResult> {
|
|
535
666
|
const result = await runControllerOperation(session, operation);
|
|
536
667
|
recordObsStatusExportResult(result);
|
|
537
668
|
recordExportOperationResult(session, result);
|
|
669
|
+
return result;
|
|
538
670
|
}
|
|
539
671
|
|
|
540
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
|
|