@posthog/agent 2.3.678 → 2.3.703

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.
@@ -116,6 +116,12 @@ export function getConnectedMcpServerNames(): string[] {
116
116
  return [...names];
117
117
  }
118
118
 
119
+ /** Snapshot of every tool currently in the metadata cache. Used by the
120
+ * context-breakdown estimator to size the MCP category. */
121
+ export function getCachedMcpTools(): McpToolMetadata[] {
122
+ return [...mcpToolMetadataCache.values()];
123
+ }
124
+
119
125
  export function getMcpToolApprovalState(
120
126
  toolName: string,
121
127
  ): McpToolApprovalState | undefined {
@@ -10,6 +10,7 @@ import type {
10
10
  } from "@anthropic-ai/claude-agent-sdk";
11
11
  import type { Pushable } from "../../utils/streams";
12
12
  import type { BaseSession } from "../base-acp-agent";
13
+ import type { ContextBreakdownBaseline } from "./context-breakdown";
13
14
  import type { McpToolApprovals } from "./mcp/tool-metadata";
14
15
  import type { SettingsManager } from "./session/settings";
15
16
  import type { CodeExecutionMode } from "./tools";
@@ -65,6 +66,8 @@ export type Session = BaseSession & {
65
66
  pendingMessages: Map<string, PendingMessage>;
66
67
  nextPendingOrder: number;
67
68
  emitRawSDKMessages: boolean | SDKMessageFilter[];
69
+ /** Refreshed at session init and on MCP/skill changes. */
70
+ contextBreakdownBaseline?: ContextBreakdownBaseline;
68
71
  };
69
72
 
70
73
  export type ToolUseCache = {
@@ -64,6 +64,12 @@ import {
64
64
  nodeWritableToWebWritable,
65
65
  } from "../../utils/streams";
66
66
  import { BaseAcpAgent, type BaseSession } from "../base-acp-agent";
67
+ import {
68
+ buildBreakdown,
69
+ type ContextBreakdownBaseline,
70
+ emptyBaseline,
71
+ estimateTokens,
72
+ } from "../claude/context-breakdown";
67
73
  import { classifyAgentError } from "../error-classification";
68
74
  import {
69
75
  enabledLocalTools,
@@ -76,6 +82,7 @@ import { normalizeCodexConfigOptions } from "./models";
76
82
  import {
77
83
  type CodexSessionState,
78
84
  createSessionState,
85
+ resetSessionState,
79
86
  resetUsage,
80
87
  } from "./session-state";
81
88
  import { CodexSettingsManager } from "./settings";
@@ -161,6 +168,19 @@ function classifyPromptError(error: unknown): unknown {
161
168
  );
162
169
  }
163
170
 
171
+ // codex-rs/protocol/src/protocol.rs BASELINE_TOKENS — the always-resident
172
+ // floor (MCP schemas, skills, preset prompt) we can't attribute per-source.
173
+ const CODEX_BASELINE_TOKENS = 12000;
174
+
175
+ function buildCodexBaseline(
176
+ meta: NewSessionMeta | undefined,
177
+ ): ContextBreakdownBaseline {
178
+ const baseline = emptyBaseline();
179
+ baseline.systemPrompt =
180
+ CODEX_BASELINE_TOKENS + estimateTokens(meta?.systemPrompt);
181
+ return baseline;
182
+ }
183
+
164
184
  const CODEX_NATIVE_MODE: Record<CodeExecutionMode, CodexNativeMode> = {
165
185
  auto: "auto",
166
186
  default: "auto",
@@ -392,8 +412,10 @@ export class CodexAcpAgent extends BaseAcpAgent {
392
412
  response.configOptions,
393
413
  );
394
414
 
395
- // Initialize session state
396
- this.sessionState = createSessionState(response.sessionId, params.cwd, {
415
+ // Initialize session state. Mutate in place — codex-client closure-
416
+ // captured this object in the constructor and writes contextUsed/
417
+ // accumulatedUsage to it on every upstream usage_update.
418
+ resetSessionState(this.sessionState, response.sessionId, params.cwd, {
397
419
  taskRunId: meta?.taskRunId,
398
420
  taskId: resolveTaskId(meta),
399
421
  modeId: response.modes?.currentModeId ?? "auto",
@@ -402,6 +424,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
402
424
  });
403
425
  this.sessionId = response.sessionId;
404
426
  this.sessionState.configOptions = response.configOptions ?? [];
427
+ this.sessionState.contextBreakdownBaseline = buildCodexBaseline(meta);
405
428
 
406
429
  await this.applyInitialPermissionMode(
407
430
  response.sessionId,
@@ -445,7 +468,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
445
468
  // notifications (TURN_COMPLETE, USAGE_UPDATE) after a reload. newSession
446
469
  // and unstable_resumeSession both do this; loadSession historically did
447
470
  // not, which silently broke task-completion tracking on re-attach.
448
- this.sessionState = createSessionState(params.sessionId, params.cwd, {
471
+ resetSessionState(this.sessionState, params.sessionId, params.cwd, {
449
472
  taskRunId: meta?.taskRunId,
450
473
  taskId: resolveTaskId(meta),
451
474
  modeId: response.modes?.currentModeId ?? "auto",
@@ -453,6 +476,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
453
476
  });
454
477
  this.sessionId = params.sessionId;
455
478
  this.sessionState.configOptions = response.configOptions ?? [];
479
+ this.sessionState.contextBreakdownBaseline = buildCodexBaseline(meta);
456
480
 
457
481
  if (meta?.taskRunId) {
458
482
  await this.client.extNotification(POSTHOG_NOTIFICATIONS.SDK_SESSION, {
@@ -491,7 +515,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
491
515
  loadResponse.modes?.currentModeId,
492
516
  meta?.permissionMode,
493
517
  );
494
- this.sessionState = createSessionState(params.sessionId, params.cwd, {
518
+ resetSessionState(this.sessionState, params.sessionId, params.cwd, {
495
519
  taskRunId: meta?.taskRunId,
496
520
  taskId: resolveTaskId(meta),
497
521
  modeId: loadResponse.modes?.currentModeId ?? "auto",
@@ -499,6 +523,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
499
523
  });
500
524
  this.sessionId = params.sessionId;
501
525
  this.sessionState.configOptions = loadResponse.configOptions ?? [];
526
+ this.sessionState.contextBreakdownBaseline = buildCodexBaseline(meta);
502
527
 
503
528
  if (meta?.taskRunId) {
504
529
  await this.client.extNotification(POSTHOG_NOTIFICATIONS.SDK_SESSION, {
@@ -538,7 +563,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
538
563
  );
539
564
 
540
565
  const requestedPermissionMode = toCodexPermissionMode(meta?.permissionMode);
541
- this.sessionState = createSessionState(newResponse.sessionId, params.cwd, {
566
+ resetSessionState(this.sessionState, newResponse.sessionId, params.cwd, {
542
567
  taskRunId: meta?.taskRunId,
543
568
  taskId: resolveTaskId(meta),
544
569
  modeId: newResponse.modes?.currentModeId ?? "auto",
@@ -546,6 +571,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
546
571
  });
547
572
  this.sessionId = newResponse.sessionId;
548
573
  this.sessionState.configOptions = newResponse.configOptions ?? [];
574
+ this.sessionState.contextBreakdownBaseline = buildCodexBaseline(meta);
549
575
 
550
576
  await this.applyInitialPermissionMode(
551
577
  newResponse.sessionId,
@@ -730,6 +756,21 @@ export class CodexAcpAgent extends BaseAcpAgent {
730
756
  }
731
757
  }
732
758
 
759
+ // Emit the per-source breakdown so the renderer's ContextBreakdownPopover
760
+ // has data to show. This fires regardless of `taskRunId` (local sessions
761
+ // need it too) and regardless of `response.usage` (codex-acp doesn't
762
+ // populate it — context size comes from upstream usage_update events,
763
+ // tracked on sessionState.contextUsed).
764
+ if (this.sessionState.contextUsed !== undefined) {
765
+ await this.client.extNotification(POSTHOG_NOTIFICATIONS.USAGE_UPDATE, {
766
+ sessionId: params.sessionId,
767
+ breakdown: buildBreakdown(
768
+ this.sessionState.contextBreakdownBaseline ?? emptyBaseline(),
769
+ this.sessionState.contextUsed,
770
+ ),
771
+ });
772
+ }
773
+
733
774
  return response;
734
775
  }
735
776
 
@@ -14,7 +14,7 @@ vi.mock("../../enrichment/file-enricher", () => ({
14
14
  }));
15
15
 
16
16
  import { createCodexClient } from "./codex-client";
17
- import { createSessionState } from "./session-state";
17
+ import { createSessionState, resetSessionState } from "./session-state";
18
18
 
19
19
  function makeUpstream(response: ReadTextFileResponse): AgentSideConnection & {
20
20
  readTextFile: ReturnType<typeof vi.fn>;
@@ -288,3 +288,51 @@ describe("createCodexClient onStructuredOutput", () => {
288
288
  expect(upstream.sessionUpdate).toHaveBeenCalledTimes(1);
289
289
  });
290
290
  });
291
+
292
+ describe("createCodexClient usage_update propagation", () => {
293
+ const logger = new Logger({ debug: false, prefix: "[test]" });
294
+
295
+ function makeUpstream(): AgentSideConnection {
296
+ return {
297
+ sessionUpdate: vi.fn(async () => {}),
298
+ requestPermission: vi.fn(),
299
+ readTextFile: vi.fn(),
300
+ writeTextFile: vi.fn(),
301
+ createTerminal: vi.fn(),
302
+ terminalOutput: vi.fn(),
303
+ releaseTerminal: vi.fn(),
304
+ waitForTerminalExit: vi.fn(),
305
+ killTerminal: vi.fn(),
306
+ extMethod: vi.fn(),
307
+ extNotification: vi.fn(),
308
+ } as unknown as AgentSideConnection;
309
+ }
310
+
311
+ // Regression: codex-client closure-captures the sessionState reference in
312
+ // its factory. CodexAcpAgent constructs the client once at startup with the
313
+ // initial "" sessionId state, then resetSessionState() mutates that same
314
+ // object on every newSession/loadSession/etc. If the agent ever reassigned
315
+ // `this.sessionState`, contextUsed writes would land on an orphan and the
316
+ // breakdown notification would never fire.
317
+ test("writes contextUsed to the same state object after resetSessionState", async () => {
318
+ const sessionState = createSessionState("", "/tmp");
319
+ const upstream = makeUpstream();
320
+ const client = createCodexClient(upstream, logger, sessionState);
321
+
322
+ resetSessionState(sessionState, "real-session", "/tmp/repo", {
323
+ taskRunId: "run-1",
324
+ });
325
+
326
+ await client.sessionUpdate?.({
327
+ sessionId: "real-session",
328
+ update: {
329
+ sessionUpdate: "usage_update",
330
+ used: 123_456,
331
+ size: 200_000,
332
+ },
333
+ } as unknown as SessionNotification);
334
+
335
+ expect(sessionState.contextUsed).toBe(123_456);
336
+ expect(sessionState.contextSize).toBe(200_000);
337
+ });
338
+ });
@@ -1,10 +1,6 @@
1
- /**
2
- * Session state tracking for Codex proxy agent.
3
- * Tracks usage accumulation, model/mode state, and config options.
4
- */
5
-
6
1
  import type { SessionConfigOption } from "@agentclientprotocol/sdk";
7
2
  import type { PermissionMode } from "../../execution-mode";
3
+ import type { ContextBreakdownBaseline } from "../claude/context-breakdown";
8
4
 
9
5
  export interface CodexUsage {
10
6
  inputTokens: number;
@@ -22,6 +18,7 @@ export interface CodexSessionState {
22
18
  accumulatedUsage: CodexUsage;
23
19
  contextSize?: number;
24
20
  contextUsed?: number;
21
+ contextBreakdownBaseline?: ContextBreakdownBaseline;
25
22
  permissionMode: PermissionMode;
26
23
  taskRunId?: string;
27
24
  taskId?: string;
@@ -56,6 +53,40 @@ export function createSessionState(
56
53
  };
57
54
  }
58
55
 
56
+ // codex-client closure-captures the original sessionState reference, so we
57
+ // must mutate in place across newSession/loadSession/resumeSession/forkSession
58
+ // — reassigning would orphan it and silently break usage propagation.
59
+ export function resetSessionState(
60
+ state: CodexSessionState,
61
+ sessionId: string,
62
+ cwd: string,
63
+ opts?: {
64
+ taskRunId?: string;
65
+ taskId?: string;
66
+ modeId?: string;
67
+ modelId?: string;
68
+ permissionMode?: PermissionMode;
69
+ },
70
+ ): void {
71
+ state.sessionId = sessionId;
72
+ state.cwd = cwd;
73
+ state.modeId = opts?.modeId ?? "auto";
74
+ state.modelId = opts?.modelId;
75
+ state.configOptions = [];
76
+ state.accumulatedUsage = {
77
+ inputTokens: 0,
78
+ outputTokens: 0,
79
+ cachedReadTokens: 0,
80
+ cachedWriteTokens: 0,
81
+ };
82
+ state.contextSize = undefined;
83
+ state.contextUsed = undefined;
84
+ state.contextBreakdownBaseline = undefined;
85
+ state.permissionMode = opts?.permissionMode ?? "auto";
86
+ state.taskRunId = opts?.taskRunId;
87
+ state.taskId = opts?.taskId;
88
+ }
89
+
59
90
  export function resetUsage(state: CodexSessionState): void {
60
91
  state.accumulatedUsage = {
61
92
  inputTokens: 0,