replicas-engine 0.1.552 → 0.1.553

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/README.md CHANGED
@@ -135,3 +135,5 @@ Outgoing endpoints:
135
135
  - `POST /v1/engine/codex/refresh-credentials`
136
136
  - `POST /v1/engine/infisical/refresh-token`
137
137
  - `POST /v1/engine/chat-turn-usage`
138
+ - `POST /v1/engine/skill-usage`
139
+ - `POST /v1/engine/mcp-usage`
package/dist/src/index.js CHANGED
@@ -260,7 +260,10 @@ var AGENT_CREDENTIAL_METHODS_IN_ORDER = {
260
260
  ]
261
261
  };
262
262
 
263
- // ../shared/src/chat-turn-usage.ts
263
+ // ../shared/src/analytics/types.ts
264
+ function isUsageRecord(value) {
265
+ return isRecord(value) && typeof value.chatId === "string" && typeof value.provider === "string" && isValidAgentProvider(value.provider) && typeof value.model === "string" && (value.credentialMethod === void 0 || isAuthMethod(value.credentialMethod)) && (value.credentialScope === void 0 || isCredentialScope(value.credentialScope)) && value.credentialMethod === void 0 === (value.credentialScope === void 0) && (value.senderUserId === void 0 || typeof value.senderUserId === "string") && typeof value.occurredAt === "string";
266
+ }
264
267
  function isAuthMethod(value) {
265
268
  return typeof value === "string" && Object.values(CREDENTIAL_METHOD).some((method) => method === value);
266
269
  }
@@ -268,7 +271,21 @@ function isCredentialScope(value) {
268
271
  return typeof value === "string" && CREDENTIAL_SCOPES.some((scope) => scope === value);
269
272
  }
270
273
  function isChatTurnUsageRecord(value) {
271
- return isRecord(value) && typeof value.turnId === "string" && typeof value.chatId === "string" && typeof value.provider === "string" && isValidAgentProvider(value.provider) && typeof value.model === "string" && (value.credentialMethod === void 0 || isAuthMethod(value.credentialMethod)) && (value.credentialScope === void 0 || isCredentialScope(value.credentialScope)) && value.credentialMethod === void 0 === (value.credentialScope === void 0) && (value.senderUserId === void 0 || typeof value.senderUserId === "string") && typeof value.occurredAt === "string" && typeof value.seconds === "number" && Number.isFinite(value.seconds) && value.seconds >= 0;
274
+ return isUsageRecord(value) && typeof value.turnId === "string" && typeof value.seconds === "number" && Number.isFinite(value.seconds) && value.seconds >= 0;
275
+ }
276
+ function isCallUsageRecord(value) {
277
+ return isUsageRecord(value) && typeof value.callId === "string" && value.callId.length > 0;
278
+ }
279
+ function isNamedCallUsageRecord(value, field) {
280
+ if (!isCallUsageRecord(value)) return false;
281
+ const name = value[field];
282
+ return typeof name === "string" && name.length > 0;
283
+ }
284
+ function isSkillUsageRecord(value) {
285
+ return isNamedCallUsageRecord(value, "skillName");
286
+ }
287
+ function isMcpUsageRecord(value) {
288
+ return isNamedCallUsageRecord(value, "mcpName");
272
289
  }
273
290
 
274
291
  // ../shared/src/aster.ts
@@ -708,7 +725,7 @@ var WORKSPACE_SIZES = ["small", "large"];
708
725
  var INVALID_WORKSPACE_SIZE_ERROR = `Invalid size: must be one of ${WORKSPACE_SIZES.join(", ")}`;
709
726
 
710
727
  // ../shared/src/e2b.ts
711
- var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-08-03-v2";
728
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-08-03-v3";
712
729
 
713
730
  // ../shared/src/runtime-env.ts
714
731
  function shellQuotePosix(value) {
@@ -2809,6 +2826,16 @@ var DEFAULT_DEFAULT_SKILLS = {
2809
2826
  }
2810
2827
  };
2811
2828
 
2829
+ // ../shared/src/prompts.ts
2830
+ var REPLICAS_INSTRUCTIONS_TAG = "replicas_instructions";
2831
+ function removeTag(text, tag) {
2832
+ const regex = new RegExp(`<${tag}>[\\s\\S]*?</${tag}>`, "g");
2833
+ return text.replace(regex, "").trim();
2834
+ }
2835
+ function removeReplicasInstructions(text) {
2836
+ return removeTag(text, REPLICAS_INSTRUCTIONS_TAG);
2837
+ }
2838
+
2812
2839
  // ../shared/src/replicas-config.ts
2813
2840
  import { parse as parseYaml } from "yaml";
2814
2841
 
