@mduan/paseo-cli 0.11.0-fork.1 → 0.11.0-fork.2

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 (44) hide show
  1. package/node_modules/@getpaseo/client/package.json +3 -3
  2. package/node_modules/@getpaseo/highlight/package.json +1 -1
  3. package/node_modules/@getpaseo/plugin/package.json +3 -3
  4. package/node_modules/@getpaseo/protocol/dist/agent-types.d.ts +2 -0
  5. package/node_modules/@getpaseo/protocol/dist/generated/validation/ws-outbound.aot.js +106 -1
  6. package/node_modules/@getpaseo/protocol/dist/messages.js +3 -0
  7. package/node_modules/@getpaseo/protocol/package.json +1 -1
  8. package/node_modules/@getpaseo/relay/package.json +1 -1
  9. package/node_modules/@getpaseo/server/dist/server/builtin-plugins/package.json +1 -1
  10. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-manager.d.ts +2 -2
  11. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-projections.js +3 -0
  12. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-sdk-types.d.ts +3 -1
  13. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-storage.d.ts +1 -0
  14. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-storage.js +1 -0
  15. package/node_modules/@getpaseo/server/dist/server/server/agent/provider-subagents/subtitle-format.d.ts +5 -0
  16. package/node_modules/@getpaseo/server/dist/server/server/agent/provider-subagents/subtitle-format.js +16 -0
  17. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/appended-lines-reader.d.ts +20 -0
  18. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/appended-lines-reader.js +59 -0
  19. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/agent.js +61 -2
  20. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/live-source.d.ts +2 -0
  21. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/live-source.js +7 -0
  22. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/presentation.d.ts +2 -0
  23. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/presentation.js +3 -8
  24. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/transcript-cost.d.ts +18 -0
  25. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/transcript-cost.js +50 -0
  26. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex/pricing.d.ts +15 -1
  27. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex/pricing.js +32 -1
  28. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex/rollout-cost.d.ts +4 -8
  29. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex/rollout-cost.js +11 -51
  30. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex-app-server-agent.d.ts +5 -1
  31. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex-app-server-agent.js +82 -11
  32. package/node_modules/@getpaseo/server/dist/server/server/agent/turn-diffs/snapshot.d.ts +14 -0
  33. package/node_modules/@getpaseo/server/dist/server/server/agent/turn-diffs/snapshot.js +91 -4
  34. package/node_modules/@getpaseo/server/dist/server/server/agent/turn-diffs/store.d.ts +3 -3
  35. package/node_modules/@getpaseo/server/dist/server/server/agent/turn-diffs/store.js +24 -18
  36. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{index-9489c4e1587c4252f0bd3b6e4b1abab3.js → index-23bdbee37d31a9422c12f2a801979611.js} +21 -21
  37. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/index-23bdbee37d31a9422c12f2a801979611.js.br +0 -0
  38. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{index-9489c4e1587c4252f0bd3b6e4b1abab3.js.gz → index-23bdbee37d31a9422c12f2a801979611.js.gz} +0 -0
  39. package/node_modules/@getpaseo/server/dist/server/web-ui/index.html +1 -1
  40. package/node_modules/@getpaseo/server/dist/server/web-ui/index.html.br +0 -0
  41. package/node_modules/@getpaseo/server/dist/server/web-ui/index.html.gz +0 -0
  42. package/node_modules/@getpaseo/server/package.json +6 -6
  43. package/package.json +7 -7
  44. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/index-9489c4e1587c4252f0bd3b6e4b1abab3.js.br +0 -0
@@ -15,6 +15,8 @@ import { observeReplaySubagents, parseClaudeSubagentMeta, } from "./subagents/re
15
15
  import { foldSubagentObservations } from "./subagents/observation.js";
16
16
  import { observeReplayWorkflows, parseClaudeWorkflowRun, } from "./subagents/workflow-replay-source.js";
17
17
  import { readClaudeWorkflowResultFile } from "./subagents/workflow-output.js";
18
+ import { ClaudeTranscriptCost } from "./subagents/transcript-cost.js";
19
+ import { getSharedCodexPriceList } from "../codex/pricing.js";
18
20
  import { buildClaudeFeatures, claudeModelSupportsFastMode } from "./feature-definitions.js";
19
21
  import { buildBinaryDiagnosticRows, buildCommandResolutionDiagnosticRows, formatProviderDiagnostic, formatProviderDiagnosticError, } from "../diagnostic-utils.js";
20
22
  import { appendOrReplaceGrowingAssistantMessage, runProviderTurn } from "../provider-runner.js";
@@ -1534,6 +1536,8 @@ class ClaudeAgentSession {
1534
1536
  getToolInput: (toolUseId) => this.toolUseCache.get(toolUseId)?.input ?? null,
1535
1537
  readWorkflowResult: readClaudeWorkflowResultFile,
1536
1538
  });
