Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit d667a11

Browse files
authored
fix(agent): export initialization failures to OTLP
Generated-By: PostHog Code Task-Id: cd137de1-90a0-4cb9-8509-56f5f210c0e9
1 parent 670be2d commit d667a11

7 files changed

Lines changed: 211 additions & 0 deletions

File tree

packages/agent/src/acp-extensions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export const POSTHOG_NOTIFICATIONS = {
3333
/** Error occurred during task execution */
3434
ERROR: "_posthog/error",
3535

36+
/** Agent runtime failed before its session became ready */
37+
INITIALIZATION_FAILED: "_posthog/initialization_failed",
38+
3639
/** Console/log output from the agent */
3740
CONSOLE: "_posthog/console",
3841

packages/agent/src/otel-telemetry.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,30 @@ describe("OtelRunTelemetry", () => {
176176
body: "run error",
177177
attrs: { error_source: "agent_server", stop_reason: "error" },
178178
},
179+
{
180+
name: "initialization_failed",
181+
entry: makeEntry("_posthog/initialization_failed", {
182+
runtimeAdapter: "codex",
183+
initializationPhase: "session_setup",
184+
initMs: 12_345,
185+
requestedModel: "gpt-5.2-codex",
186+
gatewayConfigured: true,
187+
errorType: "timeout",
188+
timeoutMs: 30_000,
189+
errorDetail: "SECRET provider response",
190+
}),
191+
severityText: "ERROR",
192+
body: "agent initialization failed",
193+
attrs: {
194+
runtime_adapter: "codex",
195+
initialization_phase: "session_setup",
196+
init_ms: 12_345,
197+
requested_model: "gpt-5.2-codex",
198+
gateway_configured: true,
199+
error_type: "timeout",
200+
timeout_ms: 30_000,
201+
},
202+
},
179203
{
180204
name: "progress",
181205
entry: makeEntry("_posthog/progress", {
@@ -346,6 +370,20 @@ describe("OtelRunTelemetry", () => {
346370
);
347371
});
348372

373+
it("never exports initialization error detail", () => {
374+
const mapped = mapNotificationToLogRecord(
375+
makeEntry("_posthog/initialization_failed", {
376+
runtimeAdapter: "pi",
377+
errorDetail: "SECRET provider response",
378+
}),
379+
);
380+
381+
expect(mapped).not.toBeNull();
382+
expect(JSON.stringify([mapped?.body, mapped?.attributes])).not.toContain(
383+
"SECRET",
384+
);
385+
});
386+
349387
it("caps body length", () => {
350388
const mapped = mapNotificationToLogRecord(
351389
makeEntry("_posthog/progress", {

packages/agent/src/otel-telemetry.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
entryTime,
2020
MAX_BODY_CHARS,
2121
normalizeMethod,
22+
numAttr,
2223
strAttr,
2324
truncate,
2425
usageAttributes,
@@ -200,6 +201,19 @@ export function mapNotificationToLogRecord(
200201
strAttr(attrs, "stop_reason", params.stopReason);
201202
return record(ERROR, "run error", method, attrs);
202203
}
204+
case POSTHOG_NOTIFICATIONS.INITIALIZATION_FAILED: {
205+
const attrs: Attributes = {};
206+
strAttr(attrs, "runtime_adapter", params.runtimeAdapter);
207+
strAttr(attrs, "initialization_phase", params.initializationPhase);
208+
numAttr(attrs, "init_ms", params.initMs);
209+
strAttr(attrs, "requested_model", params.requestedModel);
210+
if (typeof params.gatewayConfigured === "boolean") {
211+
attrs.gateway_configured = params.gatewayConfigured;
212+
}
213+
strAttr(attrs, "error_type", params.errorType);
214+
numAttr(attrs, "timeout_ms", params.timeoutMs);
215+
return record(ERROR, "agent initialization failed", method, attrs);
216+
}
203217
// POSTHOG_NOTIFICATIONS.CONSOLE is deliberately NOT exported: those are
204218
// free-text agent-server diagnostics that interpolate arbitrary data
205219
// (prompt previews, stringified extension params), so shipping them would

packages/agent/src/server/agent-server.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,58 @@ describe("AgentServer HTTP Mode", () => {
448448
);
449449
};
450450

451+
it("exports safe telemetry when session initialization fails", async () => {
452+
const append = vi.fn();
453+
const shutdown = vi.fn(async () => {});
454+
const testServer = createServer({
455+
runtimeAdapter: "codex",
456+
model: "gpt-5.2-codex",
457+
}) as unknown as {
458+
initializingTelemetry: {
459+
append: typeof append;
460+
shutdown: typeof shutdown;
461+
};
462+
_doInitializeSession(
463+
payload: JwtPayload,
464+
controller: null,
465+
): Promise<void>;
466+
initializeSession(payload: JwtPayload, controller: null): Promise<void>;
467+
};
468+
testServer._doInitializeSession = vi.fn(async () => {
469+
testServer.initializingTelemetry = { append, shutdown };
470+
throw new Error("SECRET provider response");
471+
});
472+
const payload = {
473+
task_id: "test-task-id",
474+
run_id: "test-run-id",
475+
team_id: 1,
476+
user_id: 1,
477+
distinct_id: "test-distinct-id",
478+
mode: "interactive" as const,
479+
};
480+
481+
await expect(testServer.initializeSession(payload, null)).rejects.toThrow(
482+
"SECRET provider response",
483+
);
484+
485+
expect(append).toHaveBeenCalledWith(
486+
"test-run-id",
487+
expect.objectContaining({
488+
notification: expect.objectContaining({
489+
method: POSTHOG_NOTIFICATIONS.INITIALIZATION_FAILED,
490+
params: expect.objectContaining({
491+
runtimeAdapter: "codex",
492+
initializationPhase: "session_setup",
493+
requestedModel: "gpt-5.2-codex",
494+
errorType: "error",
495+
}),
496+
}),
497+
}),
498+
);
499+
expect(JSON.stringify(append.mock.calls)).not.toContain("SECRET");
500+
expect(shutdown).toHaveBeenCalledOnce();
501+
});
502+
451503
it("replays ACP notifications emitted before cloud session assignment", () => {
452504
const testServer = createServer() as unknown as {
453505
session: { sseController: null } | null;

packages/agent/src/server/agent-server.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ export class AgentServer {
400400
// often arrives while newSession() is still awaited (this.session is still null),
401401
// causing a second session to be created and duplicate Slack messages to be sent.
402402
private initializationPromise: Promise<void> | null = null;
403+
private initializingTelemetry: OtelRunTelemetry | undefined;
403404
private pendingEvents: Record<string, unknown>[] = [];
404405
/** ACP notifications emitted by newSession/resumeSession before this.session is assigned. */
405406
private preSessionEvents: Record<string, unknown>[] = [];
@@ -1487,9 +1488,36 @@ export class AgentServer {
14871488
payload,
14881489
sseController,
14891490
);
1491+
const initStartedAt = Date.now();
14901492
try {
14911493
await this.initializationPromise;
1494+
} catch (error) {
1495+
const telemetry = this.initializingTelemetry;
1496+
telemetry?.append(payload.run_id, {
1497+
type: "notification",
1498+
timestamp: new Date().toISOString(),
1499+
notification: {
1500+
jsonrpc: "2.0",
1501+
method: POSTHOG_NOTIFICATIONS.INITIALIZATION_FAILED,
1502+
params: {
1503+
runtimeAdapter: this.getRuntimeAdapter(),
1504+
initializationPhase: "session_setup",
1505+
initMs: Date.now() - initStartedAt,
1506+
requestedModel: this.config.model,
1507+
gatewayConfigured: Boolean(
1508+
process.env.LLM_GATEWAY_URL || this.config.apiUrl,
1509+
),
1510+
errorType:
1511+
error instanceof Error && error.name === "TimeoutError"
1512+
? "timeout"
1513+
: "error",
1514+
},
1515+
},
1516+
});
1517+
await telemetry?.shutdown();
1518+
throw error;
14921519
} finally {
1520+
this.initializingTelemetry = undefined;
14931521
this.initializationPromise = null;
14941522
}
14951523
}
@@ -1603,6 +1631,7 @@ export class AgentServer {
16031631
deviceInfo,
16041632
runtimeAdapter,
16051633
);
1634+
this.initializingTelemetry = telemetry;
16061635

16071636
const logWriter = new SessionLogWriter({
16081637
posthogAPI,
@@ -1821,6 +1850,7 @@ export class AgentServer {
18211850
pendingHandoffGitState: undefined,
18221851
sessionMeta: effectiveSessionMeta,
18231852
};
1853+
this.initializingTelemetry = undefined;
18241854
this.flushPreSessionEvents();
18251855

18261856
this.logger = new Logger({

packages/agent/src/server/pi-agent-server.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ describe("PiAgentServer", () => {
3333
sessionPayload: typeof payload,
3434
sseController: null,
3535
): Promise<void>;
36+
createRunTelemetry: ReturnType<typeof vi.fn>;
3637
};
38+
const append = vi.fn();
39+
const shutdown = vi.fn(async () => {});
40+
server.createRunTelemetry = vi.fn(() => ({ append, shutdown }));
3741
server.createSession = vi.fn(async () => {
3842
throw new Error("Pi RPC startup failed");
3943
});
@@ -58,6 +62,20 @@ describe("PiAgentServer", () => {
5862
}),
5963
}),
6064
);
65+
expect(append).toHaveBeenCalledWith(
66+
"run-1",
67+
expect.objectContaining({
68+
notification: expect.objectContaining({
69+
method: "_posthog/initialization_failed",
70+
params: expect.objectContaining({
71+
runtimeAdapter: "pi",
72+
initializationPhase: "session_setup",
73+
errorType: "error",
74+
}),
75+
}),
76+
}),
77+
);
78+
expect(shutdown).toHaveBeenCalledOnce();
6179
});
6280

6381
it.each([

packages/agent/src/server/pi-agent-server.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {
1111
import { serializeError } from "@posthog/shared";
1212
import { Hono } from "hono";
1313
import { z } from "zod/v4";
14+
import { POSTHOG_NOTIFICATIONS } from "../acp-extensions";
15+
import { OtelRunTelemetry } from "../otel-telemetry";
1416
import { createPiRpcClient, type PiRpcClient } from "../pi/rpc-client";
1517
import { piRpcCommandSchema, type RpcCommand } from "../pi/rpc-transport";
1618
import { PiRuntime } from "../pi/runtime";
@@ -132,6 +134,37 @@ export class PiAgentServer {
132134
this.app = this.createApp();
133135
}
134136

137+
private createRunTelemetry(
138+
payload: JwtPayload,
139+
): OtelRunTelemetry | undefined {
140+
const { otelLogsUrl, otelLogsToken } = this.config;
141+
if (!otelLogsUrl || !otelLogsToken) return undefined;
142+
try {
143+
return new OtelRunTelemetry(
144+
{
145+
url: otelLogsUrl,
146+
token: otelLogsToken,
147+
tracesUrl: this.config.otelTracesUrl,
148+
},
149+
{
150+
taskId: payload.task_id,
151+
runId: payload.run_id,
152+
deviceType: "cloud",
153+
teamId: payload.team_id,
154+
userId: payload.user_id,
155+
distinctId: payload.distinct_id,
156+
adapter: "pi",
157+
mode: payload.mode ?? this.config.mode,
158+
agentVersion: this.config.version,
159+
},
160+
new Logger({ debug: false, prefix: "[OtelRunTelemetry]" }),
161+
);
162+
} catch (error) {
163+
this.logger.warn("Failed to initialize OTel run telemetry", error);
164+
return undefined;
165+
}
166+
}
167+
135168
async start(): Promise<void> {
136169
await new Promise<void>((resolve) => {
137170
this.server = serve(
@@ -388,6 +421,29 @@ export class PiAgentServer {
388421
try {
389422
await initializationPromise;
390423
} catch (error) {
424+
const telemetry = this.createRunTelemetry(payload);
425+
telemetry?.append(payload.run_id, {
426+
type: "notification",
427+
timestamp: new Date().toISOString(),
428+
notification: {
429+
jsonrpc: "2.0",
430+
method: POSTHOG_NOTIFICATIONS.INITIALIZATION_FAILED,
431+
params: {
432+
runtimeAdapter: "pi",
433+
initializationPhase: "session_setup",
434+
initMs: Date.now() - initStartedAt,
435+
requestedModel: this.config.model,
436+
gatewayConfigured: Boolean(
437+
process.env.LLM_GATEWAY_URL || this.config.apiUrl,
438+
),
439+
errorType:
440+
error instanceof Error && error.name === "TimeoutError"
441+
? "timeout"
442+
: "error",
443+
},
444+
},
445+
});
446+
await telemetry?.shutdown();
391447
this.logger.error("Pi session initialization failed", {
392448
runtimeAdapter: "pi",
393449
initializationPhase: "session_setup",

0 commit comments

Comments
 (0)