@@ -3560,7 +3587,17 @@ function isTerminalBackgroundTaskStatus(status) {
3560
3587
  // ../shared/src/display-message/constants.ts
3561
3588
  var USER_MESSAGE_MATCH_GRACE_PERIOD_MS = 3e4;
3562
3589
 
3590
+ // ../shared/src/json.ts
3591
+ function safeJsonParse(str, fallback) {
3592
+ try {
3593
+ return JSON.parse(str);
3594
+ } catch {
3595
+ return fallback;
3596
+ }
3597
+ }
3598
+
3563
3599
  // ../shared/src/display-message/parsers/utils.ts
3600
+ var INTERRUPTED_MESSAGE_REGEX = /^\[Request interrupted by user.*\]$/;
3564
3601
  function userMessageImages(value) {
3565
3602
  if (!Array.isArray(value)) return void 0;
3566
3603
  const images = value.filter((item) => isRecord(item) && item.type === "image" && typeof item.mediaType === "string" && typeof item.data === "string");
@@ -3575,6 +3612,32 @@ function stringifyDisplayValue(value) {
3575
3612
  return String(value);
3576
3613
  }
3577
3614
  }
3615
+ function upsertDisplayMessage(messages, message) {
3616
+ const index = messages.findIndex((candidate) => candidate.id === message.id);
3617
+ if (index === -1) messages.push(message);
3618
+ else messages[index] = message;
3619
+ }
3620
+ function parseSkillName(tool2, input) {
3621
+ if (typeof tool2 !== "string") return null;
3622
+ const normalizedTool = tool2.toLowerCase();
3623
+ if (normalizedTool.startsWith("skill.")) return tool2.slice("skill.".length) || null;
3624
+ if (normalizedTool !== "skill") return null;
3625
+ const parsedInput = typeof input === "string" ? safeJsonParse(input, null) : input;
3626
+ if (!isRecord(parsedInput)) return null;
3627
+ const skillName = parsedInput.skill ?? parsedInput.name;
3628
+ return typeof skillName === "string" && skillName ? skillName : null;
3629
+ }
3630
+ function createCallDisplayMessage(message) {
3631
+ const skillName = message.server.toLowerCase() === "skills" ? message.tool : parseSkillName(message.tool, message.input);
3632
+ if (skillName === null) return { ...message, type: "tool_call" };
3633
+ return {
3634
+ id: message.id,
3635
+ type: "skill",
3636
+ skillName,
3637
+ status: message.status,
3638
+ timestamp: message.timestamp
3639
+ };
3640
+ }
3578
3641
 
3579
3642
  // ../shared/src/user-message-matching.ts
3580
3643
  function parseTimestampMs(timestamp) {
@@ -3656,15 +3719,6 @@ function parseAgentEventJsonlWithCodexAspTranscript(content, options = {}) {
3656
3719
  return { events, transcript, transcriptsByThreadId };
3657
3720
  }
3658
3721
 
3659
- // ../shared/src/json.ts
3660
- function safeJsonParse(str, fallback) {
3661
- try {
3662
- return JSON.parse(str);
3663
- } catch {
3664
- return fallback;
3665
- }
3666
- }
3667
-
3668
3722
  // ../shared/src/display-message/parsers/codex-parser.ts
3669
3723
  function getStatusFromExitCode(exitCode) {
3670
3724
  return exitCode === 0 ? "completed" : "failed";
@@ -3831,17 +3885,17 @@ function parseCodexEvents(events) {
3831
3885
  const operations = parsePatch(input);
3832
3886
  pendingPatches.set(callId, { input, status, timestamp: event.timestamp, operations });
3833
3887
  } else {
3834
- const msg = {
3835
- id: `toolcall-${callId || getPayloadString(event, CODEX_ASP_ITEM_ID_PAYLOAD_KEY) || `${event.timestamp}-${eventIndex}`}`,
3836
- type: "tool_call",
3888
+ const id = `toolcall-${callId || getPayloadString(event, CODEX_ASP_ITEM_ID_PAYLOAD_KEY) || `${event.timestamp}-${eventIndex}`}`;
3889
+ const msg = createCallDisplayMessage({
3890
+ id,
3837
3891
  server,
3838
3892
  tool: name,
3839
3893
  input,
3840
3894
  status,
3841
3895
  timestamp: event.timestamp
3842
- };
3896
+ });
3843
3897
  messages.push(msg);
3844
- if (callId) {
3898
+ if (callId && msg.type === "tool_call") {
3845
3899
  pendingToolCalls.set(callId, msg);
3846
3900
  }
3847
3901
  }
@@ -3925,10 +3979,10 @@ function getCursorThinkingText(event) {
3925
3979
  function isTerminalStatus(status) {
3926
3980
  return status === "FINISHED" || status === "ERROR" || status === "CANCELLED" || status === "EXPIRED";
3927
3981
  }
3928
- function finalizeOpenTools(messages, toolIndexes, status) {
3929
- for (const index of toolIndexes.values()) {
3982
+ function finalizeOpenCalls(messages, callIndexes, status) {
3983
+ for (const index of callIndexes.values()) {
3930
3984
  const message = messages[index];
3931
- if (message?.type === "tool_call" && message.status === "in_progress") {
3985
+ if ((message?.type === "tool_call" || message?.type === "skill") && message.status === "in_progress") {
3932
3986
  messages[index] = {
3933
3987
  ...message,
3934
3988
  status
@@ -3938,7 +3992,7 @@ function finalizeOpenTools(messages, toolIndexes, status) {
3938
3992
  }
3939
3993
  function parseCursorEvents(events) {
3940
3994
  const messages = [];
3941
- const toolIndexes = /* @__PURE__ */ new Map();
3995
+ const callIndexes = /* @__PURE__ */ new Map();
3942
3996
  const runAssistantSegments = /* @__PURE__ */ new Map();
3943
3997
  const runThinkingSegments = /* @__PURE__ */ new Map();
3944
3998
  let activeAssistant = null;
@@ -4030,21 +4084,20 @@ function parseCursorEvents(events) {
4030
4084
  const tool2 = typeof event.payload.name === "string" ? event.payload.name : "tool";
4031
4085
  const status = cursorStatusToDisplayStatus(event.payload.status);
4032
4086
  const input = isRecord(event.payload.args) ? event.payload.args : typeof event.payload.args === "string" ? event.payload.args : void 0;
4033
- const existing = toolIndexes.get(callId);
4034
- const next = {
4087
+ const existing = callIndexes.get(callId);
4088
+ const next = createCallDisplayMessage({
4035
4089
  id: callId,
4036
- type: "tool_call",
4037
4090
  server: "cursor",
4038
4091
  tool: tool2,
4039
4092
  ...input !== void 0 ? { input } : {},
4040
4093
  output: stringifyDisplayValue(event.payload.result),
4041
4094
  status,
4042
4095
  timestamp: event.timestamp
4043
- };
4096
+ });
4044
4097
  if (existing === void 0) {
4045
- toolIndexes.set(callId, messages.push(next) - 1);
4098
+ callIndexes.set(callId, messages.push(next) - 1);
4046
4099
  } else {
4047
- messages[existing] = { ...messages[existing], ...next };
4100
+ messages[existing] = next;
4048
4101
  }
4049
4102
  continue;
4050
4103
  }
@@ -4066,7 +4119,7 @@ function parseCursorEvents(events) {
4066
4119
  if (isTerminalStatus(status)) {
4067
4120
  const displayStatus = cursorStatusToDisplayStatus(status);
4068
4121
  finalizeActiveThinking(displayStatus);
4069
- finalizeOpenTools(messages, toolIndexes, displayStatus);
4122
+ finalizeOpenCalls(messages, callIndexes, displayStatus);
4070
4123
  activeThinking = null;
4071
4124
  activeAssistant = null;
4072
4125
  }
@@ -4074,7 +4127,7 @@ function parseCursorEvents(events) {
4074
4127
  }
4075
4128
  if (event.type === "cursor-error") {
4076
4129
  finalizeActiveThinking("failed");
4077
- finalizeOpenTools(messages, toolIndexes, "failed");
4130
+ finalizeOpenCalls(messages, callIndexes, "failed");
4078
4131
  activeAssistant = null;
4079
4132
  activeThinking = null;
4080
4133
  const message = typeof event.payload.message === "string" ? event.payload.message : "Cursor run failed";
@@ -4099,14 +4152,6 @@ function partFromEvent(event) {
4099
4152
  function toolStatus(status) {
4100
4153
  return status === "completed" ? "completed" : status === "error" ? "failed" : "in_progress";
4101
4154
  }
4102
- function setById(messages, id, next) {
4103
- const index = messages.findIndex((message) => message.id === id);
4104
- if (index === -1) {
4105
- messages.push(next);
4106
- } else {
4107
- messages[index] = next;
4108
- }
4109
- }
4110
4155
  function assistantError(info) {
4111
4156
  if (!isRecord(info) || !isRecord(info.error)) return null;
4112
4157
  const data = isRecord(info.error.data) ? info.error.data : null;
@@ -4151,15 +4196,32 @@ function parseOpencodeEvents(events) {
4151
4196
  });
4152
4197
  continue;
4153
4198
  }
4199
+ if (event.type === "opencode-session.next.tool.called") {
4200
+ const tool2 = typeof event.payload.tool === "string" ? event.payload.tool : "tool";
4201
+ const input = event.payload.input ?? event.payload.arguments;
4202
+ const callId = typeof event.payload.callID === "string" ? event.payload.callID : null;
4203
+ const id2 = callId ? `opencode-${callId}` : `opencode-${event.timestamp}-${messages.length}`;
4204
+ upsertDisplayMessage(messages, createCallDisplayMessage({
4205
+ id: id2,
4206
+ server: "opencode",
4207
+ tool: tool2,
4208
+ input: isRecord(input) ? input : stringifyDisplayValue(input),
4209
+ status: "in_progress",
4210
+ timestamp: event.timestamp
4211
+ }));
4212
+ continue;
4213
+ }
4154
4214
  const part = partFromEvent(event);
4155
4215
  if (!part) continue;
4156
- const id = typeof part.id === "string" ? `opencode-${part.id}` : `opencode-${event.timestamp}-${messages.length}`;
4216
+ let id = `opencode-${event.timestamp}-${messages.length}`;
4217
+ if (typeof part.callID === "string") id = `opencode-${part.callID}`;
4218
+ else if (typeof part.id === "string") id = `opencode-${part.id}`;
4157
4219
  const time = isRecord(part.time) ? part.time : null;
4158
4220
  const timestamp = timestampFromMs(time?.start, event.timestamp);
4159
4221
  if (part.type === "text") {
4160
4222
  const text = typeof part.text === "string" ? part.text : "";
4161
4223
  if (text.trim()) {
4162
- setById(messages, id, {
4224
+ upsertDisplayMessage(messages, {
4163
4225
  id,
4164
4226
  type: "agent",
4165
4227
  content: text,
@@ -4171,7 +4233,7 @@ function parseOpencodeEvents(events) {
4171
4233
  if (part.type === "reasoning") {
4172
4234
  const text = typeof part.text === "string" ? part.text : "";
4173
4235
  if (text.trim()) {
4174
- setById(messages, id, {
4236
+ upsertDisplayMessage(messages, {
4175
4237
  id,
4176
4238
  type: "reasoning",
4177
4239
  content: text,
@@ -4184,23 +4246,24 @@ function parseOpencodeEvents(events) {
4184
4246
  if (part.type === "tool" && isRecord(part.state)) {
4185
4247
  const state = part.state;
4186
4248
  const status = toolStatus(state.status);
4249
+ const tool2 = typeof part.tool === "string" ? part.tool : "tool";
4250
+ const input = state.input;
4187
4251
  const output = state.status === "completed" ? stringifyDisplayValue(state.output) : state.status === "error" ? stringifyDisplayValue(state.error) : void 0;
4188
- setById(messages, id, {
4252
+ upsertDisplayMessage(messages, createCallDisplayMessage({
4189
4253
  id,
4190
- type: "tool_call",
4191
4254
  server: "opencode",
4192
- tool: typeof part.tool === "string" ? part.tool : "tool",
4193
- input: isRecord(state.input) ? state.input : stringifyDisplayValue(state.input),
4255
+ tool: tool2,
4256
+ input: isRecord(input) ? input : stringifyDisplayValue(input),
4194
4257
  output,
4195
4258
  status,
4196
4259
  timestamp
4197
- });
4260
+ }));
4198
4261
  continue;
4199
4262
  }
4200
4263
  if (part.type === "patch") {
4201
4264
  const files = Array.isArray(part.files) ? part.files.filter((file) => typeof file === "string") : [];
4202
4265
  if (files.length > 0) {
4203
- setById(messages, id, {
4266
+ upsertDisplayMessage(messages, {
4204
4267
  id,
4205
4268
  type: "file_change",
4206
4269
  changes: files.map((path6) => ({ path: path6, kind: "update" })),
@@ -4217,11 +4280,6 @@ function parseOpencodeEvents(events) {
4217
4280
  function nestedPayload(event) {
4218
4281
  return isRecord(event.payload.assistantMessageEvent) ? event.payload.assistantMessageEvent : event.payload;
4219
4282
  }
4220
- function setById2(messages, id, message) {
4221
- const index = messages.findIndex((candidate) => candidate.id === id);
4222
- if (index === -1) messages.push(message);
4223
- else messages[index] = message;
4224
- }
4225
4283
  function toolStatus2(value) {
4226
4284
  return value === "completed" || value === "success" ? "completed" : value === "error" ? "failed" : "in_progress";
4227
4285
  }
@@ -4257,8 +4315,8 @@ function parsePiEvents(events) {
4257
4315
  if (payload.type === "thinking_delta" && typeof payload.delta === "string") thinking.set(id, `${thinking.get(id) ?? ""}${payload.delta}`);
4258
4316
  const textContent = text.get(id);
4259
4317
  const thinkingContent = thinking.get(id);
4260
- if (textContent !== void 0) setById2(messages, `pi-${id}`, { id: `pi-${id}`, type: "agent", content: textContent, timestamp: event.timestamp });
4261
- if (thinkingContent !== void 0) setById2(messages, `pi-thinking-${id}`, { id: `pi-thinking-${id}`, type: "reasoning", content: thinkingContent, status: "in_progress", timestamp: event.timestamp });
4318
+ if (textContent !== void 0) upsertDisplayMessage(messages, { id: `pi-${id}`, type: "agent", content: textContent, timestamp: event.timestamp });
4319
+ if (thinkingContent !== void 0) upsertDisplayMessage(messages, { id: `pi-thinking-${id}`, type: "reasoning", content: thinkingContent, status: "in_progress", timestamp: event.timestamp });
4262
4320
  continue;
4263
4321
  }
4264
4322
  if (event.type === "pi-message_start") {
@@ -4270,16 +4328,18 @@ function parsePiEvents(events) {
4270
4328
  const payload = event.payload;
4271
4329
  const input = payload.args ?? payload.input;
4272
4330
  const id = typeof payload.toolCallId === "string" ? payload.toolCallId : typeof payload.id === "string" ? payload.id : `tool-${event.timestamp}`;
4273
- setById2(messages, `pi-tool-${id}`, {
4274
- id: `pi-tool-${id}`,
4275
- type: "tool_call",
4331
+ const tool2 = typeof payload.toolName === "string" ? payload.toolName : "tool";
4332
+ const messageId = `pi-tool-${id}`;
4333
+ const status = event.type === "pi-tool_execution_end" ? toolStatus2(payload.isError ? "error" : "completed") : "in_progress";
4334
+ upsertDisplayMessage(messages, createCallDisplayMessage({
4335
+ id: messageId,
4276
4336
  server: "pi",
4277
- tool: typeof payload.toolName === "string" ? payload.toolName : "tool",
4337
+ tool: tool2,
4278
4338
  input: isRecord(input) ? input : stringifyDisplayValue(input),
4279
4339
  output: event.type === "pi-tool_execution_end" ? stringifyDisplayValue(payload.result ?? payload.error) : void 0,
4280
- status: event.type === "pi-tool_execution_end" ? toolStatus2(payload.isError ? "error" : "completed") : "in_progress",
4340
+ status,
4281
4341
  timestamp: event.timestamp
4282
- });
4342
+ }));
4283
4343
  }
4284
4344
  }
4285
4345
  return messages;
@@ -4443,14 +4503,6 @@ function isClaudeResultError(payload) {
4443
4503
  if (payload.errors?.length && stripAgentDiagnosticErrors(payload.errors).length === 0) return false;
4444
4504
  return Boolean(payload.is_error) || payload.subtype !== "success";
4445
4505
  }
4446
- function upsertDisplayMessage(messages, message) {
4447
- const index = messages.findIndex((existing) => existing.id === message.id);
4448
- if (index === -1) {
4449
- messages.push(message);
4450
- } else {
4451
- messages[index] = message;
4452
- }
4453
- }
4454
4506
  var LOCAL_COMMAND_ECHO_REGEX = /^<(?:command-name|command-message|local-command-stdout|local-command-stderr)>/;
4455
4507
  function parseClaudeEvents(events, parentToolUseId) {
4456
4508
  const messages = [];
@@ -4733,7 +4785,7 @@ function parseClaudeEvents(events, parentToolUseId) {
4733
4785
  } else if (toolName === "Skill") {
4734
4786
  const inputObj = typeof toolInput === "string" ? safeJsonParse(toolInput, {}) : toolInput;
4735
4787
  messages.push({
4736
- id: `skill-${event.timestamp}-${messages.length}`,
4788
+ id: `skill-${toolUseId}`,
4737
4789
  type: "skill",
4738
4790
  skillName: inputObj.skill || "unknown",
4739
4791
  args: inputObj.args,
@@ -4942,8 +4994,196 @@ function parseClaudeEvents(events, parentToolUseId) {
4942
4994
 
4943
4995
  // ../shared/src/display-message/parsers/codex-asp-parser.ts
4944
4996
  var DUPLICATE_WINDOW_MS = 5 * 60 * 1e3;
4997
+ function nearTimestamp(a, b) {
4998
+ return Math.abs(parseTimestampMs(a) - parseTimestampMs(b)) <= DUPLICATE_WINDOW_MS;
4999
+ }
5000
+ function inputForDisplay(input) {
5001
+ if (input === void 0 || input === null) return void 0;
5002
+ if (typeof input === "string") return input;
5003
+ if (isRecord(input)) return input;
5004
+ return String(input);
5005
+ }
5006
+ function isInternalUserMessage(content) {
5007
+ const trimmed = content.trim();
5008
+ return trimmed.startsWith("<environment_context>") || trimmed.startsWith("<goal_context>");
5009
+ }
5010
+ function messageForItem(item) {
5011
+ if (item.type === "userMessage") {
5012
+ if (isInternalUserMessage(item.content)) return null;
5013
+ return {
5014
+ id: `user-${item.id}`,
5015
+ type: "user",
5016
+ content: item.content,
5017
+ images: item.images,
5018
+ timestamp: item.timestamp
5019
+ };
5020
+ }
5021
+ if (item.type === "agentMessage") {
5022
+ if (!item.text) return null;
5023
+ return {
5024
+ id: `agent-${item.id}`,
5025
+ type: "agent",
5026
+ content: item.text,
5027
+ timestamp: item.timestamp
5028
+ };
5029
+ }
5030
+ if (item.type === "reasoning") {
5031
+ if (!item.text || item.status === "completed") return null;
5032
+ return {
5033
+ id: `reasoning-${item.id}`,
5034
+ type: "reasoning",
5035
+ content: item.text,
5036
+ status: normalizeCodexAspTranscriptStatus(item.status),
5037
+ timestamp: item.timestamp
5038
+ };
5039
+ }
5040
+ if (item.type === "commandExecution") {
5041
+ return {
5042
+ id: `command-${item.id}`,
5043
+ type: "command",
5044
+ command: item.command,
5045
+ output: item.output,
5046
+ exitCode: item.exitCode ?? void 0,
5047
+ status: normalizeCodexAspTranscriptStatus(item.status),
5048
+ timestamp: item.timestamp
5049
+ };
5050
+ }
5051
+ if (item.type === "fileChange") {
5052
+ return {
5053
+ id: `patch-${item.id}`,
5054
+ type: "patch",
5055
+ operations: item.operations,
5056
+ output: item.output,
5057
+ exitCode: item.exitCode ?? void 0,
5058
+ status: normalizeCodexAspTranscriptStatus(item.status),
5059
+ timestamp: item.timestamp
5060
+ };
5061
+ }
5062
+ if (item.type === "toolCall") {
5063
+ return createCallDisplayMessage({
5064
+ id: `toolcall-${item.id}`,
5065
+ server: item.server,
5066
+ tool: item.tool,
5067
+ input: inputForDisplay(item.input),
5068
+ output: item.output,
5069
+ status: normalizeCodexAspTranscriptStatus(item.status),
5070
+ timestamp: item.timestamp
5071
+ });
5072
+ }
5073
+ if (item.type === "subagent") {
5074
+ return {
5075
+ id: `subagent-${item.id}`,
5076
+ type: "subagent",
5077
+ toolUseId: item.id,
5078
+ description: item.description,
5079
+ prompt: item.prompt,
5080
+ subagentType: item.subagentType,
5081
+ ...item.receiverThreadIds ? { receiverThreadIds: item.receiverThreadIds } : {},
5082
+ ...item.model ? { model: item.model } : {},
5083
+ status: normalizeCodexAspTranscriptStatus(item.status),
5084
+ ...item.output ? { output: item.output } : {},
5085
+ nestedEvents: [],
5086
+ timestamp: item.timestamp
5087
+ };
5088
+ }
5089
+ if (item.type === "webSearch") {
5090
+ return {
5091
+ id: `web-search-${item.id}`,
5092
+ type: "web_search",
5093
+ query: item.query,
5094
+ status: normalizeCodexAspTranscriptStatus(item.status),
5095
+ timestamp: item.timestamp
5096
+ };
5097
+ }
5098
+ if (item.type === "plan") {
5099
+ if (!item.text) return null;
5100
+ return {
5101
+ id: `todo-${item.id}`,
5102
+ type: "todo_list",
5103
+ items: item.text.split("\n").map((line) => line.trim()).filter(Boolean).map((text) => ({ text, completed: item.status === "completed" })),
5104
+ status: normalizeCodexAspTranscriptStatus(item.status),
5105
+ timestamp: item.timestamp
5106
+ };
5107
+ }
5108
+ if (item.type === "contextCompaction") {
5109
+ return {
5110
+ id: `reasoning-${item.id}`,
5111
+ type: "reasoning",
5112
+ content: "Context compacted",
5113
+ status: normalizeCodexAspTranscriptStatus(item.status),
5114
+ timestamp: item.timestamp
5115
+ };
5116
+ }
5117
+ return {
5118
+ id: `error-${item.id}`,
5119
+ type: "error",
5120
+ message: item.message,
5121
+ timestamp: item.timestamp
5122
+ };
5123
+ }
5124
+ function stableString(value) {
5125
+ if (value === void 0) return "";
5126
+ if (typeof value !== "object" || value === null) return String(value);
5127
+ try {
5128
+ return JSON.stringify(value, Object.keys(value).sort());
5129
+ } catch {
5130
+ return String(value);
5131
+ }
5132
+ }
5133
+ function duplicateMessage(a, b) {
5134
+ if (a.id === b.id) return true;
5135
+ if (a.type !== b.type || !nearTimestamp(a.timestamp, b.timestamp)) {
5136
+ return false;
5137
+ }
5138
+ if (a.type === "user" && b.type === "user") return a.content === b.content;
5139
+ if (a.type === "agent" && b.type === "agent") return a.content === b.content;
5140
+ if (a.type === "reasoning" && b.type === "reasoning") return a.content === b.content;
5141
+ if (a.type === "command" && b.type === "command") return a.command === b.command;
5142
+ if (a.type === "web_search" && b.type === "web_search") return a.query === b.query;
5143
+ if (a.type === "subagent" && b.type === "subagent") {
5144
+ return a.description === b.description && a.prompt === b.prompt && a.subagentType === b.subagentType && a.model === b.model;
5145
+ }
5146
+ if (a.type === "todo_list" && b.type === "todo_list") {
5147
+ return stableString(a.items) === stableString(b.items);
5148
+ }
5149
+ if (a.type === "patch" && b.type === "patch") {
5150
+ return stableString(a.operations) === stableString(b.operations);
5151
+ }
5152
+ if (a.type === "tool_call" && b.type === "tool_call") {
5153
+ return a.server === b.server && a.tool === b.tool && stableString(a.input) === stableString(b.input);
5154
+ }
5155
+ return false;
5156
+ }
5157
+ function mergeCodexAspDisplayMessages(primary, supplemental) {
5158
+ const merged = [...primary];
5159
+ for (const message of supplemental) {
5160
+ const duplicateIndex = merged.findIndex((existing) => duplicateMessage(existing, message));
5161
+ if (duplicateIndex === -1) {
5162
+ merged.push(message);
5163
+ } else if (message.type === "user") {
5164
+ merged[duplicateIndex] = { ...merged[duplicateIndex], timestamp: message.timestamp };
5165
+ }
5166
+ }
5167
+ return merged.map((message, index) => ({ message, index })).sort((a, b) => parseTimestampMs(a.message.timestamp) - parseTimestampMs(b.message.timestamp) || a.index - b.index).map(({ message }) => message);
5168
+ }
5169
+ function parseCodexAspTranscript(transcript) {
5170
+ const messages = [];
5171
+ const orderedItems = transcript.turns.map((turn, turnIndex) => ({ turn, turnIndex })).flatMap(({ turn, turnIndex }) => turn.items.map((item, itemIndex) => ({ item, turnIndex, itemIndex }))).sort((a, b) => {
5172
+ const aSequence = a.item.sequence ?? Number.MAX_SAFE_INTEGER;
5173
+ const bSequence = b.item.sequence ?? Number.MAX_SAFE_INTEGER;
5174
+ return aSequence - bSequence || parseTimestampMs(a.item.timestamp) - parseTimestampMs(b.item.timestamp) || a.turnIndex - b.turnIndex || a.itemIndex - b.itemIndex;
5175
+ });
5176
+ for (const { item } of orderedItems) {
5177
+ const message = messageForItem(item);
5178
+ if (message) {
5179
+ messages.push(message);
5180
+ }
5181
+ }
5182
+ return messages;
5183
+ }
4945
5184
 
4946
5185
  // ../shared/src/display-message/parsers/index.ts
5186
+ var INTERRUPTION_DEDUP_WINDOW_MS = 15e3;
4947
5187
  function parseAgentEvents(events, agentType) {
4948
5188
  if (agentType === "codex") {
4949
5189
  return parseCodexEvents(events);
@@ -4959,11 +5199,82 @@ function parseAgentEvents(events, agentType) {
4959
5199
  }
4960
5200
  return parseClaudeEvents(events);
4961
5201
  }
5202
+ function parseDisplayMessages(events, agentType, codexAspTranscript, options = {}) {
5203
+ const shouldFilter = options.filter ?? true;
5204
+ const parsedEvents = agentType === "claude" || agentType === "relay" ? parseClaudeEvents(events, options.parentToolUseId) : parseAgentEvents(events, agentType);
5205
+ const legacyMessages = shouldFilter ? filterDisplayMessages(parsedEvents, agentType) : parsedEvents;
5206
+ if (agentType !== "codex" || !codexAspTranscript) {
5207
+ return shouldFilter ? applyInterruptions(legacyMessages, events) : legacyMessages;
5208
+ }
5209
+ const nativeCodexMessages = shouldFilter ? filterDisplayMessages(parseCodexAspTranscript(codexAspTranscript), agentType) : parseCodexAspTranscript(codexAspTranscript);
5210
+ const merged = mergeCodexAspDisplayMessages(nativeCodexMessages, legacyMessages);
5211
+ return shouldFilter ? applyInterruptions(merged, events) : merged;
5212
+ }
5213
+ function applyInterruptions(messages, events) {
5214
+ const result = [...messages];
5215
+ for (const event of events) {
5216
+ if (event.type !== CHAT_INTERRUPTED_EVENT_TYPE) continue;
5217
+ const eventMs = parseTimestampMs(event.timestamp);
5218
+ let index = result.length;
5219
+ while (index > 0 && parseTimestampMs(result[index - 1].timestamp) > eventMs) index--;
5220
+ result.splice(index, 0, {
5221
+ id: `interruption-${event.timestamp}`,
5222
+ type: "interruption",
5223
+ timestamp: event.timestamp
5224
+ });
5225
+ }
5226
+ let lastUserMs = null;
5227
+ let lastInterruption = null;
5228
+ const finalized = [];
5229
+ for (const msg of result) {
5230
+ if (msg.type === "user") {
5231
+ lastUserMs = parseTimestampMs(msg.timestamp);
5232
+ lastInterruption = null;
5233
+ finalized.push(msg);
5234
+ continue;
5235
+ }
5236
+ if (msg.type !== "interruption") {
5237
+ finalized.push(msg);
5238
+ continue;
5239
+ }
5240
+ const ms = parseTimestampMs(msg.timestamp);
5241
+ if (lastInterruption && ms - parseTimestampMs(lastInterruption.timestamp) <= INTERRUPTION_DEDUP_WINDOW_MS) {
5242
+ continue;
5243
+ }
5244
+ lastInterruption = {
5245
+ ...msg,
5246
+ ...lastUserMs !== null && ms >= lastUserMs ? { durationMs: ms - lastUserMs } : {}
5247
+ };
5248
+ finalized.push(lastInterruption);
5249
+ }
5250
+ return finalized;
5251
+ }
5252
+ function isCodexInitializationPrompt(message) {
5253
+ return message.type === "user" && removeReplicasInstructions(message.content).trim() === "Hello";
5254
+ }
4962
5255
  function isAgentBackendEvent(value) {
4963
5256
  if (!value || typeof value !== "object") return false;
4964
5257
  const candidate = value;
4965
5258
  return typeof candidate.timestamp === "string" && typeof candidate.type === "string" && typeof candidate.payload === "object" && candidate.payload !== null;
4966
5259
  }
5260
+ function filterDisplayMessages(messages, provider) {
5261
+ let result = messages;
5262
+ if (provider === "codex") {
5263
+ const userMessages = result.map((msg, index) => ({ msg, index })).filter(({ msg }) => msg.type === "user");
5264
+ if (userMessages.length >= 2 && isCodexInitializationPrompt(userMessages[0].msg)) {
5265
+ result = result.slice(userMessages[1].index);
5266
+ }
5267
+ }
5268
+ result = result.map((msg) => {
5269
+ if (msg.type !== "user") return msg;
5270
+ const cleaned = removeReplicasInstructions(msg.content).trim();
5271
+ if (INTERRUPTED_MESSAGE_REGEX.test(cleaned)) {
5272
+ return { id: msg.id, type: "interruption", timestamp: msg.timestamp };
5273
+ }
5274
+ return cleaned !== msg.content ? { ...msg, content: cleaned } : msg;
5275
+ });
5276
+ return result;
5277
+ }
4967
5278
 
4968
5279
  // ../shared/src/onboarding-emails.ts
4969
5280
  var EMAIL_KEYS = [
@@ -10297,7 +10608,7 @@ var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
10297
10608
  var MIN_CODEX_CLI_VERSION = "0.144.6";
10298
10609
  var CODEX_UPGRADE_TIMEOUT_MS = 12e4;
10299
10610
  var codexCliVersionEnsured = null;
10300
- var ENGINE_PACKAGE_VERSION = "0.1.552";
10611
+ var ENGINE_PACKAGE_VERSION = "0.1.553";
10301
10612
  var INITIALIZE_METHOD = "initialize";
10302
10613
  var INITIALIZED_NOTIFICATION = "initialized";
10303
10614
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -14519,153 +14830,234 @@ var RelayManager = class {
14519
14830
  }
14520
14831
  };
14521
14832
 
14522
- // src/services/chat/turn-usage.ts
14833
+ // src/analytics/analytics.service.ts
14523
14834
  import { randomUUID as randomUUID5 } from "crypto";
14835
+
14836
+ // src/analytics/analytics-buffer.service.ts
14524
14837
  import { appendFile as appendFile3, mkdir as mkdir14, readFile as readFile12, readdir as readdir6, rename as rename2, unlink as unlink3 } from "fs/promises";
14525
14838
  import { join as join22 } from "path";
14526
- var LIVE_FILE = join22(ENGINE_DIR2, "turn-usage.jsonl");
14527
- var SEGMENT_FILE_RE = /^turn-usage\.(\d+)\.jsonl$/;
14528
- var UPLOADED_SUFFIX = ".uploaded";
14529
- var MAX_TRACKED_MESSAGES = 500;
14530
14839
  var MAX_FAILED_RECORDS = 500;
14531
14840
  var FLUSH_DEBOUNCE_MS = 15e3;
14532
14841
  var MAX_UPLOADED_SEGMENTS = 50;
14533
- var messageAttrs = /* @__PURE__ */ new Map();
14534
- var activeTurns = /* @__PURE__ */ new Map();
14535
- var flushTimer = null;
14536
- var activeFlush = Promise.resolve({ flushed: 0, failed: 0 });
14537
- var pendingAppends = /* @__PURE__ */ new Set();
14538
- var failedRecords = [];
14539
- function noteMessageAccepted(messageId, request) {
14540
- messageAttrs.set(messageId, {
14541
- ...request.model ? { model: request.model } : {},
14542
- ...request.senderUserId ? { senderUserId: request.senderUserId } : {}
14543
- });
14544
- for (const key of messageAttrs.keys()) {
14545
- if (messageAttrs.size <= MAX_TRACKED_MESSAGES) break;
14546
- messageAttrs.delete(key);
14547
- }
14548
- }
14549
- function noteTurnStarted(chatId, messageId, provider) {
14550
- const credential = ENGINE_ENV.REPLICAS_AGENT_CREDENTIALS[provider];
14551
- activeTurns.set(chatId, {
14552
- turnId: randomUUID5(),
14553
- messageId,
14554
- startedAtMs: Date.now(),
14555
- provider,
14556
- ...credential ? { credentialMethod: credential.method, credentialScope: credential.scope } : {}
14557
- });
14558
- }
14559
- function noteTurnEnded(chatId) {
14560
- const turn = activeTurns.get(chatId);
14561
- if (!turn) return;
14562
- activeTurns.delete(chatId);
14563
- recordTurn(chatId, turn);
14564
- scheduleFlush();
14565
- }
14566
- function scheduleFlush() {
14567
- if (flushTimer) return;
14568
- flushTimer = setTimeout(() => {
14569
- flushTimer = null;
14570
- void flushAllTurnUsage(false).catch((err) => {
14571
- console.error("[TurnUsage] Scheduled flush failed, retrying on the next turn:", err);
14842
+ var UPLOADED_SUFFIX = ".uploaded";
14843
+ var AnalyticsBufferService = class {
14844
+ constructor(options) {
14845
+ this.options = options;
14846
+ this.liveFile = join22(ENGINE_DIR2, `${options.name}.jsonl`);
14847
+ this.segmentFilePattern = new RegExp(`^${options.name}\\.(\\d+)\\.jsonl$`);
14848
+ }
14849
+ options;
14850
+ liveFile;
14851
+ segmentFilePattern;
14852
+ failedRecords = [];
14853
+ pendingAppends = /* @__PURE__ */ new Set();
14854
+ flushTimer = null;
14855
+ activeFlush = Promise.resolve({ flushed: 0, failed: 0 });
14856
+ append(record) {
14857
+ const pending = mkdir14(ENGINE_DIR2, { recursive: true }).then(() => appendFile3(this.liveFile, `${JSON.stringify(record)}
14858
+ `, "utf-8")).catch((error) => {
14859
+ console.error(`[${this.options.name}] Append failed, will retry on flush:`, error);
14860
+ if (this.failedRecords.length < MAX_FAILED_RECORDS) this.failedRecords.push(record);
14572
14861
  });
14573
- }, FLUSH_DEBOUNCE_MS);
14574
- flushTimer.unref?.();
14575
- }
14576
- function recordTurn(chatId, turn) {
14577
- const attrs = messageAttrs.get(turn.messageId) ?? {};
14578
- messageAttrs.delete(turn.messageId);
14579
- writeRecord({
14580
- turnId: turn.turnId,
14581
- chatId,
14582
- provider: turn.provider,
14583
- ...turn.credentialMethod && turn.credentialScope ? { credentialMethod: turn.credentialMethod, credentialScope: turn.credentialScope } : {},
14584
- ...attrs,
14585
- // A request without a model runs the agent's default, so record that rather than nothing.
14586
- model: attrs.model ?? getDefaultAgentModel(turn.provider),
14587
- occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
14588
- seconds: Math.round((Date.now() - turn.startedAtMs) / 1e3)
14589
- });
14590
- }
14591
- function writeRecord(record) {
14592
- const pending = mkdir14(ENGINE_DIR2, { recursive: true }).then(() => appendFile3(LIVE_FILE, JSON.stringify(record) + "\n", "utf-8")).catch((err) => {
14593
- console.error("[TurnUsage] Append failed, will retry on flush:", err);
14594
- if (failedRecords.length < MAX_FAILED_RECORDS) failedRecords.push(record);
14595
- });
14596
- pendingAppends.add(pending);
14597
- void pending.finally(() => pendingAppends.delete(pending));
14598
- }
14599
- function flushAllTurnUsage(finalizeInFlight = true) {
14600
- if (flushTimer) {
14601
- clearTimeout(flushTimer);
14602
- flushTimer = null;
14603
- }
14604
- activeFlush = activeFlush.catch(() => {
14605
- }).then(() => runFlush(finalizeInFlight));
14606
- return activeFlush;
14607
- }
14608
- async function runFlush(finalizeInFlight) {
14609
- for (const record of failedRecords.splice(0)) writeRecord(record);
14610
- if (finalizeInFlight) {
14611
- for (const [chatId, turn] of activeTurns) recordTurn(chatId, turn);
14612
- activeTurns.clear();
14613
- }
14614
- while (pendingAppends.size > 0) await Promise.allSettled([...pendingAppends]);
14615
- await rename2(LIVE_FILE, join22(ENGINE_DIR2, `turn-usage.${Date.now()}.jsonl`)).catch(() => {
14616
- });
14617
- const entries = await readdir6(ENGINE_DIR2).catch(() => []);
14618
- let flushed = 0;
14619
- let failed = 0;
14620
- for (const entry of entries) {
14621
- if (!SEGMENT_FILE_RE.test(entry)) continue;
14622
- try {
14623
- await uploadSegment(join22(ENGINE_DIR2, entry));
14624
- flushed++;
14625
- } catch (err) {
14626
- failed++;
14627
- console.error("[TurnUsage] Segment upload failed, retained for retry:", { entry, err });
14862
+ this.pendingAppends.add(pending);
14863
+ void pending.finally(() => this.pendingAppends.delete(pending));
14864
+ }
14865
+ scheduleFlush() {
14866
+ if (this.flushTimer) return;
14867
+ this.flushTimer = setTimeout(() => {
14868
+ this.flushTimer = null;
14869
+ void this.flush().catch((error) => {
14870
+ console.error(`[${this.options.name}] Scheduled flush failed, retrying on the next record:`, error);
14871
+ });
14872
+ }, FLUSH_DEBOUNCE_MS);
14873
+ this.flushTimer.unref?.();
14874
+ }
14875
+ flush() {
14876
+ if (this.flushTimer) {
14877
+ clearTimeout(this.flushTimer);
14878
+ this.flushTimer = null;
14628
14879
  }
14880
+ this.activeFlush = this.activeFlush.catch(() => {
14881
+ }).then(() => this.runFlush());
14882
+ return this.activeFlush;
14629
14883
  }
14630
- const stranded = failedRecords.splice(0);
14631
- if (stranded.length > 0) {
14632
- try {
14633
- await uploadTurns(stranded);
14634
- flushed++;
14635
- } catch (err) {
14636
- failed++;
14637
- failedRecords.unshift(...stranded.slice(0, MAX_FAILED_RECORDS));
14638
- console.error("[TurnUsage] Stranded-record upload failed, retained for retry:", err);
14884
+ async runFlush() {
14885
+ for (const record of this.failedRecords.splice(0)) this.append(record);
14886
+ while (this.pendingAppends.size > 0) await Promise.allSettled([...this.pendingAppends]);
14887
+ await rename2(this.liveFile, join22(ENGINE_DIR2, `${this.options.name}.${Date.now()}.jsonl`)).catch(() => {
14888
+ });
14889
+ const entries = await readdir6(ENGINE_DIR2).catch(() => []);
14890
+ let flushed = 0;
14891
+ let failed = 0;
14892
+ for (const entry of entries) {
14893
+ if (!this.segmentFilePattern.test(entry)) continue;
14894
+ try {
14895
+ await this.uploadSegment(join22(ENGINE_DIR2, entry));
14896
+ flushed++;
14897
+ } catch (error) {
14898
+ failed++;
14899
+ console.error(`[${this.options.name}] Segment upload failed, retained for retry:`, { entry, error });
14900
+ }
14639
14901
  }
14902
+ const failedRecords = this.failedRecords.splice(0);
14903
+ if (failedRecords.length > 0) {
14904
+ try {
14905
+ await this.options.upload(failedRecords);
14906
+ flushed++;
14907
+ } catch (error) {
14908
+ failed++;
14909
+ this.failedRecords.unshift(...failedRecords.slice(0, MAX_FAILED_RECORDS));
14910
+ console.error(`[${this.options.name}] Failed records upload failed, retained for retry:`, error);
14911
+ }
14912
+ }
14913
+ const uploaded = entries.filter((entry) => entry.startsWith(`${this.options.name}.`) && entry.endsWith(UPLOADED_SUFFIX)).sort();
14914
+ for (const entry of uploaded.slice(0, -MAX_UPLOADED_SEGMENTS)) {
14915
+ await unlink3(join22(ENGINE_DIR2, entry)).catch(() => {
14916
+ });
14917
+ }
14918
+ return { flushed, failed };
14640
14919
  }
14641
- const uploaded = entries.filter((entry) => entry.endsWith(UPLOADED_SUFFIX)).sort();
14642
- for (const entry of uploaded.slice(0, -MAX_UPLOADED_SEGMENTS)) {
14643
- await unlink3(join22(ENGINE_DIR2, entry)).catch(() => {
14920
+ async uploadSegment(filePath) {
14921
+ const records = (await readFile12(filePath, "utf-8")).split("\n").flatMap((line) => {
14922
+ try {
14923
+ const parsed = JSON.parse(line);
14924
+ return this.options.validate(parsed) ? [parsed] : [];
14925
+ } catch {
14926
+ return [];
14927
+ }
14928
+ });
14929
+ if (records.length > 0) await this.options.upload(records);
14930
+ await rename2(filePath, `${filePath}${UPLOADED_SUFFIX}`).catch(() => {
14644
14931
  });
14645
14932
  }
14646
- return { flushed, failed };
14933
+ };
14934
+
14935
+ // src/analytics/extract-skill-mcp-calls.ts
14936
+ var NON_MCP_SERVERS = /* @__PURE__ */ new Set(["claude", "cursor", "opencode", "pi", "custom", "dynamic"]);
14937
+ function mcpNameFromToolCall(message) {
14938
+ const parsedName = parseMcpToolName(message.tool);
14939
+ if (parsedName) return parsedName.server;
14940
+ if (message.tool.startsWith("mcp.")) {
14941
+ const [server, ...toolParts] = message.tool.slice("mcp.".length).split(".");
14942
+ if (server && toolParts.length > 0) return server;
14943
+ }
14944
+ return NON_MCP_SERVERS.has(message.server) ? null : message.server;
14945
+ }
14946
+ function extractSkillMcpCalls(messages) {
14947
+ return messages.flatMap((message) => {
14948
+ if (message.type === "skill") {
14949
+ return [{ kind: "skill", id: message.id, skillName: message.skillName, occurredAt: message.timestamp }];
14950
+ }
14951
+ if (message.type !== "tool_call") return [];
14952
+ const mcpName = mcpNameFromToolCall(message);
14953
+ return mcpName ? [{ kind: "mcp", id: message.id, mcpName, occurredAt: message.timestamp }] : [];
14954
+ });
14647
14955
  }
14648
- async function uploadSegment(filePath) {
14649
- const turns = (await readFile12(filePath, "utf-8")).split("\n").flatMap((line) => {
14650
- try {
14651
- const parsed = JSON.parse(line);
14652
- return isChatTurnUsageRecord(parsed) ? [parsed] : [];
14653
- } catch {
14654
- return [];
14655
- }
14956
+
14957
+ // src/analytics/analytics.service.ts
14958
+ var MAX_TRACKED_MESSAGES = 500;
14959
+ async function uploadAnalyticsRecords(endpoint, body) {
14960
+ const response = await monolithRequest(endpoint, { body });
14961
+ if (!response.ok) throw new Error(`upload failed: ${response.status} ${await response.text()}`);
14962
+ }
14963
+ var AnalyticsService = class {
14964
+ pendingMessages = /* @__PURE__ */ new Map();
14965
+ activeTurns = /* @__PURE__ */ new Map();
14966
+ turnBuffer = new AnalyticsBufferService({
14967
+ name: "turn-usage",
14968
+ validate: isChatTurnUsageRecord,
14969
+ upload: (turns) => uploadAnalyticsRecords("/v1/engine/chat-turn-usage", { turns })
14656
14970
  });
14657
- await uploadTurns(turns);
14658
- await rename2(filePath, `${filePath}${UPLOADED_SUFFIX}`).catch(() => {
14971
+ skillBuffer = new AnalyticsBufferService({
14972
+ name: "skill-usage",
14973
+ validate: isSkillUsageRecord,
14974
+ upload: (skills) => uploadAnalyticsRecords("/v1/engine/skill-usage", { skills })
14659
14975
  });
14660
- }
14661
- async function uploadTurns(turns) {
14662
- if (turns.length === 0) return;
14663
- const body = { turns };
14664
- const response = await monolithRequest("/v1/engine/chat-turn-usage", { body });
14665
- if (!response.ok) {
14666
- throw new Error(`upload failed: ${response.status} ${await response.text()}`);
14976
+ mcpBuffer = new AnalyticsBufferService({
14977
+ name: "mcp-usage",
14978
+ validate: isMcpUsageRecord,
14979
+ upload: (mcps) => uploadAnalyticsRecords("/v1/engine/mcp-usage", { mcps })
14980
+ });
14981
+ noteMessageAccepted(messageId, request) {
14982
+ this.pendingMessages.set(messageId, {
14983
+ model: request.model,
14984
+ senderUserId: request.senderUserId
14985
+ });
14986
+ for (const key of this.pendingMessages.keys()) {
14987
+ if (this.pendingMessages.size <= MAX_TRACKED_MESSAGES) break;
14988
+ this.pendingMessages.delete(key);
14989
+ }
14990
+ }
14991
+ noteTurnStarted(chatId, messageId, provider) {
14992
+ const attributes = this.pendingMessages.get(messageId) ?? {};
14993
+ const credential = ENGINE_ENV.REPLICAS_AGENT_CREDENTIALS[provider];
14994
+ this.pendingMessages.delete(messageId);
14995
+ this.activeTurns.set(chatId, {
14996
+ turnId: randomUUID5(),
14997
+ startedAtMs: Date.now(),
14998
+ provider,
14999
+ model: attributes.model ?? getDefaultAgentModel(provider),
15000
+ senderUserId: attributes.senderUserId,
15001
+ credentialMethod: credential?.method,
15002
+ credentialScope: credential?.scope,
15003
+ skills: [],
15004
+ mcps: [],
15005
+ seenCallIds: /* @__PURE__ */ new Set()
15006
+ });
14667
15007
  }
14668
- }
15008
+ noteSkillMcpCalls(chatId, messages) {
15009
+ const turn = this.activeTurns.get(chatId);
15010
+ if (!turn) return;
15011
+ for (const call of extractSkillMcpCalls(messages)) {
15012
+ if (turn.seenCallIds.has(call.id)) continue;
15013
+ turn.seenCallIds.add(call.id);
15014
+ const common = {
15015
+ callId: call.id,
15016
+ chatId,
15017
+ provider: turn.provider,
15018
+ model: turn.model,
15019
+ senderUserId: turn.senderUserId,
15020
+ occurredAt: call.occurredAt,
15021
+ credentialMethod: turn.credentialMethod,
15022
+ credentialScope: turn.credentialScope
15023
+ };
15024
+ if (call.kind === "skill") turn.skills.push({ ...common, skillName: call.skillName });
15025
+ else turn.mcps.push({ ...common, mcpName: call.mcpName });
15026
+ }
15027
+ }
15028
+ noteTurnEnded(chatId) {
15029
+ const turn = this.activeTurns.get(chatId);
15030
+ if (!turn) return;
15031
+ this.activeTurns.delete(chatId);
15032
+ this.turnBuffer.append({
15033
+ turnId: turn.turnId,
15034
+ chatId,
15035
+ provider: turn.provider,
15036
+ model: turn.model,
15037
+ senderUserId: turn.senderUserId,
15038
+ credentialMethod: turn.credentialMethod,
15039
+ credentialScope: turn.credentialScope,
15040
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
15041
+ seconds: Math.round((Date.now() - turn.startedAtMs) / 1e3)
15042
+ });
15043
+ for (const skill of turn.skills) this.skillBuffer.append(skill);
15044
+ for (const mcp of turn.mcps) this.mcpBuffer.append(mcp);
15045
+ this.turnBuffer.scheduleFlush();
15046
+ if (turn.skills.length > 0) this.skillBuffer.scheduleFlush();
15047
+ if (turn.mcps.length > 0) this.mcpBuffer.scheduleFlush();
15048
+ }
15049
+ async flush(finalizeInFlight = true) {
15050
+ if (finalizeInFlight) {
15051
+ for (const chatId of this.activeTurns.keys()) this.noteTurnEnded(chatId);
15052
+ }
15053
+ const [turnUsage, skillUsage, mcpUsage] = await Promise.all([
15054
+ this.turnBuffer.flush(),
15055
+ this.skillBuffer.flush(),
15056
+ this.mcpBuffer.flush()
15057
+ ]);
15058
+ return { turnUsage, skillUsage, mcpUsage };
15059
+ }
15060
+ };
14669
15061
 
14670
15062
  // src/services/keep-alive-service.ts
14671
15063
  var KeepAliveService = class _KeepAliveService {
@@ -15217,15 +15609,9 @@ function getCodexTranscriptUserMessages(transcript) {
15217
15609
  }
15218
15610
  function getCodexTranscriptFromEvent(event) {
15219
15611
  if (event.type !== CODEX_ASP_TRANSCRIPT_UPDATED_EVENT_TYPE) return null;
15220
- const transcript = event.payload.transcript;
15221
- if (isCodexAspTranscript(transcript)) {
15222
- return transcript;
15223
- }
15224
- const transcriptDelta = event.payload.transcriptDelta;
15225
- if (!isCodexAspTranscriptDelta(transcriptDelta)) {
15226
- return null;
15227
- }
15228
- return applyCodexAspTranscriptDelta(null, transcriptDelta);
15612
+ if (isCodexAspTranscript(event.payload.transcript)) return event.payload.transcript;
15613
+ if (!isCodexAspTranscriptDelta(event.payload.transcriptDelta)) return null;
15614
+ return applyCodexAspTranscriptDelta(null, event.payload.transcriptDelta);
15229
15615
  }
15230
15616
  function terminalErrorsFromEvent(event, provider, codexTranscript) {
15231
15617
  if (event.type === "claude-result") {
@@ -15296,13 +15682,15 @@ function corruptChatsFilePath() {
15296
15682
  return `${CHATS_FILE}.corrupt-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
15297
15683
  }
15298
15684
  var ChatService = class {
15299
- constructor(workingDirectory) {
15685
+ constructor(workingDirectory, analyticsService2) {
15300
15686
  this.workingDirectory = workingDirectory;
15687
+ this.analyticsService = analyticsService2;
15301
15688
  keepAliveService.setActivityCheck(
15302
15689
  () => [...this.chats.values()].some((chat) => !chat.persisted.deletedAt && isChatActive(chat))
15303
15690
  );
15304
15691
  }
15305
15692
  workingDirectory;
15693
+ analyticsService;
15306
15694
  chats = /* @__PURE__ */ new Map();
15307
15695
  persistInFlight = false;
15308
15696
  persistQueued = false;
@@ -15424,7 +15812,7 @@ var ChatService = class {
15424
15812
  request.images
15425
15813
  );
15426
15814
  chat.pendingMessageIds.push(result.messageId);
15427
- noteMessageAccepted(result.messageId, request);
15815
+ this.analyticsService.noteMessageAccepted(result.messageId, request);
15428
15816
  if (request.errorNotificationTarget) {
15429
15817
  chat.errorNotificationTargets.set(result.messageId, request.errorNotificationTarget);
15430
15818
  }
@@ -15480,7 +15868,7 @@ var ChatService = class {
15480
15868
  async interrupt(chatId) {
15481
15869
  const chat = this.requireChat(chatId);
15482
15870
  const result = await chat.provider.interrupt();
15483
- noteTurnEnded(chatId);
15871
+ this.analyticsService.noteTurnEnded(chatId);
15484
15872
  chat.hasActiveTurn = false;
15485
15873
  chat.activeMessageId = null;
15486
15874
  chat.pendingMessageIds = [];
@@ -15502,7 +15890,7 @@ var ChatService = class {
15502
15890
  return { interrupted: false, queue: [], goal: null };
15503
15891
  }
15504
15892
  const interruptResult = await chat.provider.interrupt();
15505
- noteTurnEnded(chatId);
15893
+ this.analyticsService.noteTurnEnded(chatId);
15506
15894
  chat.hasActiveTurn = false;
15507
15895
  chat.activeMessageId = null;
15508
15896
  chat.pendingMessageIds = [];
@@ -15692,14 +16080,14 @@ var ChatService = class {
15692
16080
  const chatsById = new Map(
15693
16081
  [...this.chats.entries()].map(([chatId, chat]) => [chatId, this.toSummary(chat)])
15694
16082
  );
15695
- const [chatTranscripts, canvas, repoState, engineLogs, turnUsage] = await Promise.all([
16083
+ const [chatTranscripts, canvas, repoState, engineLogs, analytics] = await Promise.all([
15696
16084
  flushAllChatTranscripts(chatsById),
15697
16085
  flushAllCanvasItems(),
15698
16086
  flushRepoState(),
15699
16087
  flushAllEngineLogs(),
15700
- flushAllTurnUsage()
16088
+ this.analyticsService.flush()
15701
16089
  ]);
15702
- return { chatTranscripts, canvas, repoState, engineLogs, turnUsage };
16090
+ return { chatTranscripts, canvas, repoState, engineLogs, ...analytics };
15703
16091
  }
15704
16092
  createRuntimeChat(persisted) {
15705
16093
  const saveSession = async (sessionId) => {
@@ -15839,7 +16227,7 @@ var ChatService = class {
15839
16227
  }
15840
16228
  chat.hasActiveTurn = true;
15841
16229
  chat.activeMessageId = messageId;
15842
- noteTurnStarted(chat.persisted.id, messageId, chat.persisted.provider);
16230
+ this.analyticsService.noteTurnStarted(chat.persisted.id, messageId, chat.persisted.provider);
15843
16231
  chat.activeErrorNotificationTarget = chat.errorNotificationTargets.get(messageId) ?? null;
15844
16232
  chat.errorNotificationTargets.delete(messageId);
15845
16233
  this.publish({
@@ -15852,6 +16240,18 @@ var ChatService = class {
15852
16240
  });
15853
16241
  }
15854
16242
  const codexTranscript = getCodexTranscriptFromEvent(event);
16243
+ if (chat.hasActiveTurn) {
16244
+ const displayMessages = parseDisplayMessages(
16245
+ codexTranscript ? [] : [event],
16246
+ chat.persisted.provider,
16247
+ codexTranscript,
16248
+ {
16249
+ filter: false,
16250
+ parentToolUseId: typeof event.payload.parent_tool_use_id === "string" ? event.payload.parent_tool_use_id : null
16251
+ }
16252
+ );
16253
+ this.analyticsService.noteSkillMcpCalls(chatId, displayMessages);
16254
+ }
15855
16255
  const terminalErrors = terminalErrorsFromEvent(event, chat.persisted.provider, codexTranscript);
15856
16256
  if (terminalErrors !== void 0) {
15857
16257
  chat.lastTurnErrors = terminalErrors;
@@ -15914,7 +16314,7 @@ var ChatService = class {
15914
16314
  }
15915
16315
  chat.hasActiveTurn = false;
15916
16316
  chat.activeMessageId = null;
15917
- noteTurnEnded(chatId);
16317
+ this.analyticsService.noteTurnEnded(chatId);
15918
16318
  this.publish({
15919
16319
  type: "chat.turn.completed",
15920
16320
  payload: {
@@ -17912,7 +18312,8 @@ var authMiddleware = async (c, next) => {
17912
18312
  }
17913
18313
  await next();
17914
18314
  };
17915
- var chatService = new ChatService(gitService.getWorkspaceRoot());
18315
+ var analyticsService = new AnalyticsService();
18316
+ var chatService = new ChatService(gitService.getWorkspaceRoot(), analyticsService);
17916
18317
  app.get("/health", async (c) => {
17917
18318
  const requestedWaitMs = Number(c.req.query(ENGINE_HEALTH_WAIT_QUERY_PARAM));
17918
18319
  const waitMs = Number.isFinite(requestedWaitMs) ? Math.min(Math.max(requestedWaitMs, 0), ENGINE_HEALTH_MAX_WAIT_MS) : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.552",
3
+ "version": "0.1.553",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",