1539
+ /** Task id -> cost reader over that subagent's transcript. */
1540
+ this.subagentCosts = new Map();
1537
1541
  this.sidechainTracker = new ClaudeSidechainTracker({
1538
1542
  getToolInput: (toolUseId) => this.toolUseCache.get(toolUseId)?.input ?? null,
1539
1543
  // Releases that predate the task protocol announce nothing, so the tracker keeps deriving
@@ -2130,6 +2134,7 @@ class ClaudeAgentSession {
2130
2134
  this.turnState = "idle";
2131
2135
  this.sidechainTracker.clear();
2132
2136
  this.taskProtocolSource.reset();
2137
+ this.subagentCosts.clear();
2133
2138
  this.input?.end();
2134
2139
  this.query?.close?.();
2135
2140
  await this.awaitWithTimeout(this.query?.interrupt?.(), "close query interrupt");
@@ -3325,6 +3330,7 @@ class ClaudeAgentSession {
3325
3330
  for (const event of foldSubagentObservations(subagentObservations)) {
3326
3331
  events.push({ type: "provider_subagent", provider: "claude", event });
3327
3332
  }
3333
+ this.refreshSubagentCost(message);
3328
3334
  for (const observation of subagentObservations) {
3329
3335
  if (observation.kind !== "declared")
3330
3336
  continue;
@@ -3851,6 +3857,53 @@ class ClaudeAgentSession {
3851
3857
  pushEvent(event) {
3852
3858
  this.notifySubscribers(event);
3853
3859
  }
3860
+ /**
3861
+ * Claude reports only a token count per subagent, so each usage report re-prices the
3862
+ * subagent's transcript from the point the previous report reached.
3863
+ */
3864
+ refreshSubagentCost(message) {
3865
+ if (message.type !== "system")
3866
+ return;
3867
+ if (message.subtype !== "task_progress" && message.subtype !== "task_notification")
3868
+ return;
3869
+ const taskId = message.task_id;
3870
+ if (typeof taskId !== "string" || !this.taskProtocolSource.isDeclaredTask(taskId))
3871
+ return;
3872
+ void this.readSubagentCost(taskId);
3873
+ }
3874
+ async readSubagentCost(taskId) {
3875
+ try {
3876
+ const transcript = this.subagentTranscriptCost(taskId);
3877
+ if (!transcript)
3878
+ return;
3879
+ const prices = await getSharedCodexPriceList(this.logger).get();
3880
+ const costUsd = prices ? await transcript.read(prices) : undefined;
3881
+ if (costUsd === undefined)
3882
+ return;
3883
+ for (const event of foldSubagentObservations(this.taskProtocolSource.observeCost(taskId, costUsd))) {
3884
+ this.notifySubscribers({ type: "provider_subagent", provider: "claude", event });
3885
+ }
3886
+ }
3887
+ catch (error) {
3888
+ this.logger.debug({ err: error, taskId }, "Failed to estimate Claude subagent cost");
3889
+ }
3890
+ }
3891
+ subagentTranscriptCost(taskId) {
3892
+ const existing = this.subagentCosts.get(taskId);
3893
+ if (existing)
3894
+ return existing;
3895
+ const historyPath = this.claudeSessionId ? this.resolveHistoryPath(this.claudeSessionId) : null;
3896
+ if (!historyPath)
3897
+ return undefined;
3898
+ // Claude Code names a subagent's transcript after the same id the task protocol calls task_id.
3899
+ const transcriptPath = path.join(claudeSubagentsDirectory(historyPath), `agent-${taskId}.jsonl`);
3900
+ // Workflows and transcript-less tasks never get one; don't fetch prices for them.
3901
+ if (!fs.existsSync(transcriptPath))
3902
+ return undefined;
3903
+ const transcript = new ClaudeTranscriptCost(transcriptPath);
3904
+ this.subagentCosts.set(taskId, transcript);
3905
+ return transcript;
3906
+ }
3854
3907
  /**
3855
3908
  * Effort is reachable only through hooks.
3856
3909
  *
@@ -4698,9 +4751,15 @@ function readClaudeReplayParentFacts(parentEntries) {
4698
4751
  };
4699
4752
  }
4700
4753
  const CLAUDE_SUBAGENT_META_FILE = /^agent-(.+)\.meta\.json$/;
4754
+ function claudeSessionDirectory(historyPath) {
4755
+ return path.join(path.dirname(historyPath), path.basename(historyPath, ".jsonl"));
4756
+ }
4757
+ function claudeSubagentsDirectory(historyPath) {
4758
+ return path.join(claudeSessionDirectory(historyPath), "subagents");
4759
+ }
4701
4760
  function readClaudeSidechainHistory(historyPath) {
4702
- const sessionDirectory = path.join(path.dirname(historyPath), path.basename(historyPath, ".jsonl"));
4703
- const sidechainDirectory = path.join(sessionDirectory, "subagents");
4761
+ const sessionDirectory = claudeSessionDirectory(historyPath);
4762
+ const sidechainDirectory = claudeSubagentsDirectory(historyPath);
4704
4763
  const history = {
4705
4764
  contents: [],
4706
4765
  workflowContents: [],
@@ -126,6 +126,8 @@ export declare class ClaudeTaskProtocolSource {
126
126
  */
127
127
  private observeTaskProgress;
128
128
  private observeUsage;
129
+ /** Cost estimated from the subagent's transcript, which the session reads as usage grows. */
130
+ observeCost(taskId: string, costUsd: number): SubagentObservation[];
129
131
  /**
130
132
  * Effort as reported by a hook firing inside a subagent.
131
133
  *
@@ -335,6 +335,13 @@ export class ClaudeTaskProtocolSource {
335
335
  return [];
336
336
  return this.updatePresentation(id, { usage });
337
337
  }
338
+ /** Cost estimated from the subagent's transcript, which the session reads as usage grows. */
339
+ observeCost(taskId, costUsd) {
340
+ const id = this.subagentIdByTaskId.get(taskId);
341
+ if (!id)
342
+ return [];
343
+ return this.updatePresentation(id, { costUsd });
344
+ }
338
345
  /**
339
346
  * Effort as reported by a hook firing inside a subagent.
340
347
  *
@@ -6,6 +6,8 @@ export interface ClaudeSubagentPresentationFacts {
6
6
  model?: string;
7
7
  effort?: string;
8
8
  usage?: ClaudeSubagentUsage;
9
+ /** Estimated from list prices; Claude reports no cost per subagent. */
10
+ costUsd?: number;
9
11
  }
10
12
  /** Build the complete compact subtitle Claude exposes to provider-neutral clients. */
11
13
  export declare function buildClaudeSubagentSubtitle(facts: ClaudeSubagentPresentationFacts): string | undefined;
@@ -1,3 +1,4 @@
1
+ import { formatSubagentCost, formatSubagentTokens, } from "../../../provider-subagents/subtitle-format.js";
1
2
  import { findClaudeModel } from "../models.js";
2
3
  /** Build the complete compact subtitle Claude exposes to provider-neutral clients. */
3
4
  export function buildClaudeSubagentSubtitle(facts) {
@@ -5,7 +6,8 @@ export function buildClaudeSubagentSubtitle(facts) {
5
6
  readPart(facts.title),
6
7
  formatModel(facts.model),
7
8
  formatEffort(facts.effort),
8
- formatTokens(facts.usage?.totalTokens),
9
+ formatSubagentTokens(facts.usage?.totalTokens),
10
+ formatSubagentCost(facts.costUsd),
9
11
  ].filter((part) => part !== undefined);
10
12
  return parts.length > 0 ? parts.join(" · ") : undefined;
11
13
  }
@@ -31,11 +33,4 @@ function formatEffort(effort) {
31
33
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
32
34
  .join(" ");
33
35
  }
34
- function formatTokens(totalTokens) {
35
- if (typeof totalTokens !== "number" || totalTokens <= 0)
36
- return undefined;
37
- if (totalTokens < 1000)
38
- return `${Math.round(totalTokens)} tokens`;
39
- return `${Math.round(totalTokens / 100) / 10}k tokens`;
40
- }
41
36
  //# sourceMappingURL=presentation.js.map
@@ -0,0 +1,18 @@
1
+ import type { CodexPrices } from "../../codex/pricing.js";
2
+ /**
3
+ * Sums the estimated cost of every model request recorded in a Claude subagent transcript.
4
+ *
5
+ * Claude Code reports only a token count per subagent, but its transcript records each request's
6
+ * model and usage split into input, cache writes, cache reads, and output, which is what a price
7
+ * needs. One request can span several entries that share a message id, so each id is priced once,
8
+ * from its latest entry.
9
+ */
10
+ export declare class ClaudeTranscriptCost {
11
+ private readonly lines;
12
+ private readonly costByMessageId;
13
+ constructor(filePath: string);
14
+ /** Returns the cost so far, or undefined when no request used a priced model. */
15
+ read(prices: CodexPrices): Promise<number | undefined>;
16
+ private applyLine;
17
+ }
18
+ //# sourceMappingURL=transcript-cost.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { AppendedLinesReader, parseRecordLine, readCount, toRecord, } from "../../appended-lines-reader.js";
2
+ /**
3
+ * Sums the estimated cost of every model request recorded in a Claude subagent transcript.
4
+ *
5
+ * Claude Code reports only a token count per subagent, but its transcript records each request's
6
+ * model and usage split into input, cache writes, cache reads, and output, which is what a price
7
+ * needs. One request can span several entries that share a message id, so each id is priced once,
8
+ * from its latest entry.
9
+ */
10
+ export class ClaudeTranscriptCost {
11
+ constructor(filePath) {
12
+ this.costByMessageId = new Map();
13
+ this.lines = new AppendedLinesReader(filePath);
14
+ }
15
+ /** Returns the cost so far, or undefined when no request used a priced model. */
16
+ async read(prices) {
17
+ await this.lines.read((line) => this.applyLine(line, prices));
18
+ if (this.costByMessageId.size === 0)
19
+ return undefined;
20
+ let total = 0;
21
+ for (const cost of this.costByMessageId.values())
22
+ total += cost;
23
+ return total;
24
+ }
25
+ applyLine(line, prices) {
26
+ // Skip user entries and tool results without parsing them.
27
+ if (!line.includes('"assistant"') || !line.includes('"usage"'))
28
+ return;
29
+ const entry = parseRecordLine(line);
30
+ if (entry?.type !== "assistant")
31
+ return;
32
+ const message = toRecord(entry.message);
33
+ const usage = toRecord(message?.usage);
34
+ if (typeof message?.id !== "string" || !usage)
35
+ return;
36
+ const cacheWrite = readCount(usage.cache_creation_input_tokens);
37
+ const cacheWrite1h = readCount(toRecord(usage.cache_creation)?.ephemeral_1h_input_tokens);
38
+ const cost = prices.claudeRequestCostUsd({
39
+ model: typeof message.model === "string" ? message.model : undefined,
40
+ inputTokens: readCount(usage.input_tokens),
41
+ cacheWrite5mTokens: Math.max(0, cacheWrite - cacheWrite1h),
42
+ cacheWrite1hTokens: cacheWrite1h,
43
+ cacheReadTokens: readCount(usage.cache_read_input_tokens),
44
+ outputTokens: readCount(usage.output_tokens),
45
+ });
46
+ if (cost !== undefined)
47
+ this.costByMessageId.set(message.id, cost);
48
+ }
49
+ }
50
+ //# sourceMappingURL=transcript-cost.js.map
@@ -6,12 +6,25 @@ export interface CodexRequestUsage {
6
6
  cachedInputTokens: number;
7
7
  outputTokens: number;
8
8
  }
9
- /** OpenAI list prices for one model request, keyed by Codex model slug. */
9
+ export interface ClaudeRequestUsage {
10
+ model: string | undefined;
11
+ inputTokens: number;
12
+ cacheWrite5mTokens: number;
13
+ cacheWrite1hTokens: number;
14
+ cacheReadTokens: number;
15
+ outputTokens: number;
16
+ }
17
+ /** List prices for one model request, keyed by Codex model slug or Claude model id. */
10
18
  export declare class CodexPrices {
11
19
  private readonly entries;
12
20
  constructor(entries: Record<string, unknown>);
13
21
  /** Returns undefined for a model missing from the price list. */
14
22
  requestCostUsd({ model, serviceTier, inputTokens, cachedInputTokens, outputTokens, }: CodexRequestUsage): number | undefined;
23
+ /**
24
+ * Returns undefined for a model missing from the price list.
25
+ * ponytail: ignores Anthropic's long-context rate, which LiteLLM lists only for some models.
26
+ */
27
+ claudeRequestCostUsd({ model, inputTokens, cacheWrite5mTokens, cacheWrite1hTokens, cacheReadTokens, outputTokens, }: ClaudeRequestUsage): number | undefined;
15
28
  }
16
29
  export interface CodexPriceListOptions {
17
30
  logger: Logger;
@@ -39,4 +52,5 @@ export declare class CodexPriceList {
39
52
  private load;
40
53
  private readSavedCopy;
41
54
  }
55
+ export declare function getSharedCodexPriceList(logger: Logger): CodexPriceList;
42
56
  //# sourceMappingURL=pricing.d.ts.map
@@ -13,7 +13,7 @@ const FETCH_TIMEOUT_MS = 15000;
13
13
  const LONG_CONTEXT_THRESHOLD_TOKENS = 272000;
14
14
  const LONG_CONTEXT_SUFFIX = "_above_272k_tokens";
15
15
  const PRIORITY_SUFFIX = "_priority";
16
- /** OpenAI list prices for one model request, keyed by Codex model slug. */
16
+ /** List prices for one model request, keyed by Codex model slug or Claude model id. */
17
17
  export class CodexPrices {
18
18
  constructor(entries) {
19
19
  this.entries = entries;
@@ -38,6 +38,31 @@ export class CodexPrices {
38
38
  cachedInputTokens * cachedInputRate +
39
39
  outputTokens * outputRate);
40
40
  }
41
+ /**
42
+ * Returns undefined for a model missing from the price list.
43
+ * ponytail: ignores Anthropic's long-context rate, which LiteLLM lists only for some models.
44
+ */
45
+ claudeRequestCostUsd({ model, inputTokens, cacheWrite5mTokens, cacheWrite1hTokens, cacheReadTokens, outputTokens, }) {
46
+ const entry = model ? toRecord(this.entries[model]) : undefined;
47
+ if (!entry)
48
+ return undefined;
49
+ const inputRate = readNumber(entry.input_cost_per_token);
50
+ const outputRate = readNumber(entry.output_cost_per_token);
51
+ if (inputRate === undefined || outputRate === undefined)
52
+ return undefined;
53
+ const cacheWrite5mRate = readNumber(entry.cache_creation_input_token_cost) ?? inputRate;
54
+ const cacheWrite1hRate = readNumber(entry.cache_creation_input_token_cost_above_1hr) ?? cacheWrite5mRate;
55
+ const cacheReadRate = readNumber(entry.cache_read_input_token_cost) ?? inputRate;
56
+ // Anthropic counts cache reads and writes apart from input tokens.
57
+ return (inputTokens * inputRate +
58
+ cacheWrite5mTokens * cacheWrite5mRate +
59
+ cacheWrite1hTokens * cacheWrite1hRate +
60
+ cacheReadTokens * cacheReadRate +
61
+ outputTokens * outputRate);
62
+ }
63
+ }
64
+ function readNumber(value) {
65
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
41
66
  }
42
67
  // Prefer the most specific rate LiteLLM lists: a model without a long-context or priority variant
43
68
  // bills that request at its plain rate.
@@ -123,6 +148,12 @@ export class CodexPriceList {
123
148
  }
124
149
  }
125
150
  }
151
+ let sharedCodexPriceList;
152
+ // One price list per daemon, so every session that estimates cost shares one fetch.
153
+ export function getSharedCodexPriceList(logger) {
154
+ sharedCodexPriceList ?? (sharedCodexPriceList = new CodexPriceList({ logger }));
155
+ return sharedCodexPriceList;
156
+ }
126
157
  function parsePriceList(text) {
127
158
  const entries = toRecord(JSON.parse(text));
128
159
  if (!entries) {
@@ -5,19 +5,16 @@ import type { CodexPrices } from "./pricing.js";
5
5
  * Codex writes each request's token counts and the model of each turn to the rollout, and the file
6
6
  * survives compaction, resume, and daemon restarts. Pricing request by request applies the
7
7
  * long-context rate and mid-session model switches, which pricing the thread total cannot.
8
- * Each read parses only the bytes appended since the previous read.
9
8
  */
10
9
  export declare class CodexRolloutCost {
11
- private readonly filePath;
12
- private offset;
13
- private readonly decoder;
14
- private pendingLine;
15
- private model;
10
+ private readonly lines;
11
+ private currentModel;
16
12
  private highestTotalTokens;
17
13
  private costUsd;
18
14
  private pricedRequests;
19
- private queue;
20
15
  constructor(filePath: string);
16
+ /** The model of the latest turn read so far. */
17
+ get model(): string | undefined;
21
18
  /**
22
19
  * Returns the session cost so far, or undefined when no request used a priced model.
23
20
  * ponytail: prices every request at the current service tier; the rollout does not record it.
@@ -26,7 +23,6 @@ export declare class CodexRolloutCost {
26
23
  prices: CodexPrices;
27
24
  serviceTier: string | null;
28
25
  }): Promise<number | undefined>;
29
- private readAppended;
30
26
  private applyLine;
31
27
  }
32
28
  //# sourceMappingURL=rollout-cost.d.ts.map
@@ -1,62 +1,39 @@
1
- import fs from "node:fs/promises";
2
- import { StringDecoder } from "node:string_decoder";
1
+ import { AppendedLinesReader, parseRecordLine, readCount, toRecord, } from "../appended-lines-reader.js";
3
2
  /**
4
3
  * Sums the estimated cost of every model request recorded in a Codex rollout file.
5
4
  *
6
5
  * Codex writes each request's token counts and the model of each turn to the rollout, and the file
7
6
  * survives compaction, resume, and daemon restarts. Pricing request by request applies the
8
7
  * long-context rate and mid-session model switches, which pricing the thread total cannot.
9
- * Each read parses only the bytes appended since the previous read.
10
8
  */
11
9
  export class CodexRolloutCost {
12
10
  constructor(filePath) {
13
- this.filePath = filePath;
14
- this.offset = 0;
15
- this.decoder = new StringDecoder("utf8");
16
- this.pendingLine = "";
17
11
  this.highestTotalTokens = -1;
18
12
  this.costUsd = 0;
19
13
  this.pricedRequests = 0;
20
- this.queue = Promise.resolve();
14
+ this.lines = new AppendedLinesReader(filePath);
15
+ }
16
+ /** The model of the latest turn read so far. */
17
+ get model() {
18
+ return this.currentModel;
21
19
  }
22
20
  /**
23
21
  * Returns the session cost so far, or undefined when no request used a priced model.
24
22
  * ponytail: prices every request at the current service tier; the rollout does not record it.
25
23
  */
26
- read({ prices, serviceTier, }) {
27
- const result = this.queue.then(() => this.readAppended(prices, serviceTier));
28
- this.queue = result.catch(() => undefined);
29
- return result;
30
- }
31
- async readAppended(prices, serviceTier) {
32
- const handle = await fs.open(this.filePath, "r");
33
- try {
34
- const { size } = await handle.stat();
35
- if (size > this.offset) {
36
- const buffer = Buffer.alloc(size - this.offset);
37
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, this.offset);
38
- this.offset += bytesRead;
39
- const lines = (this.pendingLine + this.decoder.write(buffer.subarray(0, bytesRead))).split("\n");
40
- this.pendingLine = lines.pop() ?? "";
41
- for (const line of lines) {
42
- this.applyLine(line, prices, serviceTier);
43
- }
44
- }
45
- }
46
- finally {
47
- await handle.close();
48
- }
24
+ async read({ prices, serviceTier, }) {
25
+ await this.lines.read((line) => this.applyLine(line, prices, serviceTier));
49
26
  return this.pricedRequests > 0 ? this.costUsd : undefined;
50
27
  }
51
28
  applyLine(line, prices, serviceTier) {
52
29
  // Skip the bulk of the rollout (messages, tool output) without parsing it.
53
30
  if (!line.includes('"turn_context"') && !line.includes('"token_count"'))
54
31
  return;
55
- const entry = parseRecord(line);
32
+ const entry = parseRecordLine(line);
56
33
  const payload = toRecord(entry?.payload);
57
34
  if (entry?.type === "turn_context") {
58
35
  if (typeof payload?.model === "string")
59
- this.model = payload.model;
36
+ this.currentModel = payload.model;
60
37
  return;
61
38
  }
62
39
  if (payload?.type !== "token_count")
@@ -71,7 +48,7 @@ export class CodexRolloutCost {
71
48
  return;
72
49
  this.highestTotalTokens = totalTokens;
73
50
  const cost = prices.requestCostUsd({
74
- model: this.model,
51
+ model: this.currentModel,
75
52
  serviceTier,
76
53
  inputTokens: readCount(last.input_tokens),
77
54
  cachedInputTokens: readCount(last.cached_input_tokens),
@@ -83,21 +60,4 @@ export class CodexRolloutCost {
83
60
  this.pricedRequests += 1;
84
61
  }
85
62
  }
86
- function parseRecord(line) {
87
- try {
88
- return toRecord(JSON.parse(line));
89
- }
90
- catch {
91
- // A torn or foreign line only drops that line from an estimate.
92
- return undefined;
93
- }
94
- }
95
- function toRecord(value) {
96
- return typeof value === "object" && value !== null && !Array.isArray(value)
97
- ? value
98
- : undefined;
99
- }
100
- function readCount(value) {
101
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
102
- }
103
63
  //# sourceMappingURL=rollout-cost.js.map
@@ -2,7 +2,7 @@ import { type AgentCapabilityFlags, type AgentClient, type AgentCreateSessionOpt
2
2
  import type { Logger } from "pino";
3
3
  import type { ChildProcessWithoutNullStreams } from "node:child_process";
4
4
  import { Dirent } from "node:fs";
5
- import { CodexPriceList } from "./codex/pricing.js";
5
+ import { type CodexPriceList } from "./codex/pricing.js";
6
6
  import { type ProviderRuntimeSettings } from "../provider-launch-config.js";
7
7
  import { type CodexThreadForkParams, type CodexThreadForkResponse, type CodexThreadRollbackParams, type CodexThreadRollbackResponse, type CodexAppServerTraceContext } from "./codex/app-server-transport.js";
8
8
  import type { WorkspaceGitService } from "../../workspace-git-service.js";
@@ -173,6 +173,7 @@ export declare class CodexAppServerAgentSession implements AgentSession {
173
173
  private emittedProviderSubagentUserMessageKeys;
174
174
  private subAgentCallsByCallId;
175
175
  private subAgentCallIdByChildThreadId;
176
+ private subAgentUsageByThreadId;
176
177
  private pendingSubAgentNotificationsByThreadId;
177
178
  private warnedUnknownNotificationMethods;
178
179
  private warnedInvalidNotificationPayloads;
@@ -336,6 +337,9 @@ export declare class CodexAppServerAgentSession implements AgentSession {
336
337
  private resetTurnTrackingState;
337
338
  private handlePlanUpdatedNotification;
338
339
  private handleTokenUsageUpdatedNotification;
340
+ private handleSubAgentTokenUsage;
341
+ private refreshSubAgentCost;
342
+ private emitSubAgentSubtitle;
339
343
  private refreshRolloutCost;
340
344
  private applyCostEstimate;
341
345
  private openRolloutCost;
@@ -11,8 +11,9 @@ import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
11
11
  import { composeSystemPromptParts } from "../system-prompt.js";
12
12
  import { curateAgentActivity } from "../activity-curator.js";
13
13
  import { CodexAsyncQuestions, codexAsyncQuestionToTimeline } from "./codex/async-questions.js";
14
- import { CodexPriceList } from "./codex/pricing.js";
14
+ import { getSharedCodexPriceList } from "./codex/pricing.js";
15
15
  import { CodexRolloutCost } from "./codex/rollout-cost.js";
16
+ import { formatSubagentCost, formatSubagentTokens } from "../provider-subagents/subtitle-format.js";
16
17
  import { mapCodexToolCallEnvelope, mapCodexToolCallFromThreadItem, normalizeCommandExecutionCommand, splitCodexMcpToolResultImages, } from "./codex/tool-call-mapper.js";
17
18
  import { checkProviderLaunchAvailable, createProviderEnv, createProviderEnvSpec, resolveProviderLaunch, } from "../provider-launch-config.js";
18
19
  import { findExecutable, probeExecutable, } from "../../../executable-resolution/executable-resolution.js";
@@ -723,12 +724,6 @@ function filterCodexThreadsByCwd(threads, cwd) {
723
724
  const belongsToWorkspace = createPathEquivalenceMatcher(cwd);
724
725
  return threads.filter((thread) => typeof thread.cwd === "string" && belongsToWorkspace(thread.cwd));
725
726
  }
726
- let sharedCodexPriceList;
727
- // One price list per daemon, so every Codex session and custom Codex provider shares one fetch.
728
- function getSharedCodexPriceList(logger) {
729
- sharedCodexPriceList ?? (sharedCodexPriceList = new CodexPriceList({ logger }));
730
- return sharedCodexPriceList;
731
- }
732
727
  var CodexCostStatus;
733
728
  (function (CodexCostStatus) {
734
729
  /** No priced request yet, an unpriced model, or no rollout file. */
@@ -750,16 +745,31 @@ function isSameCostEstimate(left, right) {
750
745
  }
751
746
  return left.status === right.status;
752
747
  }
753
- // Both cost fields are always written, undefined included, so the agent manager's usage merge on
748
+ // All cost fields are always written, undefined included, so the agent manager's usage merge on
754
749
  // turn completion cannot keep a stale value from the previous state.
755
750
  function withCostEstimate(usage, estimate) {
756
751
  switch (estimate.status) {
757
752
  case CodexCostStatus.Estimated:
758
- return { ...usage, totalCostUsd: estimate.costUsd, totalCostUnavailable: false };
753
+ return {
754
+ ...usage,
755
+ totalCostUsd: estimate.costUsd,
756
+ totalCostUnavailable: false,
757
+ totalCostEstimated: true,
758
+ };
759
759
  case CodexCostStatus.Unavailable:
760
- return { ...usage, totalCostUsd: undefined, totalCostUnavailable: true };
760
+ return {
761
+ ...usage,
762
+ totalCostUsd: undefined,
763
+ totalCostUnavailable: true,
764
+ totalCostEstimated: undefined,
765
+ };
761
766
  case CodexCostStatus.Pending:
762
- return { ...usage, totalCostUsd: undefined, totalCostUnavailable: undefined };
767
+ return {
768
+ ...usage,
769
+ totalCostUsd: undefined,
770
+ totalCostUnavailable: undefined,
771
+ totalCostEstimated: undefined,
772
+ };
763
773
  }
764
774
  }
765
775
  export function toAgentUsage(tokenUsage) {
@@ -2598,6 +2608,7 @@ export class CodexAppServerAgentSession {
2598
2608
  this.emittedProviderSubagentUserMessageKeys = new Set();
2599
2609
  this.subAgentCallsByCallId = new Map();
2600
2610
  this.subAgentCallIdByChildThreadId = new Map();
2611
+ this.subAgentUsageByThreadId = new Map();
2601
2612
  this.pendingSubAgentNotificationsByThreadId = new Map();
2602
2613
  this.warnedUnknownNotificationMethods = new Set();
2603
2614
  this.warnedInvalidNotificationPayloads = new Set();
@@ -3000,6 +3011,7 @@ export class CodexAppServerAgentSession {
3000
3011
  const { timeline, subAgentRoutes } = history;
3001
3012
  this.subAgentCallsByCallId.clear();
3002
3013
  this.subAgentCallIdByChildThreadId.clear();
3014
+ this.subAgentUsageByThreadId.clear();
3003
3015
  this.pendingSubAgentNotificationsByThreadId.clear();
3004
3016
  this.persistedProviderSubagentEvents = [];
3005
3017
  this.loadingPersistedHistory = true;
@@ -4271,6 +4283,9 @@ export class CodexAppServerAgentSession {
4271
4283
  }
4272
4284
  dispatchSubAgentNotification(parsed, callId) {
4273
4285
  switch (parsed.kind) {
4286
+ case "token_usage_updated":
4287
+ this.handleSubAgentTokenUsage(parsed);
4288
+ return;
4274
4289
  case "thread_started":
4275
4290
  this.emitSubAgentActivityUpdate(callId, "running", { reopen: true });
4276
4291
  return;
@@ -4947,6 +4962,62 @@ export class CodexAppServerAgentSession {
4947
4962
  }
4948
4963
  void this.refreshRolloutCost();
4949
4964
  }
4965
+ handleSubAgentTokenUsage(parsed) {
4966
+ const threadId = parsed.threadId;
4967
+ if (!threadId)
4968
+ return;
4969
+ let usage = this.subAgentUsageByThreadId.get(threadId);
4970
+ if (!usage) {
4971
+ usage = {};
4972
+ this.subAgentUsageByThreadId.set(threadId, usage);
4973
+ }
4974
+ // The last request's context, which is what the Claude subagent rows report as tokens too.
4975
+ usage.tokens = toAgentUsage(parsed.tokenUsage)?.contextWindowUsedTokens;
4976
+ this.emitSubAgentSubtitle(threadId, usage);
4977
+ void this.refreshSubAgentCost(threadId, usage);
4978
+ }
4979
+ async refreshSubAgentCost(threadId, usage) {
4980
+ const priceList = this.deps.codexPriceList;
4981
+ if (!priceList)
4982
+ return;
4983
+ try {
4984
+ usage.rolloutCost ?? (usage.rolloutCost = this.openRolloutCost(threadId));
4985
+ const rolloutCost = await usage.rolloutCost;
4986
+ const prices = await priceList.get();
4987
+ if (!rolloutCost || !prices)
4988
+ return;
4989
+ usage.costUsd = await rolloutCost.read({ prices, serviceTier: this.serviceTier });
4990
+ usage.model = rolloutCost.model;
4991
+ if (this.subAgentUsageByThreadId.get(threadId) !== usage)
4992
+ return;
4993
+ this.emitSubAgentSubtitle(threadId, usage);
4994
+ }
4995
+ catch (error) {
4996
+ usage.rolloutCost = undefined;
4997
+ this.logger.warn({ error, threadId }, "Failed to read a Codex subagent rollout for its cost");
4998
+ }
4999
+ }
5000
+ emitSubAgentSubtitle(threadId, usage) {
5001
+ // A provider subtitle replaces the row's agent type, so it leads with it.
5002
+ const callId = this.subAgentCallIdByChildThreadId.get(threadId);
5003
+ const detail = callId ? this.subAgentCallsByCallId.get(callId)?.toolCall.detail : undefined;
5004
+ const subtitle = [
5005
+ detail?.type === "sub_agent" ? (detail.subAgentType ?? "Codex subagent") : undefined,
5006
+ usage.model,
5007
+ formatSubagentTokens(usage.tokens),
5008
+ formatSubagentCost(usage.costUsd),
5009
+ ]
5010
+ .filter((part) => part !== undefined)
5011
+ .join(" · ");
5012
+ if (!subtitle || subtitle === usage.subtitle)
5013
+ return;
5014
+ usage.subtitle = subtitle;
5015
+ this.emitEvent({
5016
+ type: "provider_subagent",
5017
+ provider: CODEX_PROVIDER,
5018
+ event: { type: "upsert", id: threadId, subtitle },
5019
+ });
5020
+ }
4950
5021
  async refreshRolloutCost() {
4951
5022
  const threadId = this.currentThreadId;
4952
5023
  const priceList = this.deps.codexPriceList;
@@ -3,12 +3,26 @@ export interface WorkingTreeSnapshot {
3
3
  repoRoot: string;
4
4
  tree: string;
5
5
  }
6
+ export type TurnStartSnapshot = WorkingTreeSnapshot & {
7
+ head?: string;
8
+ headLogPath: string;
9
+ headLogSize: number;
10
+ };
6
11
  /**
7
12
  * Writes the whole working tree (tracked changes plus untracked, non-ignored files up to 1 MiB)
8
13
  * as a git tree object. Staging goes through a copy of the index, so the user's index, HEAD and
9
14
  * branches are untouched; only git objects are written.
10
15
  */
11
16
  export declare function snapshotWorkingTree(cwd: string): Promise<WorkingTreeSnapshot | null>;
17
+ /** A working tree snapshot plus where HEAD and its reflog stood, to separate out branch moves. */
18
+ export declare function snapshotTurnStart(cwd: string): Promise<TurnStartSnapshot | null>;
19
+ /**
20
+ * The tree to diff a turn from. When the turn moved HEAD to another commit, the start snapshot's
21
+ * uncommitted changes are replayed onto where HEAD landed, so the diff holds the agent's edits and
22
+ * commits and none of the content the move brought in. Undefined when that can't be worked out
23
+ * (no reflog, unborn branch, replay conflict); the turn then gets no diff.
24
+ */
25
+ export declare function resolveTurnFromTree(start: TurnStartSnapshot): Promise<string | undefined>;
12
26
  export declare function diffTreeStats(params: {
13
27
  repoRoot: string;
14
28
  fromTree: string;