replicas-engine 0.1.699 → 0.1.701

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/dist/src/index.js CHANGED
@@ -68,6 +68,7 @@ import {
68
68
  MEMORY_ROOT,
69
69
  MEMORY_SUMMARY_FILENAME,
70
70
  MERGED_MESSAGE_SEPARATOR,
71
+ MODEL_REROUTED_METHOD,
71
72
  REASONING_SUMMARY_PART_ADDED_METHOD,
72
73
  REASONING_SUMMARY_TEXT_DELTA_METHOD,
73
74
  REASONING_TEXT_DELTA_METHOD,
@@ -80,6 +81,7 @@ import {
80
81
  THREAD_GOAL_UPDATED_METHOD,
81
82
  THREAD_TOKEN_USAGE_UPDATED_METHOD,
82
83
  TIMEOUT,
84
+ TOKEN_USAGE_PATH,
83
85
  TURN_COMPLETED_METHOD,
84
86
  TURN_PLAN_UPDATED_METHOD,
85
87
  TURN_STARTED_METHOD,
@@ -116,6 +118,7 @@ import {
116
118
  createErrorResult,
117
119
  createProviderSlashCommand,
118
120
  createSuccessResult,
121
+ createUuidFromHex,
119
122
  decodePathSegments,
120
123
  detectAgentQuotaLimit,
121
124
  detectLanguageByPath,
@@ -126,6 +129,7 @@ import {
126
129
  extractErrorText,
127
130
  extractToolResultText,
128
131
  fetchAiGatewayModels,
132
+ fetchModelsDevCatalog,
129
133
  findGitCommitSignals,
130
134
  findPrMergeSignals,
131
135
  getChatHistoryPageSenders,
@@ -159,9 +163,12 @@ import {
159
163
  isDefaultChat,
160
164
  isGitHubUrl,
161
165
  isLatestChatHistoryPage,
166
+ isNonEmptyString,
162
167
  isRecord,
163
168
  isSkillRegistryManifest,
164
169
  isTerminalBackgroundTaskStatus,
170
+ isTokenUsageCounts,
171
+ isTokenUsageUploadRecord,
165
172
  isTransientErrorText,
166
173
  isValidAgentProvider,
167
174
  isValidEngineLogSessionId,
@@ -191,15 +198,16 @@ import {
191
198
  renumberCodexAspTranscriptItems,
192
199
  resolveWarmHookConfig,
193
200
  sanitizeCanvasFilename,
201
+ sendChatMessageRequestSchema,
194
202
  serializeCanvasContentResponse,
195
203
  shellQuotePosix,
196
204
  stripAgentDiagnosticErrors
197
- } from "./chunk-UTGTXG54.js";
205
+ } from "./chunk-OI5T44T7.js";
198
206
 
199
207
  // src/index.ts
200
208
  import { serve } from "@hono/node-server";
201
209
  import { Hono as Hono2 } from "hono";
202
- import { existsSync as existsSync11 } from "fs";
210
+ import { existsSync as existsSync12 } from "fs";
203
211
  import { randomUUID as randomUUID11 } from "crypto";
204
212
  import { connect } from "net";
205
213
 
@@ -2814,7 +2822,7 @@ async function registerDesktopPreview() {
2814
2822
  }
2815
2823
 
2816
2824
  // src/services/chat/chat-service.ts
2817
- import { existsSync as existsSync8 } from "fs";
2825
+ import { existsSync as existsSync9 } from "fs";
2818
2826
  import { appendFile as appendFile4, copyFile, mkdir as mkdir17, readFile as readFile17, rename as rename3, rm as rm2 } from "fs/promises";
2819
2827
  import { join as join29 } from "path";
2820
2828
  import { randomUUID as randomUUID9 } from "crypto";
@@ -3741,6 +3749,7 @@ var CodingAgentManager = class {
3741
3749
  initialSessionId;
3742
3750
  onSaveSessionId;
3743
3751
  onEvent;
3752
+ onUsageUpdate;
3744
3753
  hostOnTurnComplete;
3745
3754
  onProcessingChanged;
3746
3755
  onMessageStarted;
@@ -3751,6 +3760,8 @@ var CodingAgentManager = class {
3751
3760
  this.initialSessionId = options.initialSessionId;
3752
3761
  this.onSaveSessionId = options.onSaveSessionId;
3753
3762
  this.onEvent = options.onEvent;
3763
+ this.onUsageUpdate = options.onUsageUpdate ?? (() => {
3764
+ });
3754
3765
  this.hostOnTurnComplete = options.onTurnComplete;
3755
3766
  this.onProcessingChanged = options.onProcessingChanged ?? (() => {
3756
3767
  });
@@ -6738,9 +6749,17 @@ var CodexAspManager = class extends CodingAgentManager {
6738
6749
  slashCommandsRequest = null;
6739
6750
  steeredTempImagePaths = [];
6740
6751
  steerLock = new AsyncLock();
6752
+ onCodexTurnStarted;
6753
+ onCodexTokenUsage;
6754
+ onCodexModelRerouted;
6755
+ onCodexTurnCompleted;
6741
6756
  constructor(options) {
6742
6757
  super({ ...options, provider: AGENT.CODEX });
6743
6758
  this.historyFile = options.historyFilePath ? new CodexHistoryFile(options.historyFilePath) : null;
6759
+ this.onCodexTurnStarted = options.onCodexTurnStarted;
6760
+ this.onCodexTokenUsage = options.onCodexTokenUsage;
6761
+ this.onCodexModelRerouted = options.onCodexModelRerouted;
6762
+ this.onCodexTurnCompleted = options.onCodexTurnCompleted;
6744
6763
  this.initializeManager(this.processMessageInternal.bind(this));
6745
6764
  }
6746
6765
  async initialize() {
@@ -7261,7 +7280,13 @@ var CodexAspManager = class extends CodingAgentManager {
7261
7280
  },
7262
7281
  [THREAD_TOKEN_USAGE_UPDATED_METHOD]: (notification) => {
7263
7282
  if (notification.params.threadId !== threadId) return;
7264
- this.emitCodexTokenUsage(notification.params.tokenUsage, model);
7283
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
7284
+ this.onCodexTokenUsage?.(notification.params, occurredAt);
7285
+ this.emitCodexTokenUsage(notification.params.tokenUsage, model, occurredAt);
7286
+ },
7287
+ [MODEL_REROUTED_METHOD]: (notification) => {
7288
+ if (!matchesTurn(notification.params.threadId, notification.params.turnId)) return;
7289
+ this.onCodexModelRerouted?.(notification.params);
7265
7290
  },
7266
7291
  [THREAD_GOAL_UPDATED_METHOD]: (notification) => {
7267
7292
  if (notification.params.threadId !== threadId) return;
@@ -7282,6 +7307,7 @@ var CodexAspManager = class extends CodingAgentManager {
7282
7307
  observedTurnId = notification.params.turn.id;
7283
7308
  observedTurnIds.add(notification.params.turn.id);
7284
7309
  this.activeTurnId = notification.params.turn.id;
7310
+ this.onCodexTurnStarted?.(threadId, notification.params.turn.id);
7285
7311
  if (goalContinuationTimer) {
7286
7312
  clearTimeout(goalContinuationTimer);
7287
7313
  goalContinuationTimer = null;
@@ -7390,6 +7416,7 @@ var CodexAspManager = class extends CodingAgentManager {
7390
7416
  if (!observedTurnIds.has(notification.params.turn.id) && notification.params.turn.id !== this.activeTurnId) return;
7391
7417
  observedTurnIds.add(notification.params.turn.id);
7392
7418
  observedTurnId = notification.params.turn.id;
7419
+ this.onCodexTurnCompleted?.(threadId, notification.params.turn.id);
7393
7420
  const completedTurn = recoverCompletedTurn(notification.params.turn, completedItems, agentMessageDeltas);
7394
7421
  this.mergeTranscriptTurn(notification.params.threadId, completedTurn);
7395
7422
  this.emitTranscriptUpdated(notification.params.threadId, { immediate: true });
@@ -7432,6 +7459,7 @@ var CodexAspManager = class extends CodingAgentManager {
7432
7459
  observedTurnId = started.turn.id;
7433
7460
  observedTurnIds.add(started.turn.id);
7434
7461
  this.activeTurnId = started.turn.id;
7462
+ if (!alreadyObserved) this.onCodexTurnStarted?.(threadId, started.turn.id);
7435
7463
  if (!alreadyObserved) {
7436
7464
  attachUserMessage(started.turn);
7437
7465
  this.mergeTranscriptTurn(threadId, started.turn);
@@ -8015,7 +8043,7 @@ var CodexAspManager = class extends CodingAgentManager {
8015
8043
  this.trackHistoryEvent(event);
8016
8044
  this.onEvent(event);
8017
8045
  }
8018
- emitCodexTokenUsage(tokenUsage, model) {
8046
+ emitCodexTokenUsage(tokenUsage, model, updatedAt) {
8019
8047
  const payload = buildCodexTokenUsageContextUsagePayload({
8020
8048
  model,
8021
8049
  modelContextWindow: tokenUsage.modelContextWindow,
@@ -8029,7 +8057,7 @@ var CodexAspManager = class extends CodingAgentManager {
8029
8057
  total: {
8030
8058
  totalTokens: tokenUsage.total.totalTokens
8031
8059
  },
8032
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
8060
+ updatedAt
8033
8061
  });
8034
8062
  const event = this.emitContextUsage(payload);
8035
8063
  this.trackHistoryEvent(event);
@@ -8469,6 +8497,7 @@ ${instructions}
8469
8497
  const outputTokens = finiteNumber(update.usage.outputTokens);
8470
8498
  const rawTotalTokens = inputTokens + cacheReadTokens + cacheWriteTokens + outputTokens;
8471
8499
  if (rawTotalTokens <= 0) return;
8500
+ this.onUsageUpdate(update, (/* @__PURE__ */ new Date()).toISOString());
8472
8501
  const maxTokens = CURSOR_COMPOSER_CONTEXT_WINDOW;
8473
8502
  const { totalTokens, totalProcessedTokens } = clampTokensToWindow(rawTotalTokens, maxTokens);
8474
8503
  const categories = [
@@ -8811,8 +8840,10 @@ var DeepseekManager = class extends CodingAgentManager {
8811
8840
  if (frame.type === "session/projection") {
8812
8841
  if (frame.key === "contextPressure" && isRecord2(frame.value)) this.contextPressure = frame.value;
8813
8842
  else if (frame.key === "contextBreakdown" && isRecord2(frame.value)) this.contextBreakdown = frame.value;
8814
- else if (frame.key === "tokenUsage" && isRecord2(frame.value)) this.tokenUsage = frame.value;
8815
- else return;
8843
+ else if (frame.key === "tokenUsage" && isRecord2(frame.value)) {
8844
+ this.tokenUsage = frame.value;
8845
+ this.onUsageUpdate(frame.value, (/* @__PURE__ */ new Date()).toISOString());
8846
+ } else return;
8816
8847
  this.emitDeepseekContextUsage();
8817
8848
  return;
8818
8849
  }
@@ -9162,6 +9193,7 @@ var AcpManager = class extends CodingAgentManager {
9162
9193
  } else if (update.sessionUpdate === "current_mode_update" && this.modes) {
9163
9194
  this.modes = { ...this.modes, currentModeId: update.currentModeId };
9164
9195
  } else if (update.sessionUpdate === "usage_update" && !this.replaying) {
9196
+ const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
9165
9197
  const { used, size } = update;
9166
9198
  this.historyFile.append(this.emitContextUsage({
9167
9199
  provider: this.acp.provider,
@@ -9171,7 +9203,7 @@ var AcpManager = class extends CodingAgentManager {
9171
9203
  maxTokens: size,
9172
9204
  percentage: size > 0 ? used / size * 100 : 0,
9173
9205
  categories: [],
9174
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9206
+ updatedAt: occurredAt
9175
9207
  }));
9176
9208
  }
9177
9209
  if (!this.replaying) {
@@ -9458,59 +9490,27 @@ import { delimiter, dirname as dirname8, join as join22 } from "path";
9458
9490
  import { randomBytes as randomBytes2 } from "crypto";
9459
9491
  import { fileURLToPath as fileURLToPath2 } from "url";
9460
9492
  import { Agent } from "undici";
9461
- import { z as z4 } from "zod";
9493
+ import { z as z3 } from "zod";
9462
9494
 
9463
9495
  // ../shared/src/credentials/opencode-go.ts
9464
- import { z as z3 } from "zod";
9465
9496
  var OPENCODE_GO_PROVIDER = "opencode-go";
9466
9497
  var OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
9467
- var OPENCODE_MODELS_DEV_URL = "https://models.dev/api.json";
9468
9498
  var DEFAULT_OPENCODE_GO_MODEL = "glm-5.2";
9469
- var opencodeGoModelsDevSchema = z3.object({
9470
- "opencode-go": z3.object({
9471
- models: z3.record(z3.string(), z3.object({
9472
- id: z3.string().optional(),
9473
- name: z3.string(),
9474
- description: z3.string().optional(),
9475
- status: z3.enum(["alpha", "beta", "deprecated"]).optional()
9476
- }))
9477
- }).optional()
9478
- });
9479
- function parseOpenCodeGoCatalog(value) {
9480
- const parsed = opencodeGoModelsDevSchema.safeParse(value);
9481
- const provider = parsed.success ? parsed.data["opencode-go"] : void 0;
9482
- if (!provider) return null;
9483
- const models = Object.fromEntries(
9484
- Object.entries(provider.models).filter(([, definition]) => definition.status !== "deprecated" && definition.status !== "alpha").map(([id, definition]) => [id, {
9485
- id: definition.id ?? id,
9486
- name: definition.name,
9487
- ...definition.description ? { description: definition.description } : {},
9488
- ...definition.status ? { status: definition.status } : {}
9489
- }])
9490
- );
9491
- if (Object.keys(models).length === 0) return null;
9492
- return { models, authoritative: true };
9493
- }
9494
- var OPENCODE_GO_CATALOG_CACHE_MS = 5 * 6e4;
9495
- var openCodeGoCatalogCache;
9496
- var openCodeGoCatalogRequest;
9497
9499
  async function fetchOpenCodeGoCatalog(fallbackModelIds = [DEFAULT_OPENCODE_GO_MODEL]) {
9498
- if (openCodeGoCatalogCache && openCodeGoCatalogCache.expiresAt > Date.now()) {
9499
- return openCodeGoCatalogCache.catalog;
9500
- }
9501
- openCodeGoCatalogRequest ??= (async () => {
9502
- const response = await fetch(OPENCODE_MODELS_DEV_URL, { signal: AbortSignal.timeout(5e3) });
9503
- if (!response.ok) throw new Error(`Failed to load OpenCode Go models (${response.status})`);
9504
- const catalog = parseOpenCodeGoCatalog(await response.json());
9505
- if (!catalog) throw new Error("OpenCode Go model catalog is unavailable");
9506
- return catalog;
9507
- })();
9508
9500
  try {
9509
- const catalog = await openCodeGoCatalogRequest;
9510
- openCodeGoCatalogCache = { catalog, expiresAt: Date.now() + OPENCODE_GO_CATALOG_CACHE_MS };
9511
- return catalog;
9501
+ const provider = (await fetchModelsDevCatalog())[OPENCODE_GO_PROVIDER];
9502
+ if (!provider) throw new Error("OpenCode Go model catalog is unavailable");
9503
+ const models = Object.fromEntries(
9504
+ Object.entries(provider.models).filter(([, definition]) => definition.status !== "deprecated" && definition.status !== "alpha").map(([id, definition]) => [id, {
9505
+ id: definition.id ?? id,
9506
+ name: definition.name,
9507
+ ...definition.description ? { description: definition.description } : {},
9508
+ ...definition.status ? { status: definition.status } : {}
9509
+ }])
9510
+ );
9511
+ if (Object.keys(models).length === 0) throw new Error("OpenCode Go model catalog is unavailable");
9512
+ return { models, authoritative: true };
9512
9513
  } catch {
9513
- if (openCodeGoCatalogCache) return openCodeGoCatalogCache.catalog;
9514
9514
  return {
9515
9515
  authoritative: false,
9516
9516
  models: Object.fromEntries(fallbackModelIds.map((id) => [id, {
@@ -9518,8 +9518,6 @@ async function fetchOpenCodeGoCatalog(fallbackModelIds = [DEFAULT_OPENCODE_GO_MO
9518
9518
  name: id === DEFAULT_OPENCODE_GO_MODEL ? "GLM 5.2" : id
9519
9519
  }]))
9520
9520
  };
9521
- } finally {
9522
- openCodeGoCatalogRequest = void 0;
9523
9521
  }
9524
9522
  }
9525
9523
 
@@ -9578,9 +9576,9 @@ var OPENCODE_VARIANT_CANDIDATES_BY_THINKING_LEVEL = {
9578
9576
  ultra: ["max", "xhigh", "high"],
9579
9577
  ultracode: ["max", "xhigh", "high"]
9580
9578
  };
9581
- var opencodeAuthSchema = z4.record(z4.string(), z4.object({
9582
- type: z4.string().optional(),
9583
- key: z4.string().optional()
9579
+ var opencodeAuthSchema = z3.record(z3.string(), z3.object({
9580
+ type: z3.string().optional(),
9581
+ key: z3.string().optional()
9584
9582
  }));
9585
9583
  async function hasOpencodeCredentials(provider) {
9586
9584
  if (!existsSync7(OPENCODE_AUTH_PATH)) return false;
@@ -10311,6 +10309,7 @@ var OpencodeManager = class extends CodingAgentManager {
10311
10309
  this.activeLinearForwarder.sendEvent(event);
10312
10310
  }
10313
10311
  recordOpencodePart(part) {
10312
+ if (part.type === "step-finish") this.onUsageUpdate(part, (/* @__PURE__ */ new Date()).toISOString());
10314
10313
  this.recordHistoryEvent(`opencode-part-${part.type}`, { part }, this.historyFile);
10315
10314
  this.forwardOpencodePartToLinear(part);
10316
10315
  }
@@ -10565,6 +10564,7 @@ var PiManager = class extends CodingAgentManager {
10565
10564
  return null;
10566
10565
  }
10567
10566
  handleEvent(event) {
10567
+ if (event.type === "message_end") this.onUsageUpdate(event, (/* @__PURE__ */ new Date()).toISOString());
10568
10568
  const payload = eventPayload(event);
10569
10569
  this.recordHistoryEvent(`pi-${event.type}`, payload, this.historyFile);
10570
10570
  }
@@ -10597,7 +10597,7 @@ var PiManager = class extends CodingAgentManager {
10597
10597
 
10598
10598
  // src/managers/relay-tools.ts
10599
10599
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
10600
- import { z as z5 } from "zod";
10600
+ import { z as z4 } from "zod";
10601
10601
 
10602
10602
  // src/managers/relay-providers.ts
10603
10603
  function isRelaySubagentProviderAllowed(provider, allowedProviders) {
@@ -10727,7 +10727,7 @@ async function getChatFinalResponse(chatId) {
10727
10727
  return extractFinalResponse(history) ?? "[No response from subagent]";
10728
10728
  }
10729
10729
  function buildSpawnAgentTool(parentChatId, availability = {}, getAllowedProviders = () => monolithService.getRelaySubagentProviders(), getProviderAvailability = () => availability) {
10730
- const providerEnum = z5.enum(VALID_RELAY_SUBAGENT_PROVIDERS);
10730
+ const providerEnum = z4.enum(VALID_RELAY_SUBAGENT_PROVIDERS);
10731
10731
  return tool(
10732
10732
  "spawn_agent",
10733
10733
  `Spawn a new subagent to perform a task. The subagent runs in its own chat with a fresh context window.
@@ -10742,8 +10742,8 @@ The tool blocks until the subagent completes and returns its final response.
10742
10742
  You will also receive the chatId so you can send follow-up messages or clean up the chat.`,
10743
10743
  {
10744
10744
  provider: providerEnum.describe("Which agent to use. Choose from the providers listed as currently available in Relay guidance. Prefer an available code provider for code writing, claude for exploration/analysis, and relay for complex multi-step orchestration."),
10745
- prompt: z5.string().describe("The full prompt/instructions for the subagent. Be detailed - it has no context from your conversation."),
10746
- model: z5.string().optional().describe([
10745
+ prompt: z4.string().describe("The full prompt/instructions for the subagent. Be detailed - it has no context from your conversation."),
10746
+ model: z4.string().optional().describe([
10747
10747
  `Model override. Claude: ${AGENT_MODELS.claude.join(", ")} (opus is the default; sonnet is faster).`,
10748
10748
  `Codex: ${AGENT_MODELS.codex.join(", ")}.`,
10749
10749
  `Cursor: ${AGENT_MODELS.cursor.join(", ")}.`,
@@ -10753,11 +10753,11 @@ You will also receive the chatId so you can send follow-up messages or clean up
10753
10753
  `Opencode: ${AGENT_MODELS.opencode.join(", ")}.`,
10754
10754
  `Pi: ${AGENT_MODELS.pi.join(", ")}.`
10755
10755
  ].join(" ")),
10756
- thinking_level: z5.enum(VALID_THINKING_LEVELS).optional().describe(
10756
+ thinking_level: z4.enum(VALID_THINKING_LEVELS).optional().describe(
10757
10757
  "Controls how much thinking/reasoning the subagent applies. low = light thinking, medium = moderate, high = deep reasoning, xhigh = extended effort, max = maximum effort, ultra = Codex ultra, ultracode = Claude Code dynamic workflows. Defaults: Claude = high; Codex, Cursor, DeepSeek Harness, fx, Kimi Code, Opencode, and Pi = medium."
10758
10758
  ),
10759
- title: z5.string().optional().describe("Optional title for the subagent chat (for identification)."),
10760
- timeout_minutes: z5.number().positive().optional().describe("Timeout in minutes for the subagent to complete (default: 10). Set higher for large tasks to avoid losing work.")
10759
+ title: z4.string().optional().describe("Optional title for the subagent chat (for identification)."),
10760
+ timeout_minutes: z4.number().positive().optional().describe("Timeout in minutes for the subagent to complete (default: 10). Set higher for large tasks to avoid losing work.")
10761
10761
  },
10762
10762
  async (args) => {
10763
10763
  try {
@@ -10838,13 +10838,13 @@ var messageAgentTool = tool(
10838
10838
 
10839
10839
  The tool blocks until the subagent completes and returns its response.`,
10840
10840
  {
10841
- chatId: z5.string().describe("The chat ID of the subagent (returned by spawn_agent)."),
10842
- message: z5.string().describe("The follow-up message to send."),
10843
- model: z5.string().optional().describe("Optional model override for this message."),
10844
- thinking_level: z5.enum(VALID_THINKING_LEVELS).optional().describe(
10841
+ chatId: z4.string().describe("The chat ID of the subagent (returned by spawn_agent)."),
10842
+ message: z4.string().describe("The follow-up message to send."),
10843
+ model: z4.string().optional().describe("Optional model override for this message."),
10844
+ thinking_level: z4.enum(VALID_THINKING_LEVELS).optional().describe(
10845
10845
  "Controls how much thinking/reasoning the subagent applies. low = light thinking, medium = moderate, high = deep reasoning, xhigh = extended effort, max = maximum effort, ultra = Codex ultra, ultracode = Claude Code dynamic workflows. Defaults: Claude = high, Codex = medium, Cursor = medium, Opencode = medium."
10846
10846
  ),
10847
- timeout_minutes: z5.number().positive().optional().describe("Timeout in minutes for the subagent to complete (default: 10). Set higher for large tasks to avoid losing work.")
10847
+ timeout_minutes: z4.number().positive().optional().describe("Timeout in minutes for the subagent to complete (default: 10). Set higher for large tasks to avoid losing work.")
10848
10848
  },
10849
10849
  async (args) => {
10850
10850
  try {
@@ -10882,7 +10882,7 @@ var deleteAgentTool = tool(
10882
10882
  "delete_agent",
10883
10883
  `Delete a subagent chat to free resources. Use this after a subagent has completed its work and you no longer need to send it follow-up messages.`,
10884
10884
  {
10885
- chatId: z5.string().describe("The chat ID of the subagent to delete.")
10885
+ chatId: z4.string().describe("The chat ID of the subagent to delete.")
10886
10886
  },
10887
10887
  async (args) => {
10888
10888
  try {
@@ -11248,6 +11248,17 @@ var RelayManager = class {
11248
11248
  };
11249
11249
 
11250
11250
  // src/analytics/agent/activity/agent-chat-activity-tracker-service.ts
11251
+ import { randomUUID as randomUUID8 } from "crypto";
11252
+
11253
+ // src/analytics/analytics-upload.ts
11254
+ async function uploadAnalytics(endpoint, body) {
11255
+ const response = await monolithRequest(endpoint, { body });
11256
+ if (!response.ok) {
11257
+ throw new Error(`Analytics upload failed: ${response.status} ${await response.text()}`);
11258
+ }
11259
+ }
11260
+
11261
+ // src/analytics/analytics-record-buffer.ts
11251
11262
  import {
11252
11263
  appendFile as appendFile3,
11253
11264
  mkdir as mkdir16,
@@ -11256,51 +11267,13 @@ import {
11256
11267
  rename as rename2,
11257
11268
  unlink as unlink3
11258
11269
  } from "fs/promises";
11270
+ import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync } from "fs";
11259
11271
  import { join as join24 } from "path";
11260
- import { randomUUID as randomUUID8 } from "crypto";
11261
-
11262
- // src/analytics/agent/activity/skill-mcp-call-extractor.ts
11263
- var NON_MCP_SERVERS = /* @__PURE__ */ new Set(["claude", "cursor", "deepseek", "fx", "kimi", "opencode", "pi", "custom", "dynamic"]);
11264
- function mcpNameFromToolCall(message) {
11265
- const parsedName = parseMcpToolName(message.tool);
11266
- if (parsedName) return parsedName.server;
11267
- if (message.tool.startsWith("mcp.")) {
11268
- const [server, ...toolParts] = message.tool.slice("mcp.".length).split(".");
11269
- if (server && toolParts.length > 0) return server;
11270
- }
11271
- return NON_MCP_SERVERS.has(message.server) ? null : message.server;
11272
- }
11273
- function skillCallsFromCommand(message) {
11274
- const skillNames = message.skillNames ?? [];
11275
- return skillNames.map((skillName, index) => ({
11276
- kind: "skill",
11277
- id: skillNames.length === 1 ? message.id : `${message.id}-${index}`,
11278
- skillName,
11279
- occurredAt: message.timestamp
11280
- }));
11281
- }
11282
- function extractSkillMcpCalls(messages, provider) {
11283
- return messages.flatMap((message) => {
11284
- if (message.type === "skill") {
11285
- if (provider === "relay") return [];
11286
- return [{ kind: "skill", id: message.id, skillName: message.skillName, occurredAt: message.timestamp }];
11287
- }
11288
- if (message.type === "command") {
11289
- return skillCallsFromCommand(message);
11290
- }
11291
- if (message.type !== "tool_call") return [];
11292
- const mcpName = mcpNameFromToolCall(message);
11293
- return mcpName ? [{ kind: "mcp", id: message.id, mcpName, occurredAt: message.timestamp }] : [];
11294
- });
11295
- }
11296
-
11297
- // src/analytics/agent/activity/agent-chat-activity-tracker-service.ts
11298
11272
  var MAX_FAILED_RECORDS = 500;
11299
11273
  var FLUSH_DEBOUNCE_MS = 15e3;
11300
11274
  var MAX_UPLOADED_SEGMENTS = 50;
11301
11275
  var UPLOADED_SUFFIX = ".uploaded";
11302
- var MAX_TRACKED_MESSAGES = 500;
11303
- var AgentChatActivityBuffer = class {
11276
+ var AnalyticsRecordBuffer = class {
11304
11277
  constructor(options) {
11305
11278
  this.options = options;
11306
11279
  this.storageNames = [
@@ -11318,26 +11291,39 @@ var AgentChatActivityBuffer = class {
11318
11291
  segmentFilePatterns;
11319
11292
  failedRecords = [];
11320
11293
  pendingAppends = /* @__PURE__ */ new Set();
11294
+ recordIds = /* @__PURE__ */ new Set();
11321
11295
  flushTimer = null;
11322
11296
  activeFlush = Promise.resolve({
11323
11297
  flushed: 0,
11324
11298
  failed: 0
11325
11299
  });
11326
- append(record) {
11327
- const pending = mkdir16(ENGINE_DIR2, { recursive: true }).then(
11328
- () => appendFile3(this.liveFile, `${JSON.stringify(record)}
11329
- `, "utf-8")
11330
- ).catch((error) => {
11300
+ append(record, retry = false) {
11301
+ const recordId = this.options.getRecordId?.(record);
11302
+ if (!retry && recordId) {
11303
+ if (this.recordIds.has(recordId)) return;
11304
+ this.recordIds.add(recordId);
11305
+ }
11306
+ const pending = mkdir16(ENGINE_DIR2, { recursive: true }).then(() => appendFile3(this.liveFile, `${JSON.stringify(record)}
11307
+ `, "utf-8")).catch((error) => {
11331
11308
  console.error(
11332
11309
  `[${this.options.storageName}] Append failed, will retry on flush:`,
11333
11310
  error
11334
11311
  );
11335
- if (this.failedRecords.length < MAX_FAILED_RECORDS)
11336
- this.failedRecords.push(record);
11312
+ if (this.failedRecords.length < MAX_FAILED_RECORDS) this.failedRecords.push(record);
11337
11313
  });
11338
11314
  this.pendingAppends.add(pending);
11339
11315
  void pending.finally(() => this.pendingAppends.delete(pending));
11340
11316
  }
11317
+ readStoredRecords() {
11318
+ if (!existsSync8(ENGINE_DIR2)) return [];
11319
+ return readdirSync(ENGINE_DIR2, { withFileTypes: true }).filter((entry) => entry.isFile() && this.storageNames.some(
11320
+ (storageName) => entry.name === `${storageName}.jsonl` || entry.name.startsWith(`${storageName}.`) && (entry.name.endsWith(".jsonl") || entry.name.endsWith(`.jsonl${UPLOADED_SUFFIX}`))
11321
+ )).map(({ name }) => name).sort().flatMap((name) => this.parseRecords(readFileSync7(join24(ENGINE_DIR2, name), "utf-8"))).map((record) => {
11322
+ const recordId = this.options.getRecordId?.(record);
11323
+ if (recordId) this.recordIds.add(recordId);
11324
+ return record;
11325
+ });
11326
+ }
11341
11327
  scheduleFlush() {
11342
11328
  if (this.flushTimer) return;
11343
11329
  this.flushTimer = setTimeout(() => {
@@ -11361,9 +11347,10 @@ var AgentChatActivityBuffer = class {
11361
11347
  return this.activeFlush;
11362
11348
  }
11363
11349
  async runFlush() {
11364
- for (const record of this.failedRecords.splice(0)) this.append(record);
11365
- while (this.pendingAppends.size > 0)
11350
+ for (const record of this.failedRecords.splice(0)) this.append(record, true);
11351
+ while (this.pendingAppends.size > 0) {
11366
11352
  await Promise.allSettled([...this.pendingAppends]);
11353
+ }
11367
11354
  for (const storageName of this.storageNames) {
11368
11355
  await rename2(
11369
11356
  join24(ENGINE_DIR2, `${storageName}.jsonl`),
@@ -11375,8 +11362,7 @@ var AgentChatActivityBuffer = class {
11375
11362
  let flushed = 0;
11376
11363
  let failed = 0;
11377
11364
  for (const entry of entries) {
11378
- if (!this.segmentFilePatterns.some((pattern) => pattern.test(entry)))
11379
- continue;
11365
+ if (!this.segmentFilePatterns.some((pattern) => pattern.test(entry))) continue;
11380
11366
  try {
11381
11367
  await this.uploadSegment(join24(ENGINE_DIR2, entry));
11382
11368
  flushed++;
@@ -11395,28 +11381,32 @@ var AgentChatActivityBuffer = class {
11395
11381
  flushed++;
11396
11382
  } catch (error) {
11397
11383
  failed++;
11398
- this.failedRecords.unshift(
11399
- ...failedRecords.slice(0, MAX_FAILED_RECORDS)
11400
- );
11384
+ this.failedRecords.unshift(...failedRecords.slice(0, MAX_FAILED_RECORDS));
11401
11385
  console.error(
11402
11386
  `[${this.options.storageName}] Failed records upload failed, retained for retry:`,
11403
11387
  error
11404
11388
  );
11405
11389
  }
11406
11390
  }
11407
- const uploaded = entries.filter(
11408
- (entry) => this.storageNames.some(
11391
+ if (!this.options.retainUploadedSegments) {
11392
+ const uploaded = entries.filter((entry) => this.storageNames.some(
11409
11393
  (storageName) => entry.startsWith(`${storageName}.`)
11410
- ) && entry.endsWith(UPLOADED_SUFFIX)
11411
- ).sort();
11412
- for (const entry of uploaded.slice(0, -MAX_UPLOADED_SEGMENTS)) {
11413
- await unlink3(join24(ENGINE_DIR2, entry)).catch(() => {
11414
- });
11394
+ ) && entry.endsWith(UPLOADED_SUFFIX)).sort();
11395
+ for (const entry of uploaded.slice(0, -MAX_UPLOADED_SEGMENTS)) {
11396
+ await unlink3(join24(ENGINE_DIR2, entry)).catch(() => {
11397
+ });
11398
+ }
11415
11399
  }
11416
11400
  return { flushed, failed };
11417
11401
  }
11418
11402
  async uploadSegment(filePath) {
11419
- const records = (await readFile14(filePath, "utf-8")).split("\n").flatMap((line) => {
11403
+ const records = this.parseRecords(await readFile14(filePath, "utf-8"));
11404
+ if (records.length > 0) await this.options.upload(records);
11405
+ await rename2(filePath, `${filePath}${UPLOADED_SUFFIX}`).catch(() => {
11406
+ });
11407
+ }
11408
+ parseRecords(contents) {
11409
+ return contents.split("\n").flatMap((line) => {
11420
11410
  try {
11421
11411
  const parsed = JSON.parse(line);
11422
11412
  return this.options.validate(parsed) ? [parsed] : [];
@@ -11424,16 +11414,51 @@ var AgentChatActivityBuffer = class {
11424
11414
  return [];
11425
11415
  }
11426
11416
  });
11427
- if (records.length > 0) await this.options.upload(records);
11428
- await rename2(filePath, `${filePath}${UPLOADED_SUFFIX}`).catch(() => {
11429
- });
11430
11417
  }
11431
11418
  };
11419
+
11420
+ // src/analytics/agent/activity/skill-mcp-call-extractor.ts
11421
+ var NON_MCP_SERVERS = /* @__PURE__ */ new Set(["claude", "cursor", "deepseek", "fx", "kimi", "opencode", "pi", "custom", "dynamic"]);
11422
+ function mcpNameFromToolCall(message) {
11423
+ const parsedName = parseMcpToolName(message.tool);
11424
+ if (parsedName) return parsedName.server;
11425
+ if (message.tool.startsWith("mcp.")) {
11426
+ const [server, ...toolParts] = message.tool.slice("mcp.".length).split(".");
11427
+ if (server && toolParts.length > 0) return server;
11428
+ }
11429
+ return NON_MCP_SERVERS.has(message.server) ? null : message.server;
11430
+ }
11431
+ function skillCallsFromCommand(message) {
11432
+ const skillNames = message.skillNames ?? [];
11433
+ return skillNames.map((skillName, index) => ({
11434
+ kind: "skill",
11435
+ id: skillNames.length === 1 ? message.id : `${message.id}-${index}`,
11436
+ skillName,
11437
+ occurredAt: message.timestamp
11438
+ }));
11439
+ }
11440
+ function extractSkillMcpCalls(messages, provider) {
11441
+ return messages.flatMap((message) => {
11442
+ if (message.type === "skill") {
11443
+ if (provider === "relay") return [];
11444
+ return [{ kind: "skill", id: message.id, skillName: message.skillName, occurredAt: message.timestamp }];
11445
+ }
11446
+ if (message.type === "command") {
11447
+ return skillCallsFromCommand(message);
11448
+ }
11449
+ if (message.type !== "tool_call") return [];
11450
+ const mcpName = mcpNameFromToolCall(message);
11451
+ return mcpName ? [{ kind: "mcp", id: message.id, mcpName, occurredAt: message.timestamp }] : [];
11452
+ });
11453
+ }
11454
+
11455
+ // src/analytics/agent/activity/agent-chat-activity-tracker-service.ts
11456
+ var MAX_TRACKED_MESSAGES = 500;
11432
11457
  var AgentChatActivityRecordTracker = class {
11433
11458
  pendingRecords = /* @__PURE__ */ new Map();
11434
11459
  buffer;
11435
11460
  constructor(options) {
11436
- this.buffer = new AgentChatActivityBuffer(options);
11461
+ this.buffer = new AnalyticsRecordBuffer(options);
11437
11462
  }
11438
11463
  track(chatId, record) {
11439
11464
  const records = this.pendingRecords.get(chatId) ?? [];
@@ -11456,17 +11481,20 @@ var AgentChatTurnActivityTracker = class {
11456
11481
  activeTurns = /* @__PURE__ */ new Map();
11457
11482
  turnBuffer;
11458
11483
  constructor(uploadTurns) {
11459
- this.turnBuffer = new AgentChatActivityBuffer({
11484
+ this.turnBuffer = new AnalyticsRecordBuffer({
11460
11485
  storageName: "chat-turn-activity",
11461
11486
  legacyStorageNames: ["turn-usage"],
11462
11487
  validate: isAgentChatTurnActivityRecord,
11463
- upload: uploadTurns
11488
+ upload: uploadTurns,
11489
+ getRecordId: (turn) => turn.turnId
11464
11490
  });
11465
11491
  }
11466
11492
  noteMessageAccepted(messageId, request) {
11467
11493
  this.pendingMessages.set(messageId, {
11468
11494
  model: request.model,
11469
- senderUserId: request.senderUserId
11495
+ senderUserId: request.senderUserId,
11496
+ automationId: request.automationId,
11497
+ environmentId: request.environmentId
11470
11498
  });
11471
11499
  for (const key of this.pendingMessages.keys()) {
11472
11500
  if (this.pendingMessages.size <= MAX_TRACKED_MESSAGES) break;
@@ -11477,16 +11505,23 @@ var AgentChatTurnActivityTracker = class {
11477
11505
  const attributes = this.pendingMessages.get(messageId) ?? {};
11478
11506
  const credential = ENGINE_ENV.REPLICAS_AGENT_CREDENTIALS[provider];
11479
11507
  this.pendingMessages.delete(messageId);
11480
- this.activeTurns.set(chatId, {
11508
+ const context = {
11509
+ chatId,
11481
11510
  turnId: randomUUID8(),
11482
- startedAtMs: Date.now(),
11483
11511
  provider,
11484
11512
  model: attributes.model ?? getDefaultAgentModel(provider),
11485
11513
  senderUserId: attributes.senderUserId,
11514
+ automationId: attributes.automationId,
11515
+ environmentId: attributes.environmentId,
11486
11516
  credentialMethod: credential?.method,
11487
- credentialScope: credential?.scope,
11517
+ credentialScope: credential?.scope
11518
+ };
11519
+ this.activeTurns.set(chatId, {
11520
+ ...context,
11521
+ startedAtMs: Date.now(),
11488
11522
  seenCallIds: /* @__PURE__ */ new Set()
11489
11523
  });
11524
+ return context;
11490
11525
  }
11491
11526
  getActiveTurn(chatId) {
11492
11527
  return this.activeTurns.get(chatId);
@@ -11515,39 +11550,32 @@ var AgentChatTurnActivityTracker = class {
11515
11550
  return this.turnBuffer.flush();
11516
11551
  }
11517
11552
  };
11518
- async function uploadAgentChatActivity(endpoint, body) {
11519
- const response = await monolithRequest(endpoint, { body });
11520
- if (!response.ok)
11521
- throw new Error(
11522
- `upload failed: ${response.status} ${await response.text()}`
11523
- );
11524
- }
11525
11553
  var AgentChatActivityTrackerService = class {
11526
11554
  turnActivityTracker;
11527
11555
  skillActivityTracker;
11528
11556
  mcpActivityTracker;
11529
11557
  constructor() {
11530
11558
  this.turnActivityTracker = new AgentChatTurnActivityTracker(
11531
- (turns) => uploadAgentChatActivity("/v1/engine/chat-turn-activity", { turns })
11559
+ (turns) => uploadAnalytics("/v1/engine/chat-turn-activity", { turns })
11532
11560
  );
11533
11561
  this.skillActivityTracker = new AgentChatActivityRecordTracker({
11534
11562
  storageName: "skill-activity",
11535
11563
  legacyStorageNames: ["skill-usage"],
11536
11564
  validate: isAgentChatSkillActivityRecord,
11537
- upload: (skills) => uploadAgentChatActivity("/v1/engine/skill-activity", { skills })
11565
+ upload: (skills) => uploadAnalytics("/v1/engine/skill-activity", { skills })
11538
11566
  });
11539
11567
  this.mcpActivityTracker = new AgentChatActivityRecordTracker({
11540
11568
  storageName: "mcp-activity",
11541
11569
  legacyStorageNames: ["mcp-usage"],
11542
11570
  validate: isAgentChatMcpActivityRecord,
11543
- upload: (mcps) => uploadAgentChatActivity("/v1/engine/mcp-activity", { mcps })
11571
+ upload: (mcps) => uploadAnalytics("/v1/engine/mcp-activity", { mcps })
11544
11572
  });
11545
11573
  }
11546
11574
  noteMessageAccepted(messageId, request) {
11547
11575
  this.turnActivityTracker.noteMessageAccepted(messageId, request);
11548
11576
  }
11549
11577
  noteTurnStarted(chatId, messageId, provider) {
11550
- this.turnActivityTracker.noteTurnStarted(chatId, messageId, provider);
11578
+ return this.turnActivityTracker.noteTurnStarted(chatId, messageId, provider);
11551
11579
  }
11552
11580
  noteSkillMcpCalls(chatId, messages) {
11553
11581
  const turn = this.turnActivityTracker.getActiveTurn(chatId);
@@ -11597,6 +11625,523 @@ var AgentChatActivityTrackerService = class {
11597
11625
  }
11598
11626
  };
11599
11627
 
11628
+ // src/analytics/token-usage/agent-token-usage-tracker.ts
11629
+ import { createHash as createHash2 } from "crypto";
11630
+
11631
+ // src/analytics/token-usage/types.ts
11632
+ function calculateCumulativeTokenUsageDelta(current, previous) {
11633
+ if (!previous || current.inputTokens < previous.inputTokens || current.outputTokens < previous.outputTokens || current.cacheReadInputTokens < previous.cacheReadInputTokens || current.cacheWriteInputTokens < previous.cacheWriteInputTokens) return current;
11634
+ return {
11635
+ inputTokens: current.inputTokens - previous.inputTokens,
11636
+ outputTokens: current.outputTokens - previous.outputTokens,
11637
+ cacheReadInputTokens: current.cacheReadInputTokens - previous.cacheReadInputTokens,
11638
+ cacheWriteInputTokens: current.cacheWriteInputTokens - previous.cacheWriteInputTokens
11639
+ };
11640
+ }
11641
+
11642
+ // src/analytics/token-usage/claude/parse.ts
11643
+ function isClaudeModelUsage(value) {
11644
+ if (!isRecord(value) || !isTokenUsageCounts(value)) return false;
11645
+ return (value.costUsd === void 0 || typeof value.costUsd === "number" && Number.isFinite(value.costUsd) && value.costUsd >= 0) && (value.pricingProvider === void 0 || isNonEmptyString(value.pricingProvider));
11646
+ }
11647
+ function parseClaudeResult(payload) {
11648
+ if (!isRecord(payload) || payload.type !== "result") return null;
11649
+ if (!isNonEmptyString(payload.session_id) || !isRecord(payload.modelUsage)) return null;
11650
+ const snapshot = {};
11651
+ for (const [rawModel, rawUsage] of Object.entries(payload.modelUsage)) {
11652
+ if (!isNonEmptyString(rawModel) || !isRecord(rawUsage)) continue;
11653
+ const usage = {
11654
+ inputTokens: rawUsage.inputTokens,
11655
+ outputTokens: rawUsage.outputTokens,
11656
+ cacheReadInputTokens: rawUsage.cacheReadInputTokens,
11657
+ cacheWriteInputTokens: rawUsage.cacheCreationInputTokens,
11658
+ ...typeof rawUsage.costUSD === "number" && Number.isFinite(rawUsage.costUSD) && rawUsage.costUSD >= 0 ? { costUsd: rawUsage.costUSD } : {},
11659
+ ...isNonEmptyString(rawUsage.provider) ? { pricingProvider: rawUsage.provider } : {}
11660
+ };
11661
+ if (!isClaudeModelUsage(usage)) continue;
11662
+ const model = isNonEmptyString(rawUsage.canonicalModel) ? rawUsage.canonicalModel : rawModel.replace(/-\d{8}$/, "");
11663
+ const previous = snapshot[model];
11664
+ snapshot[model] = previous ? {
11665
+ inputTokens: previous.inputTokens + usage.inputTokens,
11666
+ outputTokens: previous.outputTokens + usage.outputTokens,
11667
+ cacheReadInputTokens: previous.cacheReadInputTokens + usage.cacheReadInputTokens,
11668
+ cacheWriteInputTokens: previous.cacheWriteInputTokens + usage.cacheWriteInputTokens,
11669
+ ...previous.costUsd !== void 0 || usage.costUsd !== void 0 ? { costUsd: (previous.costUsd ?? 0) + (usage.costUsd ?? 0) } : {},
11670
+ ...usage.pricingProvider ?? previous.pricingProvider ? { pricingProvider: usage.pricingProvider ?? previous.pricingProvider } : {}
11671
+ } : usage;
11672
+ }
11673
+ if (Object.keys(snapshot).length === 0) return null;
11674
+ return {
11675
+ sessionId: payload.session_id,
11676
+ snapshot
11677
+ };
11678
+ }
11679
+
11680
+ // src/analytics/token-usage/claude/index.ts
11681
+ function isClaudeTokenUsageCheckpoint(value) {
11682
+ return isRecord(value) && isNonEmptyString(value.chatId) && isNonEmptyString(value.sessionId) && isRecord(value.snapshot) && Object.values(value.snapshot).every(isClaudeModelUsage);
11683
+ }
11684
+ function normalizeClaudeTokenUsage(result, previous, context, occurredAt) {
11685
+ return Object.entries(result.snapshot).flatMap(([model, currentMetrics]) => {
11686
+ const previousMetrics = previous?.[model];
11687
+ const tokenCounts = calculateCumulativeTokenUsageDelta(currentMetrics, previousMetrics);
11688
+ const providerCostUsd = currentMetrics.costUsd === void 0 ? void 0 : previousMetrics?.costUsd === void 0 || currentMetrics.costUsd < previousMetrics.costUsd ? currentMetrics.costUsd : currentMetrics.costUsd - previousMetrics.costUsd;
11689
+ const pricingProvider = currentMetrics.pricingProvider ?? previousMetrics?.pricingProvider;
11690
+ return Object.values(tokenCounts).some((count) => count > 0) ? [{
11691
+ ...context,
11692
+ model,
11693
+ ...tokenCounts,
11694
+ harness: AGENT.CLAUDE,
11695
+ occurredAt,
11696
+ ...pricingProvider ? { pricingProvider } : {},
11697
+ ...providerCostUsd !== void 0 ? { reportedCostUsd: providerCostUsd } : {}
11698
+ }] : [];
11699
+ });
11700
+ }
11701
+ var ClaudeTokenUsageService = class {
11702
+ snapshots = /* @__PURE__ */ new Map();
11703
+ record(event) {
11704
+ if (event.type !== "usage-updated") return void 0;
11705
+ const result = parseClaudeResult(event.payload);
11706
+ if (!result) return void 0;
11707
+ const snapshotsBySession = this.snapshots.get(event.chatId) ?? /* @__PURE__ */ new Map();
11708
+ this.snapshots.set(event.chatId, snapshotsBySession);
11709
+ const previous = snapshotsBySession.get(result.sessionId);
11710
+ const records = normalizeClaudeTokenUsage(
11711
+ result,
11712
+ previous,
11713
+ event.context,
11714
+ event.occurredAt
11715
+ );
11716
+ snapshotsBySession.set(result.sessionId, result.snapshot);
11717
+ return {
11718
+ records,
11719
+ checkpoint: {
11720
+ chatId: event.chatId,
11721
+ sessionId: result.sessionId,
11722
+ snapshot: result.snapshot
11723
+ }
11724
+ };
11725
+ }
11726
+ restoreCheckpoint(value) {
11727
+ if (!isClaudeTokenUsageCheckpoint(value)) return;
11728
+ const snapshotsBySession = this.snapshots.get(value.chatId) ?? /* @__PURE__ */ new Map();
11729
+ snapshotsBySession.set(value.sessionId, value.snapshot);
11730
+ this.snapshots.set(value.chatId, snapshotsBySession);
11731
+ }
11732
+ };
11733
+
11734
+ // src/analytics/token-usage/codex/parse.ts
11735
+ function isTokenCount(value) {
11736
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
11737
+ }
11738
+ function parseCodexTurn(value) {
11739
+ if (!isRecord(value) || !isNonEmptyString(value.threadId) || !isNonEmptyString(value.turnId)) return void 0;
11740
+ return { threadId: value.threadId, turnId: value.turnId };
11741
+ }
11742
+ function parseCodexUsage(value) {
11743
+ if (!isRecord(value) || !isTokenCount(value.inputTokens) || !isTokenCount(value.cachedInputTokens) || !isTokenCount(value.outputTokens)) {
11744
+ return void 0;
11745
+ }
11746
+ const cacheWriteInputTokens = value.cacheWriteInputTokens;
11747
+ if (cacheWriteInputTokens !== void 0 && !isTokenCount(cacheWriteInputTokens)) return void 0;
11748
+ return {
11749
+ inputTokens: value.inputTokens,
11750
+ cachedInputTokens: value.cachedInputTokens,
11751
+ outputTokens: value.outputTokens,
11752
+ ...cacheWriteInputTokens === void 0 ? {} : { cacheWriteInputTokens }
11753
+ };
11754
+ }
11755
+ function parseCodexUsageUpdate(value) {
11756
+ if (!isRecord(value)) return void 0;
11757
+ const turn = parseCodexTurn(value);
11758
+ const usage = isRecord(value.tokenUsage) ? parseCodexUsage(value.tokenUsage.last) : void 0;
11759
+ if (!turn || !usage) return void 0;
11760
+ return { ...turn, usage };
11761
+ }
11762
+ function parseCodexModelReroute(value) {
11763
+ if (!isRecord(value) || !isNonEmptyString(value.toModel)) return void 0;
11764
+ const turn = parseCodexTurn(value);
11765
+ return turn ? { ...turn, model: value.toModel } : void 0;
11766
+ }
11767
+
11768
+ // src/analytics/token-usage/codex/index.ts
11769
+ var CodexTokenUsageAdapter = class {
11770
+ constructor(chatId, getContext, recordEvent) {
11771
+ this.chatId = chatId;
11772
+ this.getContext = getContext;
11773
+ this.recordEvent = recordEvent;
11774
+ }
11775
+ chatId;
11776
+ getContext;
11777
+ recordEvent;
11778
+ onTurnStarted = (threadId, turnId) => {
11779
+ const context = this.getContext();
11780
+ if (!context) return;
11781
+ this.recordEvent({
11782
+ harness: AGENT.CODEX,
11783
+ type: "turn-started",
11784
+ chatId: this.chatId,
11785
+ payload: { threadId, turnId },
11786
+ context
11787
+ });
11788
+ };
11789
+ onTokenUsage = (notification, occurredAt) => {
11790
+ const context = this.getContext();
11791
+ if (!context) return;
11792
+ this.recordEvent({
11793
+ harness: AGENT.CODEX,
11794
+ type: "usage-updated",
11795
+ chatId: this.chatId,
11796
+ payload: notification,
11797
+ context,
11798
+ occurredAt
11799
+ });
11800
+ };
11801
+ onModelRerouted = (notification) => {
11802
+ if (!this.getContext()) return;
11803
+ this.recordEvent({
11804
+ harness: AGENT.CODEX,
11805
+ type: "model-rerouted",
11806
+ chatId: this.chatId,
11807
+ payload: notification
11808
+ });
11809
+ };
11810
+ onTurnCompleted = (threadId, turnId) => {
11811
+ this.recordEvent({
11812
+ harness: AGENT.CODEX,
11813
+ type: "turn-completed",
11814
+ chatId: this.chatId,
11815
+ payload: { threadId, turnId }
11816
+ });
11817
+ };
11818
+ };
11819
+ function getTurnKey({ threadId, turnId }) {
11820
+ return `${threadId}\0${turnId}`;
11821
+ }
11822
+ var CodexTokenUsageService = class {
11823
+ turns = /* @__PURE__ */ new Map();
11824
+ record(event) {
11825
+ switch (event.type) {
11826
+ case "turn-started": {
11827
+ const turn = parseCodexTurn(event.payload);
11828
+ if (!turn) return void 0;
11829
+ const key = getTurnKey(turn);
11830
+ if (!this.turns.has(key)) {
11831
+ this.turns.set(key, { ...turn, context: event.context });
11832
+ }
11833
+ return void 0;
11834
+ }
11835
+ case "usage-updated": {
11836
+ const update = parseCodexUsageUpdate(event.payload);
11837
+ if (!update) return void 0;
11838
+ const key = getTurnKey(update);
11839
+ const turn = this.turns.get(key) ?? {
11840
+ ...update,
11841
+ context: event.context
11842
+ };
11843
+ turn.latestUsage = update.usage;
11844
+ turn.occurredAt = event.occurredAt;
11845
+ this.turns.set(key, turn);
11846
+ return void 0;
11847
+ }
11848
+ case "model-rerouted": {
11849
+ const reroute = parseCodexModelReroute(event.payload);
11850
+ if (!reroute) return void 0;
11851
+ const tracked = this.turns.get(getTurnKey(reroute));
11852
+ if (tracked) tracked.context = { ...tracked.context, model: reroute.model };
11853
+ return void 0;
11854
+ }
11855
+ case "turn-completed": {
11856
+ const turn = parseCodexTurn(event.payload);
11857
+ return turn ? this.finishTurn(event.chatId, turn) : void 0;
11858
+ }
11859
+ case "chat-ended": {
11860
+ const records = [];
11861
+ for (const turn of this.turns.values()) {
11862
+ if (turn.context.chatId !== event.chatId) continue;
11863
+ const batch = this.finishTurn(event.chatId, turn);
11864
+ if (batch) records.push(...batch.records);
11865
+ }
11866
+ return records.length > 0 ? { records } : void 0;
11867
+ }
11868
+ }
11869
+ }
11870
+ finishTurn(chatId, turn) {
11871
+ const key = getTurnKey(turn);
11872
+ const tracked = this.turns.get(key);
11873
+ if (!tracked || tracked.context.chatId !== chatId) return void 0;
11874
+ this.turns.delete(key);
11875
+ if (!tracked.latestUsage || !tracked.occurredAt) return void 0;
11876
+ const cacheWriteInputTokens = tracked.latestUsage.cacheWriteInputTokens ?? 0;
11877
+ return {
11878
+ records: [{
11879
+ ...tracked.context,
11880
+ harness: AGENT.CODEX,
11881
+ inputTokens: Math.max(
11882
+ 0,
11883
+ tracked.latestUsage.inputTokens - tracked.latestUsage.cachedInputTokens - cacheWriteInputTokens
11884
+ ),
11885
+ outputTokens: tracked.latestUsage.outputTokens,
11886
+ cacheReadInputTokens: tracked.latestUsage.cachedInputTokens,
11887
+ cacheWriteInputTokens,
11888
+ occurredAt: tracked.occurredAt
11889
+ }]
11890
+ };
11891
+ }
11892
+ };
11893
+
11894
+ // src/analytics/token-usage/cursor/index.ts
11895
+ function parseCursorTokenUsage(payload) {
11896
+ if (!isRecord(payload) || payload.type !== "turn-ended" || !isRecord(payload.usage)) return void 0;
11897
+ const usage = {
11898
+ inputTokens: payload.usage.inputTokens,
11899
+ outputTokens: payload.usage.outputTokens,
11900
+ cacheReadInputTokens: payload.usage.cacheReadTokens,
11901
+ cacheWriteInputTokens: payload.usage.cacheWriteTokens
11902
+ };
11903
+ return isTokenUsageCounts(usage) ? usage : void 0;
11904
+ }
11905
+ var CursorTokenUsageService = class {
11906
+ turns = /* @__PURE__ */ new Map();
11907
+ record(event) {
11908
+ if (event.type === "usage-updated") {
11909
+ const usage = parseCursorTokenUsage(event.payload);
11910
+ if (!usage) return void 0;
11911
+ const turnId2 = event.turnId ?? event.chatId;
11912
+ this.turns.set(`${event.chatId}\0${turnId2}`, {
11913
+ context: event.context,
11914
+ usage,
11915
+ occurredAt: event.occurredAt
11916
+ });
11917
+ return void 0;
11918
+ }
11919
+ if (event.type !== "chat-ended") return void 0;
11920
+ const turnId = event.turnId ?? event.chatId;
11921
+ const key = `${event.chatId}\0${turnId}`;
11922
+ const turn = this.turns.get(key);
11923
+ if (!turn) return void 0;
11924
+ this.turns.delete(key);
11925
+ const record = {
11926
+ ...turn.context,
11927
+ harness: AGENT.CURSOR,
11928
+ ...turn.usage,
11929
+ occurredAt: turn.occurredAt
11930
+ };
11931
+ return { records: [record] };
11932
+ }
11933
+ };
11934
+
11935
+ // src/analytics/token-usage/deepseek/index.ts
11936
+ function parseDeepseekTokenUsage(payload) {
11937
+ if (!isRecord(payload)) return void 0;
11938
+ const source = isRecord(payload.totals) ? payload.totals : payload;
11939
+ const snapshot = {
11940
+ inputTokens: source.uncachedInputTokens,
11941
+ outputTokens: source.outputTokens,
11942
+ cacheReadInputTokens: source.cacheReadTokens,
11943
+ cacheWriteInputTokens: source.cacheWriteTokens
11944
+ };
11945
+ return isTokenUsageCounts(snapshot) ? snapshot : void 0;
11946
+ }
11947
+ var DeepseekTokenUsageService = class {
11948
+ snapshots = /* @__PURE__ */ new Map();
11949
+ record(event) {
11950
+ if (event.type !== "usage-updated") return void 0;
11951
+ const current = parseDeepseekTokenUsage(event.payload);
11952
+ if (!current) return void 0;
11953
+ const previous = this.snapshots.get(event.chatId);
11954
+ this.snapshots.set(event.chatId, current);
11955
+ const usage = calculateCumulativeTokenUsageDelta(current, previous);
11956
+ const records = Object.values(usage).some((count) => count > 0) ? [{ ...event.context, harness: AGENT.DEEPSEEK, ...usage, occurredAt: event.occurredAt }] : [];
11957
+ return {
11958
+ records,
11959
+ checkpoint: { chatId: event.chatId, snapshot: current }
11960
+ };
11961
+ }
11962
+ restoreCheckpoint(value) {
11963
+ if (!isRecord(value) || !isNonEmptyString(value.chatId) || !isTokenUsageCounts(value.snapshot)) return;
11964
+ this.snapshots.set(value.chatId, value.snapshot);
11965
+ }
11966
+ };
11967
+
11968
+ // src/analytics/token-usage/opencode/index.ts
11969
+ function parseOpencodeTokenUsage(payload) {
11970
+ if (!isRecord(payload) || payload.type !== "step-finish" || !isRecord(payload.tokens) || !isRecord(payload.tokens.cache)) return void 0;
11971
+ const usage = {
11972
+ inputTokens: payload.tokens.input,
11973
+ outputTokens: payload.tokens.output,
11974
+ cacheReadInputTokens: payload.tokens.cache.read,
11975
+ cacheWriteInputTokens: payload.tokens.cache.write
11976
+ };
11977
+ return isTokenUsageCounts(usage) ? usage : void 0;
11978
+ }
11979
+ var OpencodeTokenUsageService = class {
11980
+ record(event) {
11981
+ if (event.type !== "usage-updated") return void 0;
11982
+ const usage = parseOpencodeTokenUsage(event.payload);
11983
+ if (!usage) return void 0;
11984
+ const record = {
11985
+ ...event.context,
11986
+ harness: AGENT.OPENCODE,
11987
+ ...usage,
11988
+ occurredAt: event.occurredAt
11989
+ };
11990
+ return { records: [record] };
11991
+ }
11992
+ };
11993
+
11994
+ // src/analytics/token-usage/pi/index.ts
11995
+ function parsePiTokenUsage(payload) {
11996
+ if (!isRecord(payload) || payload.type !== "message_end" || !isRecord(payload.message) || payload.message.role !== "assistant" || !isRecord(payload.message.usage)) return void 0;
11997
+ const usage = {
11998
+ inputTokens: payload.message.usage.input,
11999
+ outputTokens: payload.message.usage.output,
12000
+ cacheReadInputTokens: payload.message.usage.cacheRead,
12001
+ cacheWriteInputTokens: payload.message.usage.cacheWrite
12002
+ };
12003
+ if (!isTokenUsageCounts(usage)) return void 0;
12004
+ const model = isNonEmptyString(payload.message.responseModel) ? payload.message.responseModel : isNonEmptyString(payload.message.model) ? payload.message.model : void 0;
12005
+ return { model, usage };
12006
+ }
12007
+ var PiTokenUsageService = class {
12008
+ record(event) {
12009
+ if (event.type !== "usage-updated") return void 0;
12010
+ const result = parsePiTokenUsage(event.payload);
12011
+ if (!result) return void 0;
12012
+ const record = {
12013
+ ...event.context,
12014
+ harness: AGENT.PI,
12015
+ model: result.model ?? event.context.model,
12016
+ ...result.usage,
12017
+ occurredAt: event.occurredAt
12018
+ };
12019
+ return { records: [record] };
12020
+ }
12021
+ };
12022
+
12023
+ // src/analytics/token-usage/agent-token-usage-factory.ts
12024
+ function createAgentTokenUsageService(harness) {
12025
+ switch (harness) {
12026
+ case AGENT.CLAUDE:
12027
+ return new ClaudeTokenUsageService();
12028
+ case AGENT.CODEX:
12029
+ return new CodexTokenUsageService();
12030
+ case AGENT.CURSOR:
12031
+ return new CursorTokenUsageService();
12032
+ case AGENT.DEEPSEEK:
12033
+ return new DeepseekTokenUsageService();
12034
+ case AGENT.OPENCODE:
12035
+ return new OpencodeTokenUsageService();
12036
+ case AGENT.PI:
12037
+ return new PiTokenUsageService();
12038
+ default:
12039
+ return void 0;
12040
+ }
12041
+ }
12042
+
12043
+ // src/analytics/token-usage/agent-token-usage-tracker.ts
12044
+ var AgentTokenUsageTracker = class {
12045
+ activeTurns = /* @__PURE__ */ new Map();
12046
+ services = /* @__PURE__ */ new Map();
12047
+ buffer;
12048
+ constructor(uploadAnalytics2) {
12049
+ this.buffer = new AnalyticsRecordBuffer({
12050
+ storageName: "chat-token-usage",
12051
+ validate: (value) => isRecord(value) && isNonEmptyString(value.harness) && Array.isArray(value.records) && value.records.every(isTokenUsageUploadRecord),
12052
+ upload: (batches) => uploadAnalytics2(TOKEN_USAGE_PATH, {
12053
+ records: batches.flatMap(({ records }) => records)
12054
+ }),
12055
+ getRecordId: ({ harness, records }) => {
12056
+ const eventIds = records.flatMap((record) => record.eventId ? [record.eventId] : []);
12057
+ if (eventIds.length === 0 || eventIds.length !== records.length) return void 0;
12058
+ return [harness, ...eventIds].join("\0");
12059
+ },
12060
+ retainUploadedSegments: true
12061
+ });
12062
+ for (const batch of this.buffer.readStoredRecords()) {
12063
+ if (batch.checkpoint !== void 0) {
12064
+ this.getService(batch.harness)?.restoreCheckpoint?.(batch.checkpoint);
12065
+ }
12066
+ }
12067
+ }
12068
+ startTurn(context) {
12069
+ this.activeTurns.set(context.chatId, context);
12070
+ }
12071
+ getTokenUsageContext(chatId) {
12072
+ const context = this.activeTurns.get(chatId);
12073
+ if (!context?.environmentId) return void 0;
12074
+ return {
12075
+ chatId: context.chatId,
12076
+ model: context.model,
12077
+ credentialMethod: context.credentialMethod,
12078
+ credentialScope: context.credentialScope,
12079
+ senderUserId: context.senderUserId,
12080
+ automationId: context.automationId,
12081
+ environmentId: context.environmentId
12082
+ };
12083
+ }
12084
+ noteAgentEvent(chatId, event) {
12085
+ if (event.type !== "claude-result") return;
12086
+ this.noteUsage(chatId, event.payload, event.timestamp);
12087
+ }
12088
+ noteUsage(chatId, payload, occurredAt) {
12089
+ const context = this.activeTurns.get(chatId);
12090
+ const tokenUsageContext = this.getTokenUsageContext(chatId);
12091
+ if (!context || !tokenUsageContext) return;
12092
+ this.recordEvent({
12093
+ harness: context.provider,
12094
+ type: "usage-updated",
12095
+ chatId,
12096
+ turnId: context.turnId,
12097
+ payload,
12098
+ context: tokenUsageContext,
12099
+ occurredAt
12100
+ });
12101
+ }
12102
+ endTurn(chatId) {
12103
+ const context = this.activeTurns.get(chatId);
12104
+ if (!context) return;
12105
+ this.recordEvent({
12106
+ harness: context.provider,
12107
+ type: "chat-ended",
12108
+ chatId,
12109
+ turnId: context.turnId
12110
+ });
12111
+ this.activeTurns.delete(chatId);
12112
+ }
12113
+ flush(finalizeInFlight = true) {
12114
+ if (finalizeInFlight) {
12115
+ for (const chatId of [...this.activeTurns.keys()]) this.endTurn(chatId);
12116
+ }
12117
+ return this.buffer.flush();
12118
+ }
12119
+ recordEvent(event) {
12120
+ const batch = this.getService(event.harness)?.record(event);
12121
+ if (!batch) return;
12122
+ this.buffer.append({
12123
+ harness: event.harness,
12124
+ records: batch.records.map((record) => ({
12125
+ ...record,
12126
+ eventId: record.eventId ?? createUuidFromHex(createHash2("sha256").update(JSON.stringify([
12127
+ event.harness,
12128
+ event.type,
12129
+ event.chatId,
12130
+ event.turnId ?? "",
12131
+ record
12132
+ ])).digest("hex"))
12133
+ })),
12134
+ checkpoint: batch.checkpoint
12135
+ });
12136
+ this.buffer.scheduleFlush();
12137
+ }
12138
+ getService(harness) {
12139
+ const service = this.services.get(harness) ?? createAgentTokenUsageService(harness);
12140
+ if (service) this.services.set(harness, service);
12141
+ return service;
12142
+ }
12143
+ };
12144
+
11600
12145
  // src/services/keep-alive-service.ts
11601
12146
  var KeepAliveService = class _KeepAliveService {
11602
12147
  interval = null;
@@ -11872,7 +12417,7 @@ async function reconcileCanvasItems(filenames) {
11872
12417
 
11873
12418
  // src/services/upload-chat-transcripts.ts
11874
12419
  import { createReadStream } from "fs";
11875
- import { createHash as createHash2 } from "crypto";
12420
+ import { createHash as createHash3 } from "crypto";
11876
12421
  import { readFile as readFile16, readdir as readdir8, stat as stat4 } from "fs/promises";
11877
12422
  import { basename as basename2, join as join27 } from "path";
11878
12423
 
@@ -11943,7 +12488,7 @@ async function uploadChatTranscript(chatId, filePath, chat, capture) {
11943
12488
  deletedAt: chat.deletedAt ?? null
11944
12489
  } : {};
11945
12490
  if (capture) {
11946
- const hash = createHash2("sha256");
12491
+ const hash = createHash3("sha256");
11947
12492
  for await (const chunk of createReadStream(filePath)) hash.update(chunk);
11948
12493
  metadata.captureId = capture.captureId;
11949
12494
  metadata.captureReason = capture.reason;
@@ -12164,7 +12709,7 @@ function isCodexAvailable() {
12164
12709
  return detectCodexAuthMethod() !== "none";
12165
12710
  }
12166
12711
  function isOpencodeAvailable() {
12167
- return existsSync8(OPENCODE_AUTH_PATH);
12712
+ return existsSync9(OPENCODE_AUTH_PATH);
12168
12713
  }
12169
12714
  function isDeepseekAvailable() {
12170
12715
  return Boolean(getDeepseekApiKey());
@@ -12176,7 +12721,7 @@ function isKimiAvailable() {
12176
12721
  return Boolean(ENGINE_ENV.AI_GATEWAY_API_KEY);
12177
12722
  }
12178
12723
  function isPiAvailable() {
12179
- return existsSync8(PI_AUTH_PATH);
12724
+ return existsSync9(PI_AUTH_PATH);
12180
12725
  }
12181
12726
  function isCursorAvailable() {
12182
12727
  return Boolean(ENGINE_ENV.CURSOR_API_KEY);
@@ -12300,15 +12845,17 @@ function corruptChatsFilePath() {
12300
12845
  return `${CHATS_FILE}.corrupt-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
12301
12846
  }
12302
12847
  var ChatService = class {
12303
- constructor(workingDirectory, agentChatActivityTrackerService2) {
12848
+ constructor(workingDirectory, agentChatActivityTrackerService2, agentTokenUsageTracker2) {
12304
12849
  this.workingDirectory = workingDirectory;
12305
12850
  this.agentChatActivityTrackerService = agentChatActivityTrackerService2;
12851
+ this.agentTokenUsageTracker = agentTokenUsageTracker2;
12306
12852
  keepAliveService.setActivityCheck(
12307
12853
  () => [...this.chats.values()].some((chat) => !chat.persisted.deletedAt && hasActiveAgentWork(chat))
12308
12854
  );
12309
12855
  }
12310
12856
  workingDirectory;
12311
12857
  agentChatActivityTrackerService;
12858
+ agentTokenUsageTracker;
12312
12859
  chats = /* @__PURE__ */ new Map();
12313
12860
  persistPromise = null;
12314
12861
  persistQueued = false;
@@ -12528,7 +13075,7 @@ var ChatService = class {
12528
13075
  async interrupt(chatId) {
12529
13076
  const chat = this.requireChat(chatId);
12530
13077
  const result = await chat.provider.interrupt();
12531
- this.agentChatActivityTrackerService.noteTurnEnded(chatId);
13078
+ this.endTrackedTurn(chatId);
12532
13079
  chat.hasActiveTurn = false;
12533
13080
  chat.activeMessageId = null;
12534
13081
  chat.pendingMessageIds = [];
@@ -12550,7 +13097,7 @@ var ChatService = class {
12550
13097
  return { interrupted: false, queue: [], goal: null };
12551
13098
  }
12552
13099
  const interruptResult = await chat.provider.interrupt();
12553
- this.agentChatActivityTrackerService.noteTurnEnded(chatId);
13100
+ this.endTrackedTurn(chatId);
12554
13101
  chat.hasActiveTurn = false;
12555
13102
  chat.activeMessageId = null;
12556
13103
  chat.pendingMessageIds = [];
@@ -12677,7 +13224,7 @@ var ChatService = class {
12677
13224
  messageId,
12678
13225
  ...chat.pendingMessageIds.filter((pendingMessageId) => pendingMessageId !== messageId && pendingMessageId !== interruptedMessageId)
12679
13226
  ];
12680
- this.agentChatActivityTrackerService.noteTurnEnded(chatId);
13227
+ this.endTrackedTurn(chatId);
12681
13228
  chat.hasActiveTurn = false;
12682
13229
  chat.activeMessageId = null;
12683
13230
  if (interruptedMessageId) {
@@ -12830,12 +13377,15 @@ var ChatService = class {
12830
13377
  const chatsById = new Map(
12831
13378
  [...this.chats.entries()].map(([chatId, chat]) => [chatId, this.toSummary(chat)])
12832
13379
  );
12833
- const [chatTranscripts, canvas, repoState, engineLogs, chatActivity] = await Promise.all([
13380
+ const [chatTranscripts, canvas, repoState, engineLogs, [chatActivity]] = await Promise.all([
12834
13381
  flushAllChatTranscripts(chatsById, capture),
12835
13382
  flushAllCanvasItems(),
12836
13383
  flushRepoState(),
12837
13384
  flushAllEngineLogs(),
12838
- this.agentChatActivityTrackerService.flush()
13385
+ Promise.all([
13386
+ this.agentChatActivityTrackerService.flush(),
13387
+ this.agentTokenUsageTracker.flush()
13388
+ ])
12839
13389
  ]);
12840
13390
  return {
12841
13391
  chatTranscripts,
@@ -12865,6 +13415,9 @@ var ChatService = class {
12865
13415
  const onProviderEvent = (event) => {
12866
13416
  this.handleTurnEvent(persisted.id, event);
12867
13417
  };
13418
+ const onProviderUsage = (payload, occurredAt) => {
13419
+ this.agentTokenUsageTracker.noteUsage(persisted.id, payload, occurredAt);
13420
+ };
12868
13421
  const onMessageStarted = (message) => this.handleTurnStarted(persisted.id, message);
12869
13422
  const onProcessingChanged = (processing) => {
12870
13423
  if (processing) return;
@@ -12923,7 +13476,8 @@ var ChatService = class {
12923
13476
  onTurnComplete: onProviderTurnComplete,
12924
13477
  onEvent: onProviderEvent,
12925
13478
  onProcessingChanged,
12926
- onMessageStarted
13479
+ onMessageStarted,
13480
+ onUsageUpdate: onProviderUsage
12927
13481
  });
12928
13482
  } else if (persisted.provider === "deepseek") {
12929
13483
  provider = new DeepseekManager({
@@ -12934,7 +13488,8 @@ var ChatService = class {
12934
13488
  onTurnComplete: onProviderTurnComplete,
12935
13489
  onEvent: onProviderEvent,
12936
13490
  onProcessingChanged,
12937
- onMessageStarted
13491
+ onMessageStarted,
13492
+ onUsageUpdate: onProviderUsage
12938
13493
  });
12939
13494
  } else if (persisted.provider === "fx") {
12940
13495
  provider = new FxManager({
@@ -12967,7 +13522,8 @@ var ChatService = class {
12967
13522
  onTurnComplete: onProviderTurnComplete,
12968
13523
  onEvent: onProviderEvent,
12969
13524
  onProcessingChanged,
12970
- onMessageStarted
13525
+ onMessageStarted,
13526
+ onUsageUpdate: onProviderUsage
12971
13527
  });
12972
13528
  } else if (persisted.provider === "pi") {
12973
13529
  provider = new PiManager({
@@ -12978,9 +13534,15 @@ var ChatService = class {
12978
13534
  onTurnComplete: onProviderTurnComplete,
12979
13535
  onEvent: onProviderEvent,
12980
13536
  onProcessingChanged,
12981
- onMessageStarted
13537
+ onMessageStarted,
13538
+ onUsageUpdate: onProviderUsage
12982
13539
  });
12983
13540
  } else {
13541
+ const codexTokenUsage = new CodexTokenUsageAdapter(
13542
+ persisted.id,
13543
+ () => this.agentTokenUsageTracker.getTokenUsageContext(persisted.id),
13544
+ (event) => this.agentTokenUsageTracker.recordEvent(event)
13545
+ );
12984
13546
  provider = new CodexAspManager({
12985
13547
  workingDirectory: this.workingDirectory,
12986
13548
  historyFilePath: join29(CODEX_HISTORY_DIR, `${persisted.id}.jsonl`),
@@ -12989,7 +13551,11 @@ var ChatService = class {
12989
13551
  onTurnComplete: onProviderTurnComplete,
12990
13552
  onEvent: onProviderEvent,
12991
13553
  onProcessingChanged,
12992
- onMessageStarted
13554
+ onMessageStarted,
13555
+ onCodexTurnStarted: codexTokenUsage.onTurnStarted,
13556
+ onCodexTokenUsage: codexTokenUsage.onTokenUsage,
13557
+ onCodexModelRerouted: codexTokenUsage.onModelRerouted,
13558
+ onCodexTurnCompleted: codexTokenUsage.onTurnCompleted
12993
13559
  });
12994
13560
  }
12995
13561
  return {
@@ -13039,25 +13605,29 @@ var ChatService = class {
13039
13605
  }
13040
13606
  return chat;
13041
13607
  }
13608
+ activateTurn(chat, messageId) {
13609
+ chat.hasActiveTurn = true;
13610
+ chat.activeMessageId = messageId;
13611
+ const context = this.agentChatActivityTrackerService.noteTurnStarted(
13612
+ chat.persisted.id,
13613
+ messageId,
13614
+ chat.persisted.provider
13615
+ );
13616
+ this.agentTokenUsageTracker.startTurn(context);
13617
+ chat.activeErrorNotificationTarget = chat.errorNotificationTargets.get(messageId) ?? null;
13618
+ chat.errorNotificationTargets.delete(messageId);
13619
+ }
13042
13620
  async handleTurnStarted(chatId, message) {
13043
13621
  const chat = this.getRuntimeChat(chatId);
13044
13622
  if (!chat) return;
13045
13623
  chat.pendingMessageIds = chat.pendingMessageIds.filter((messageId) => messageId !== message.id);
13046
- chat.hasActiveTurn = true;
13047
- chat.activeMessageId = message.id;
13048
13624
  const { event: acceptedEvent, sender } = createTurnMetadata(
13049
13625
  message,
13050
13626
  message.id,
13051
13627
  message.queuedAt
13052
13628
  );
13053
13629
  chat.acceptedUserEvents.set(message.id, acceptedEvent);
13054
- this.agentChatActivityTrackerService.noteTurnStarted(
13055
- chat.persisted.id,
13056
- message.id,
13057
- chat.persisted.provider
13058
- );
13059
- chat.activeErrorNotificationTarget = chat.errorNotificationTargets.get(message.id) ?? null;
13060
- chat.errorNotificationTargets.delete(message.id);
13630
+ this.activateTurn(chat, message.id);
13061
13631
  await this.publish({
13062
13632
  type: "chat.turn.started",
13063
13633
  payload: {
@@ -13074,6 +13644,22 @@ var ChatService = class {
13074
13644
  return;
13075
13645
  }
13076
13646
  keepAliveService.noteActivity();
13647
+ if (!chat.hasActiveTurn && chat.pendingMessageIds.length > 0) {
13648
+ const messageId = chat.pendingMessageIds.shift();
13649
+ if (!messageId) {
13650
+ return;
13651
+ }
13652
+ this.activateTurn(chat, messageId);
13653
+ this.publish({
13654
+ type: "chat.turn.started",
13655
+ payload: {
13656
+ chatId: chat.persisted.id,
13657
+ messageId
13658
+ }
13659
+ }).catch(() => {
13660
+ });
13661
+ }
13662
+ this.agentTokenUsageTracker.noteAgentEvent(chatId, event);
13077
13663
  const codexTranscript = getCodexTranscriptFromEvent(event);
13078
13664
  if (chat.hasActiveTurn) {
13079
13665
  const displayMessages = codexTranscript ? parseLatestCodexAspTranscriptTurn(codexTranscript) : parseDisplayMessages([event], chat.persisted.provider, null, {
@@ -13134,6 +13720,10 @@ var ChatService = class {
13134
13720
  });
13135
13721
  }
13136
13722
  }
13723
+ endTrackedTurn(chatId) {
13724
+ this.agentTokenUsageTracker.endTurn(chatId);
13725
+ this.agentChatActivityTrackerService.noteTurnEnded(chatId);
13726
+ }
13137
13727
  async handleTurnFinished(chatId) {
13138
13728
  const chat = this.getRuntimeChat(chatId);
13139
13729
  if (!chat) {
@@ -13146,7 +13736,7 @@ var ChatService = class {
13146
13736
  }
13147
13737
  chat.hasActiveTurn = false;
13148
13738
  chat.activeMessageId = null;
13149
- this.agentChatActivityTrackerService.noteTurnEnded(chatId);
13739
+ this.endTrackedTurn(chatId);
13150
13740
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
13151
13741
  const processing = chat.provider.getQueue().length > 0 || (chat.provider.hasActiveBackgroundTasks?.() ?? false);
13152
13742
  this.publish({
@@ -13584,14 +14174,14 @@ var RepoFileService = class {
13584
14174
 
13585
14175
  // src/v1-routes.ts
13586
14176
  import { Hono } from "hono";
13587
- import { z as z6 } from "zod";
14177
+ import { z as z5 } from "zod";
13588
14178
  import { readdir as readdir11, stat as stat7, readFile as readFile21 } from "fs/promises";
13589
14179
  import { join as join33, resolve as resolve3 } from "path";
13590
14180
 
13591
14181
  // src/services/warm-hooks-service.ts
13592
14182
  import { spawn as spawn6 } from "child_process";
13593
14183
  import { readFile as readFile20 } from "fs/promises";
13594
- import { existsSync as existsSync9 } from "fs";
14184
+ import { existsSync as existsSync10 } from "fs";
13595
14185
  import { join as join32 } from "path";
13596
14186
 
13597
14187
  // src/services/warm-hook-logs-service.ts
@@ -13713,7 +14303,7 @@ var warmHookLogsService = new WarmHookLogsService();
13713
14303
  async function readRepoWarmHook(repoPath) {
13714
14304
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
13715
14305
  const configPath = join32(repoPath, filename);
13716
- if (!existsSync9(configPath)) {
14306
+ if (!existsSync10(configPath)) {
13717
14307
  continue;
13718
14308
  }
13719
14309
  try {
@@ -13974,7 +14564,7 @@ ${combinedScript}` : combinedScript;
13974
14564
 
13975
14565
  // src/services/terminal-service.ts
13976
14566
  import { randomUUID as randomUUID10 } from "crypto";
13977
- import { existsSync as existsSync10 } from "fs";
14567
+ import { existsSync as existsSync11 } from "fs";
13978
14568
  import { spawn as spawn7 } from "node-pty";
13979
14569
  var MAX_REPLAY_CHARS = 1024 * 1024;
13980
14570
  var MAX_TERMINAL_SESSIONS = 8;
@@ -13993,7 +14583,7 @@ var TerminalService = class {
13993
14583
  });
13994
14584
  }
13995
14585
  const id = randomUUID10();
13996
- const shell = process.env.SHELL && existsSync10(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
14586
+ const shell = process.env.SHELL && existsSync11(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
13997
14587
  const pty = spawn7(shell, ["-l"], {
13998
14588
  name: "xterm-256color",
13999
14589
  cols,
@@ -14092,72 +14682,26 @@ var TerminalService = class {
14092
14682
  var terminalService = new TerminalService();
14093
14683
 
14094
14684
  // src/v1-routes.ts
14095
- var imageMediaTypeSchema = z6.enum(IMAGE_MEDIA_TYPES);
14096
- var createPreviewSchema = z6.object({
14097
- port: z6.number().int().min(1).max(65535),
14098
- publicUrl: z6.string().min(1)
14099
- });
14100
- var terminalSizeSchema = z6.object({
14101
- cols: z6.number().int().min(2).max(500),
14102
- rows: z6.number().int().min(1).max(200)
14685
+ var createPreviewSchema = z5.object({
14686
+ port: z5.number().int().min(1).max(65535),
14687
+ publicUrl: z5.string().min(1)
14103
14688
  });
14104
- var writeTerminalSessionSchema = z6.object({
14105
- data: z6.string().max(64 * 1024),
14106
- generation: z6.number().int().nonnegative(),
14107
- sequence: z6.number().int().nonnegative()
14689
+ var terminalSizeSchema = z5.object({
14690
+ cols: z5.number().int().min(2).max(500),
14691
+ rows: z5.number().int().min(1).max(200)
14108
14692
  });
14109
- var sendMessageSchema = z6.object({
14110
- messageId: z6.string().min(1).optional(),
14111
- submittedAt: z6.string().datetime().optional(),
14112
- message: z6.string().min(1),
14113
- model: z6.string().optional(),
14114
- customInstructions: z6.string().optional(),
14115
- planMode: z6.boolean().optional(),
14116
- images: z6.array(z6.object({
14117
- type: z6.literal("image"),
14118
- source: z6.union([
14119
- z6.object({
14120
- type: z6.literal("base64"),
14121
- media_type: imageMediaTypeSchema,
14122
- data: z6.string().min(1)
14123
- }),
14124
- z6.object({
14125
- type: z6.literal("url"),
14126
- url: z6.string().url()
14127
- })
14128
- ])
14129
- })).optional(),
14130
- thinkingLevel: z6.enum(VALID_THINKING_LEVELS).optional(),
14131
- goalMode: z6.boolean().optional(),
14132
- fastMode: z6.boolean().optional(),
14133
- enableInteractiveTools: z6.boolean().optional(),
14134
- type: z6.string().min(1).optional(),
14135
- merge: z6.boolean().optional(),
14136
- idempotencyKey: z6.string().min(1).max(128).optional(),
14137
- senderUserId: z6.string().optional(),
14138
- senderEmail: z6.string().optional(),
14139
- senderDisplayName: z6.string().optional(),
14140
- senderAvatarUrl: z6.string().optional(),
14141
- errorNotificationTarget: z6.discriminatedUnion("type", [
14142
- z6.object({ type: z6.literal("slack") }),
14143
- z6.object({ type: z6.literal("linear"), sessionId: z6.string().min(1) }),
14144
- z6.object({
14145
- type: z6.literal("code_host"),
14146
- provider: z6.enum(["github", "gitlab"]),
14147
- resource: z6.enum(["issue", "pull_request"]),
14148
- repositoryId: z6.string().min(1),
14149
- resourceNumber: z6.number().int().positive()
14150
- }),
14151
- z6.object({ type: z6.literal("automation"), executionId: z6.string().min(1) })
14152
- ]).optional()
14693
+ var writeTerminalSessionSchema = z5.object({
14694
+ data: z5.string().max(64 * 1024),
14695
+ generation: z5.number().int().nonnegative(),
14696
+ sequence: z5.number().int().nonnegative()
14153
14697
  });
14154
- var respondToolInputSchema = z6.object({
14155
- requestId: z6.string().min(1),
14156
- selectionId: z6.string().min(1)
14698
+ var respondToolInputSchema = z5.object({
14699
+ requestId: z5.string().min(1),
14700
+ selectionId: z5.string().min(1)
14157
14701
  });
14158
- var updateGoalSchema = z6.object({
14159
- objective: z6.string().trim().min(1).max(MAX_CODEX_GOAL_OBJECTIVE_CHARS).optional(),
14160
- status: z6.enum(["active", "paused"]).optional()
14702
+ var updateGoalSchema = z5.object({
14703
+ objective: z5.string().trim().min(1).max(MAX_CODEX_GOAL_OBJECTIVE_CHARS).optional(),
14704
+ status: z5.enum(["active", "paused"]).optional()
14161
14705
  }).refine((body) => body.objective !== void 0 || body.status !== void 0, {
14162
14706
  message: "Goal objective or status required"
14163
14707
  });
@@ -14332,7 +14876,7 @@ function createV1Routes(deps) {
14332
14876
  });
14333
14877
  app2.post("/chats/:chatId/messages", async (c) => {
14334
14878
  try {
14335
- const body = sendMessageSchema.parse(await c.req.json());
14879
+ const body = sendChatMessageRequestSchema.strip().parse(await c.req.json());
14336
14880
  const goalValidationError = getGoalCommandObjectiveValidationError(body.message, body.goalMode);
14337
14881
  if (goalValidationError) {
14338
14882
  return c.json(jsonError(goalValidationError), 400);
@@ -14371,7 +14915,7 @@ function createV1Routes(deps) {
14371
14915
  const result = await deps.chatService.updateGoal(c.req.param("chatId"), body);
14372
14916
  return c.json(result);
14373
14917
  } catch (error) {
14374
- if (error instanceof z6.ZodError) {
14918
+ if (error instanceof z5.ZodError) {
14375
14919
  return c.json(jsonError(error.issues[0]?.message || "Invalid goal update"), 400);
14376
14920
  }
14377
14921
  if (error instanceof ChatNotFoundError) {
@@ -15114,7 +15658,7 @@ async function timeStartupStep(name, fn) {
15114
15658
  async function waitForInitializationGate() {
15115
15659
  if (!ENGINE_ENV.REPLICAS_ENGINE_DEFER_INITIALIZATION) return;
15116
15660
  const deadline = Date.now() + ENGINE_INIT_GATE_TIMEOUT_MS;
15117
- while (!existsSync11(SANDBOX_PATHS.ENGINE_INIT_GATE)) {
15661
+ while (!existsSync12(SANDBOX_PATHS.ENGINE_INIT_GATE)) {
15118
15662
  if (Date.now() >= deadline) {
15119
15663
  throw new Error("Timed out waiting for deferred engine initialization gate");
15120
15664
  }
@@ -15179,7 +15723,12 @@ var authMiddleware = async (c, next) => {
15179
15723
  await next();
15180
15724
  };
15181
15725
  var agentChatActivityTrackerService = new AgentChatActivityTrackerService();
15182
- var chatService = new ChatService(gitService.getWorkspaceRoot(), agentChatActivityTrackerService);
15726
+ var agentTokenUsageTracker = new AgentTokenUsageTracker(uploadAnalytics);
15727
+ var chatService = new ChatService(
15728
+ gitService.getWorkspaceRoot(),
15729
+ agentChatActivityTrackerService,
15730
+ agentTokenUsageTracker
15731
+ );
15183
15732
  app.get("/health", async (c) => {
15184
15733
  const requestedWaitMs = Number(c.req.query(ENGINE_HEALTH_WAIT_QUERY_PARAM));
15185
15734
  const waitMs = Number.isFinite(requestedWaitMs) ? Math.min(Math.max(requestedWaitMs, 0), ENGINE_HEALTH_MAX_WAIT_MS) : 0;