replicas-engine 0.1.609 → 0.1.611

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
@@ -60,6 +60,9 @@ var VALID_CODING_AGENT_PROVIDERS = VALID_AGENT_PROVIDERS.filter(
60
60
  function isValidAgentProvider(value) {
61
61
  return VALID_AGENT_PROVIDERS.some((p) => p === value);
62
62
  }
63
+ function isValidCodingAgentProvider(value) {
64
+ return VALID_CODING_AGENT_PROVIDERS.some((provider) => provider === value);
65
+ }
63
66
  var VALID_THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max", "ultra", "ultracode"];
64
67
  var STANDARD_THINKING_LEVELS = ["low", "medium", "high", "xhigh", "max"];
65
68
  var THINKING_LEVELS_BY_AGENT = {
@@ -86,6 +89,117 @@ function codexReasoningEffortForThinkingLevel(thinkingLevel) {
86
89
  return thinkingLevel ? CODEX_REASONING_EFFORT_BY_THINKING_LEVEL[thinkingLevel] : void 0;
87
90
  }
88
91
 
92
+ // ../shared/src/agent-quota.ts
93
+ var OUT_OF_CREDITS_PATTERNS = [
94
+ /credit balance is too low/,
95
+ /out.of.credits/,
96
+ /credits?.?(?:depleted|exhausted|remaining)/,
97
+ /insufficient credits/,
98
+ /insufficient_quota/
99
+ ];
100
+ var RATE_LIMIT_PATTERNS = [
101
+ // Loose on purpose: providers word this as "rate limit", "rate_limit",
102
+ // "usageLimitExceeded" and so on, and these only ever run against error text.
103
+ /rate.?limit/,
104
+ /usage.?limit/,
105
+ /too many requests/,
106
+ /\bquota\b/
107
+ ];
108
+ var HTTP_429_PATTERN = /(?:^|[^0-9])429(?:$|[^0-9])/;
109
+ var HTTP_CONTEXT_PATTERN = /(api error|request|response|status|http)/;
110
+ function detectAgentQuotaLimit(text) {
111
+ const normalized = text.toLowerCase();
112
+ if (!normalized) return null;
113
+ if (OUT_OF_CREDITS_PATTERNS.some((pattern) => pattern.test(normalized))) return "out_of_credits";
114
+ if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(normalized))) return "rate_limit";
115
+ return HTTP_429_PATTERN.test(normalized) && HTTP_CONTEXT_PATTERN.test(normalized) ? "rate_limit" : null;
116
+ }
117
+
118
+ // ../shared/src/credentials/types.ts
119
+ var CREDENTIAL_SCOPE = {
120
+ USER: "user",
121
+ ORG: "org"
122
+ };
123
+ var CREDENTIAL_SCOPES = Object.values(CREDENTIAL_SCOPE);
124
+ var CREDENTIAL_SCOPE_PRIORITY = [CREDENTIAL_SCOPE.USER, CREDENTIAL_SCOPE.ORG];
125
+
126
+ // ../shared/src/credentials/agents.ts
127
+ var CREDENTIAL_METHOD = {
128
+ OAUTH: "oauth",
129
+ API_KEY: "api_key",
130
+ FOUNDRY: "foundry",
131
+ BEDROCK: "bedrock",
132
+ OPENCODE_GO: "opencode-go",
133
+ ASTER: "aster",
134
+ OPENROUTER: "openrouter"
135
+ };
136
+ var CREDENTIAL_TYPE = {
137
+ CLAUDE_OAUTH: "claude",
138
+ ANTHROPIC_API_KEY: "claude_anthropic_api_key",
139
+ CLAUDE_FOUNDRY: "claude_foundry",
140
+ CLAUDE_BEDROCK: "claude_bedrock",
141
+ CODEX_OAUTH: "codex",
142
+ OPENAI_API_KEY: "openai_api_key",
143
+ CODEX_FOUNDRY: "codex_foundry",
144
+ CURSOR_API_KEY: "cursor_api_key",
145
+ OPENCODE_GO_API_KEY: "opencode_go_api_key",
146
+ ASTER_API_KEY: "aster_api_key",
147
+ OPENROUTER_API_KEY: "openrouter_api_key"
148
+ };
149
+ var AGENT_CREDENTIAL_METHODS_IN_ORDER = {
150
+ [AGENT.CLAUDE]: [
151
+ { method: CREDENTIAL_METHOD.OAUTH, credentialType: CREDENTIAL_TYPE.CLAUDE_OAUTH },
152
+ { method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.ANTHROPIC_API_KEY },
153
+ { method: CREDENTIAL_METHOD.FOUNDRY, credentialType: CREDENTIAL_TYPE.CLAUDE_FOUNDRY },
154
+ { method: CREDENTIAL_METHOD.BEDROCK, credentialType: CREDENTIAL_TYPE.CLAUDE_BEDROCK }
155
+ ],
156
+ [AGENT.CODEX]: [
157
+ { method: CREDENTIAL_METHOD.OAUTH, credentialType: CREDENTIAL_TYPE.CODEX_OAUTH },
158
+ { method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.OPENAI_API_KEY },
159
+ { method: CREDENTIAL_METHOD.FOUNDRY, credentialType: CREDENTIAL_TYPE.CODEX_FOUNDRY }
160
+ ],
161
+ [AGENT.CURSOR]: [
162
+ { method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.CURSOR_API_KEY }
163
+ ],
164
+ [AGENT.OPENCODE]: [
165
+ { method: CREDENTIAL_METHOD.OPENCODE_GO, credentialType: CREDENTIAL_TYPE.OPENCODE_GO_API_KEY },
166
+ { method: CREDENTIAL_METHOD.ASTER, credentialType: CREDENTIAL_TYPE.ASTER_API_KEY },
167
+ { method: CREDENTIAL_METHOD.OPENROUTER, credentialType: CREDENTIAL_TYPE.OPENROUTER_API_KEY }
168
+ ],
169
+ [AGENT.PI]: [
170
+ { method: CREDENTIAL_METHOD.ASTER, credentialType: CREDENTIAL_TYPE.ASTER_API_KEY },
171
+ { method: CREDENTIAL_METHOD.OPENROUTER, credentialType: CREDENTIAL_TYPE.OPENROUTER_API_KEY }
172
+ ]
173
+ };
174
+
175
+ // ../shared/src/analytics/types.ts
176
+ function isAgentChatActivityRecord(value) {
177
+ 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";
178
+ }
179
+ function isAuthMethod(value) {
180
+ return typeof value === "string" && Object.values(CREDENTIAL_METHOD).some((method) => method === value);
181
+ }
182
+ function isCredentialScope(value) {
183
+ return typeof value === "string" && CREDENTIAL_SCOPES.some((scope) => scope === value);
184
+ }
185
+ function isAgentChatTurnActivityRecord(value) {
186
+ return isAgentChatActivityRecord(value) && typeof value.turnId === "string" && typeof value.seconds === "number" && Number.isFinite(value.seconds) && value.seconds >= 0;
187
+ }
188
+ function isAgentChatActivityCallRecord(value) {
189
+ return isAgentChatActivityRecord(value) && typeof value.callId === "string" && value.callId.length > 0;
190
+ }
191
+ function isNamedAgentChatActivityRecord(value, field) {
192
+ if (!isAgentChatActivityCallRecord(value)) return false;
193
+ const name = value[field];
194
+ return typeof name === "string" && name.length > 0;
195
+ }
196
+ function isAgentChatSkillActivityRecord(value) {
197
+ return isNamedAgentChatActivityRecord(value, "skillName");
198
+ }
199
+ function isAgentChatMcpActivityRecord(value) {
200
+ return isNamedAgentChatActivityRecord(value, "mcpName");
201
+ }
202
+
89
203
  // ../shared/src/event.ts
90
204
  var CLAUDE_PARTIAL_MESSAGE_EVENT_TYPE = "claude-partial-message";
91
205
  function coerceClaudePartialMessagePayload(payload) {
@@ -112,6 +226,23 @@ var COMPACTION_STATUS_EVENT_TYPE = "compaction-status";
112
226
  var CHAT_INTERRUPTED_EVENT_TYPE = "replicas-interrupted";
113
227
  var CHAT_GOAL_EVENT_TYPE = "chat-goal";
114
228
  var AUTH_RETRY_STATUS_EVENT_TYPE = "auth-retry-status";
229
+ var AUTH_FALLBACK_EVENT_TYPE = "auth-fallback";
230
+ function coerceAuthFallbackPayload(payload) {
231
+ const { provider, reason, exhaustedMethod, exhaustedScope, candidateMethod, candidateScope, status, detail } = payload;
232
+ if (typeof provider !== "string" || !isValidCodingAgentProvider(provider) || reason !== "rate_limit" && reason !== "out_of_credits" || !isAuthMethod(exhaustedMethod) || !isAuthMethod(candidateMethod) || !isCredentialScope(candidateScope) || status !== "switched" && status !== "failed") {
233
+ return null;
234
+ }
235
+ return {
236
+ provider,
237
+ reason,
238
+ exhaustedMethod,
239
+ ...isCredentialScope(exhaustedScope) ? { exhaustedScope } : {},
240
+ candidateMethod,
241
+ candidateScope,
242
+ status,
243
+ ...typeof detail === "string" && detail ? { detail } : {}
244
+ };
245
+ }
115
246
  var CONTEXT_USAGE_EVENT_TYPE = "context-usage";
116
247
 
117
248
  // ../shared/src/languages.ts
@@ -205,91 +336,6 @@ function detectLanguageByPath(filePath) {
205
336
  return EXT_TO_LANGUAGE[ext] ?? null;
206
337
  }
207
338
 
208
- // ../shared/src/credentials/types.ts
209
- var CREDENTIAL_SCOPE = {
210
- USER: "user",
211
- ORG: "org"
212
- };
213
- var CREDENTIAL_SCOPES = Object.values(CREDENTIAL_SCOPE);
214
- var CREDENTIAL_SCOPE_PRIORITY = [CREDENTIAL_SCOPE.USER, CREDENTIAL_SCOPE.ORG];
215
-
216
- // ../shared/src/credentials/agents.ts
217
- var CREDENTIAL_METHOD = {
218
- OAUTH: "oauth",
219
- API_KEY: "api_key",
220
- FOUNDRY: "foundry",
221
- BEDROCK: "bedrock",
222
- OPENCODE_GO: "opencode-go",
223
- ASTER: "aster",
224
- OPENROUTER: "openrouter"
225
- };
226
- var CREDENTIAL_TYPE = {
227
- CLAUDE_OAUTH: "claude",
228
- ANTHROPIC_API_KEY: "claude_anthropic_api_key",
229
- CLAUDE_FOUNDRY: "claude_foundry",
230
- CLAUDE_BEDROCK: "claude_bedrock",
231
- CODEX_OAUTH: "codex",
232
- OPENAI_API_KEY: "openai_api_key",
233
- CODEX_FOUNDRY: "codex_foundry",
234
- CURSOR_API_KEY: "cursor_api_key",
235
- OPENCODE_GO_API_KEY: "opencode_go_api_key",
236
- ASTER_API_KEY: "aster_api_key",
237
- OPENROUTER_API_KEY: "openrouter_api_key"
238
- };
239
- var AGENT_CREDENTIAL_METHODS_IN_ORDER = {
240
- [AGENT.CLAUDE]: [
241
- { method: CREDENTIAL_METHOD.OAUTH, credentialType: CREDENTIAL_TYPE.CLAUDE_OAUTH },
242
- { method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.ANTHROPIC_API_KEY },
243
- { method: CREDENTIAL_METHOD.FOUNDRY, credentialType: CREDENTIAL_TYPE.CLAUDE_FOUNDRY },
244
- { method: CREDENTIAL_METHOD.BEDROCK, credentialType: CREDENTIAL_TYPE.CLAUDE_BEDROCK }
245
- ],
246
- [AGENT.CODEX]: [
247
- { method: CREDENTIAL_METHOD.OAUTH, credentialType: CREDENTIAL_TYPE.CODEX_OAUTH },
248
- { method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.OPENAI_API_KEY },
249
- { method: CREDENTIAL_METHOD.FOUNDRY, credentialType: CREDENTIAL_TYPE.CODEX_FOUNDRY }
250
- ],
251
- [AGENT.CURSOR]: [
252
- { method: CREDENTIAL_METHOD.API_KEY, credentialType: CREDENTIAL_TYPE.CURSOR_API_KEY }
253
- ],
254
- [AGENT.OPENCODE]: [
255
- { method: CREDENTIAL_METHOD.OPENCODE_GO, credentialType: CREDENTIAL_TYPE.OPENCODE_GO_API_KEY },
256
- { method: CREDENTIAL_METHOD.ASTER, credentialType: CREDENTIAL_TYPE.ASTER_API_KEY },
257
- { method: CREDENTIAL_METHOD.OPENROUTER, credentialType: CREDENTIAL_TYPE.OPENROUTER_API_KEY }
258
- ],
259
- [AGENT.PI]: [
260
- { method: CREDENTIAL_METHOD.ASTER, credentialType: CREDENTIAL_TYPE.ASTER_API_KEY },
261
- { method: CREDENTIAL_METHOD.OPENROUTER, credentialType: CREDENTIAL_TYPE.OPENROUTER_API_KEY }
262
- ]
263
- };
264
-
265
- // ../shared/src/analytics/types.ts
266
- function isUsageRecord(value) {
267
- 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";
268
- }
269
- function isAuthMethod(value) {
270
- return typeof value === "string" && Object.values(CREDENTIAL_METHOD).some((method) => method === value);
271
- }
272
- function isCredentialScope(value) {
273
- return typeof value === "string" && CREDENTIAL_SCOPES.some((scope) => scope === value);
274
- }
275
- function isChatTurnUsageRecord(value) {
276
- return isUsageRecord(value) && typeof value.turnId === "string" && typeof value.seconds === "number" && Number.isFinite(value.seconds) && value.seconds >= 0;
277
- }
278
- function isCallUsageRecord(value) {
279
- return isUsageRecord(value) && typeof value.callId === "string" && value.callId.length > 0;
280
- }
281
- function isNamedCallUsageRecord(value, field) {
282
- if (!isCallUsageRecord(value)) return false;
283
- const name = value[field];
284
- return typeof name === "string" && name.length > 0;
285
- }
286
- function isSkillUsageRecord(value) {
287
- return isNamedCallUsageRecord(value, "skillName");
288
- }
289
- function isMcpUsageRecord(value) {
290
- return isNamedCallUsageRecord(value, "mcpName");
291
- }
292
-
293
339
  // ../shared/src/aster.ts
294
340
  var ASTER_PROVIDER = "aster";
295
341
  var ASTER_BASE_URL = "https://api.asterlab.ai/v1";
@@ -2501,7 +2547,8 @@ var DEFAULT_USER_PREFERENCES = {
2501
2547
  auto_draft_prs: false,
2502
2548
  open_prs_in_graphite: false,
2503
2549
  default_fast_mode: false,
2504
- agent_defaults: { ...EMPTY_AGENT_DEFAULT_SETTINGS }
2550
+ agent_defaults: { ...EMPTY_AGENT_DEFAULT_SETTINGS },
2551
+ credential_priority: null
2505
2552
  };
2506
2553
  function defaultReplicasAgentAbilities() {
2507
2554
  const abilities = {};
@@ -2686,7 +2733,7 @@ var TRANSIENT_NETWORK_ERROR_PATTERNS = [
2686
2733
  /connection closed/,
2687
2734
  /closed unexpectedly/
2688
2735
  ];
2689
- var HTTP_CONTEXT_PATTERN = /(api error|request|response|status|http)/;
2736
+ var HTTP_CONTEXT_PATTERN2 = /(api error|request|response|status|http)/;
2690
2737
  var HTTP_STATUS_PATTERN = /(^|[^0-9])([1-5][0-9]{2})(?=$|[^0-9])/g;
2691
2738
  function extractErrorText(error) {
2692
2739
  const parts = [];
@@ -2728,7 +2775,7 @@ function isTransientErrorText(text, options = {}) {
2728
2775
  const httpStatuses = new Set(options.httpStatuses ?? []);
2729
2776
  const httpStatusClasses = new Set(options.httpStatusClasses ?? []);
2730
2777
  if (httpStatuses.size === 0 && httpStatusClasses.size === 0) return false;
2731
- if (options.requireHttpContext !== false && !HTTP_CONTEXT_PATTERN.test(normalized)) return false;
2778
+ if (options.requireHttpContext !== false && !HTTP_CONTEXT_PATTERN2.test(normalized)) return false;
2732
2779
  for (const match of normalized.matchAll(HTTP_STATUS_PATTERN)) {
2733
2780
  const status = Number(match[2]);
2734
2781
  if (httpStatuses.has(status) || httpStatusClasses.has(Math.floor(status / 100))) return true;
@@ -2739,7 +2786,7 @@ function isTransientErrorText(text, options = {}) {
2739
2786
  // ../shared/src/claude-auth.ts
2740
2787
  function isClaudeAuthErrorText(text) {
2741
2788
  const lower = text.toLowerCase();
2742
- return lower.includes("failed to authenticate") || lower.includes("authentication_error") || lower.includes("authentication_failed") || lower.includes("authentication failed") || lower.includes("invalid authentication credentials") || lower.includes("not logged in") || lower.includes("please run /login") || lower.includes("credit balance is too low") || lower.includes("401") && lower.includes("authentic");
2789
+ return lower.includes("failed to authenticate") || lower.includes("authentication_error") || lower.includes("authentication_failed") || lower.includes("authentication failed") || lower.includes("invalid authentication credentials") || lower.includes("not logged in") || lower.includes("please run /login") || lower.includes("401") && lower.includes("authentic");
2743
2790
  }
2744
2791
 
2745
2792
  // ../shared/src/codex-auth.ts
@@ -5122,21 +5169,24 @@ function parseDisplayMessages(events, agentType, codexAspTranscript, options = {
5122
5169
  const shouldFilter = options.filter ?? true;
5123
5170
  const parsedEvents = agentType === "claude" || agentType === "relay" ? parseClaudeEvents(events, options.parentToolUseId) : parseAgentEvents(events, agentType);
5124
5171
  const legacyMessages = shouldFilter ? filterDisplayMessages(parsedEvents, agentType) : parsedEvents;
5172
+ const finalize = (messages) => shouldFilter ? applyAuthFallbackNotices(applyInterruptions(messages, events), events) : messages;
5125
5173
  if (agentType !== "codex" || !codexAspTranscript) {
5126
- return shouldFilter ? applyInterruptions(legacyMessages, events) : legacyMessages;
5174
+ return finalize(legacyMessages);
5127
5175
  }
5128
5176
  const nativeCodexMessages = shouldFilter ? filterDisplayMessages(parseCodexAspTranscript(codexAspTranscript), agentType) : parseCodexAspTranscript(codexAspTranscript);
5129
- const merged = mergeCodexAspDisplayMessages(nativeCodexMessages, legacyMessages);
5130
- return shouldFilter ? applyInterruptions(merged, events) : merged;
5177
+ return finalize(mergeCodexAspDisplayMessages(nativeCodexMessages, legacyMessages));
5178
+ }
5179
+ function insertByTimestamp(messages, message) {
5180
+ const messageMs = parseTimestampMs(message.timestamp);
5181
+ let index = messages.length;
5182
+ while (index > 0 && parseTimestampMs(messages[index - 1].timestamp) > messageMs) index--;
5183
+ messages.splice(index, 0, message);
5131
5184
  }
5132
5185
  function applyInterruptions(messages, events) {
5133
5186
  const result = [...messages];
5134
5187
  for (const event of events) {
5135
5188
  if (event.type !== CHAT_INTERRUPTED_EVENT_TYPE) continue;
5136
- const eventMs = parseTimestampMs(event.timestamp);
5137
- let index = result.length;
5138
- while (index > 0 && parseTimestampMs(result[index - 1].timestamp) > eventMs) index--;
5139
- result.splice(index, 0, {
5189
+ insertByTimestamp(result, {
5140
5190
  id: `interruption-${event.timestamp}`,
5141
5191
  type: "interruption",
5142
5192
  timestamp: event.timestamp
@@ -5168,6 +5218,24 @@ function applyInterruptions(messages, events) {
5168
5218
  }
5169
5219
  return finalized;
5170
5220
  }
5221
+ function applyAuthFallbackNotices(messages, events) {
5222
+ const notices = [];
5223
+ events.forEach((event, index) => {
5224
+ if (event.type !== AUTH_FALLBACK_EVENT_TYPE) return;
5225
+ const payload = coerceAuthFallbackPayload(event.payload);
5226
+ if (!payload) return;
5227
+ notices.push({
5228
+ ...payload,
5229
+ id: `auth-fallback-${event.timestamp}-${index}`,
5230
+ type: "auth_fallback",
5231
+ timestamp: event.timestamp
5232
+ });
5233
+ });
5234
+ if (notices.length === 0) return messages;
5235
+ const result = [...messages];
5236
+ for (const notice of notices) insertByTimestamp(result, notice);
5237
+ return result;
5238
+ }
5171
5239
  function isCodexInitializationPrompt(message) {
5172
5240
  return message.type === "user" && removeReplicasInstructions(message.content).trim() === "Hello";
5173
5241
  }
@@ -5392,6 +5460,28 @@ function setAgentCredentialSnapshot(provider, snapshot) {
5392
5460
  process.env.REPLICAS_AGENT_CREDENTIALS = JSON.stringify(ENGINE_ENV.REPLICAS_AGENT_CREDENTIALS);
5393
5461
  }
5394
5462
 
5463
+ // src/services/credential-fallbacks.ts
5464
+ var fallbacksByAgent = /* @__PURE__ */ new Map();
5465
+ var exhaustedByAgent = /* @__PURE__ */ new Map();
5466
+ function recordCredentialFallback(notice) {
5467
+ fallbacksByAgent.set(notice.provider, notice);
5468
+ }
5469
+ function listCredentialFallbacks() {
5470
+ return [...fallbacksByAgent.values()].filter((notice) => {
5471
+ const live = ENGINE_ENV.REPLICAS_AGENT_CREDENTIALS[notice.provider];
5472
+ if (!live) return false;
5473
+ return notice.status === "switched" ? live.method === notice.candidateMethod && live.scope === notice.candidateScope : live.method === notice.exhaustedMethod && live.scope === notice.exhaustedScope;
5474
+ });
5475
+ }
5476
+ function listExhaustedCredentials(provider) {
5477
+ return [...exhaustedByAgent.get(provider)?.values() ?? []];
5478
+ }
5479
+ function recordExhaustedCredential(provider, credential) {
5480
+ const spent = exhaustedByAgent.get(provider) ?? /* @__PURE__ */ new Map();
5481
+ spent.set(`${credential.method}|${credential.scope}`, credential);
5482
+ exhaustedByAgent.set(provider, spent);
5483
+ }
5484
+
5395
5485
  // src/managers/base-refresh-manager.ts
5396
5486
  var BaseRefreshManager = class {
5397
5487
  constructor(managerName, intervalMs = 15 * 60 * 1e3) {
@@ -5446,6 +5536,29 @@ var BaseRefreshManager = class {
5446
5536
  }
5447
5537
  this.scheduleNextRefresh();
5448
5538
  }
5539
+ async swapCredentials(params) {
5540
+ if (!this.getRuntimeConfig()) {
5541
+ return createErrorResult({ message: `${this.managerName} has no runtime config`, code: "not_configured" });
5542
+ }
5543
+ try {
5544
+ console.log(`[${this.managerName}] Fetching fresh credentials from monolith (${params.failureKind})...`);
5545
+ const excludeCredentials = listExhaustedCredentials(params.provider);
5546
+ await params.refresh(excludeCredentials.length > 0 ? { excludeCredentials } : {});
5547
+ if (params.isOauthNow()) {
5548
+ this.start().catch((error) => {
5549
+ console.error(`[${this.managerName}] Failed to restart OAuth refresh service after fallback:`, error);
5550
+ });
5551
+ }
5552
+ return createSuccessResult();
5553
+ } catch (error) {
5554
+ const message = error instanceof Error ? error.message : String(error);
5555
+ console.error(`[${this.managerName}] Failed to fetch fresh credentials:`, error);
5556
+ return createErrorResult({
5557
+ message,
5558
+ code: message.includes('"code":"no_credentials"') ? "no_credentials" : "refresh_failed"
5559
+ });
5560
+ }
5561
+ }
5449
5562
  stop() {
5450
5563
  if (!this.intervalHandle) {
5451
5564
  return;
@@ -6465,26 +6578,14 @@ var ClaudeTokenManager = class extends BaseRefreshManager {
6465
6578
  if (data.scope) setAgentCredentialSnapshot("claude", { method: data.type, scope: data.scope });
6466
6579
  console.log(`[ClaudeTokenManager] Credentials refreshed (method=${data.type})`);
6467
6580
  }
6468
- async fetchFreshCredentials(failureReason) {
6469
- const config = this.getRuntimeConfig();
6470
- if (!config) return false;
6471
- try {
6472
- console.log("[ClaudeTokenManager] Fetching fresh credentials from monolith after auth failure...");
6473
- const failedMethod = ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD;
6474
- await this.refreshWithRequest(failedMethod && failedMethod !== "none" ? {
6475
- failedMethod,
6476
- failureReason
6477
- } : void 0);
6478
- if (ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD === "oauth") {
6479
- this.start().catch((error) => {
6480
- console.error("[ClaudeTokenManager] Failed to restart OAuth refresh service after fallback:", error);
6481
- });
6482
- }
6483
- return true;
6484
- } catch (error) {
6485
- console.error("[ClaudeTokenManager] Failed to fetch fresh credentials:", error);
6486
- return false;
6487
- }
6581
+ async fetchFreshCredentials(failureReason, failureKind = "rejected") {
6582
+ const failedMethod = ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD;
6583
+ return this.swapCredentials({
6584
+ provider: "claude",
6585
+ failureKind,
6586
+ refresh: (exclusions) => this.refreshWithRequest(failedMethod && failedMethod !== "none" ? { failedMethod, failureReason, failureKind, ...exclusions } : exclusions),
6587
+ isOauthNow: () => ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD === "oauth"
6588
+ });
6488
6589
  }
6489
6590
  async applyCredentialsResponse(response) {
6490
6591
  if (response.type === "oauth") {
@@ -6573,26 +6674,16 @@ var CodexTokenManager = class extends BaseRefreshManager {
6573
6674
  if (data.scope) setAgentCredentialSnapshot("codex", { method: data.type, scope: data.scope });
6574
6675
  console.log(`[CodexTokenManager] Credentials refreshed (method=${data.type})`);
6575
6676
  }
6576
- async fetchFreshCredentials(failureReason) {
6577
- const config = this.getRuntimeConfig();
6578
- if (!config) return false;
6579
- try {
6580
- console.log("[CodexTokenManager] Fetching fresh credentials from monolith after auth failure...");
6581
- const failedMethod = ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD;
6582
- await this.refreshWithRequest(failedMethod === "oauth" || failedMethod === "api_key" || failedMethod === "foundry" ? {
6583
- failedMethod,
6584
- failureReason
6585
- } : void 0);
6586
- if (ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD === "oauth") {
6587
- this.start().catch((error) => {
6588
- console.error("[CodexTokenManager] Failed to restart OAuth refresh service after fallback:", error);
6589
- });
6590
- }
6591
- return true;
6592
- } catch (error) {
6593
- console.error("[CodexTokenManager] Failed to fetch fresh credentials:", error);
6594
- return false;
6595
- }
6677
+ async fetchFreshCredentials(failureReason, failureKind = "rejected") {
6678
+ const failedMethod = ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD;
6679
+ return this.swapCredentials({
6680
+ provider: "codex",
6681
+ failureKind,
6682
+ refresh: (exclusions) => this.refreshWithRequest(
6683
+ failedMethod === "oauth" || failedMethod === "api_key" || failedMethod === "foundry" ? { failedMethod, failureReason, failureKind, ...exclusions } : exclusions
6684
+ ),
6685
+ isOauthNow: () => ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD === "oauth"
6686
+ });
6596
6687
  }
6597
6688
  async applyCredentialsResponse(response) {
6598
6689
  if (response.type === "oauth") {
@@ -6990,6 +7081,7 @@ var EnvironmentDetailsService = class {
6990
7081
  details.cursorAuthMethod = detectCursorAuthMethod();
6991
7082
  details.opencodeAuthMethod = detectOpencodeAuthMethod();
6992
7083
  details.piAuthMethod = detectPiAuthMethod();
7084
+ details.credentialFallbacks = listCredentialFallbacks();
6993
7085
  details.gitIdentityConfigured = gitIdentityConfigured;
6994
7086
  const ghConfigured = existsSync2(GH_HOSTS_PATH);
6995
7087
  details.githubAccessConfigured = ghConfigured;
@@ -7594,13 +7686,13 @@ import { mkdir as mkdir7 } from "fs/promises";
7594
7686
  import { homedir as homedir9 } from "os";
7595
7687
  import { join as join12 } from "path";
7596
7688
  import { randomUUID } from "crypto";
7597
- var ENGINE_DIR = join12(homedir9(), ".replicas", "engine");
7598
- var EVENTS_FILE = join12(ENGINE_DIR, "events.jsonl");
7689
+ var ENGINE_DIR2 = join12(homedir9(), ".replicas", "engine");
7690
+ var EVENTS_FILE = join12(ENGINE_DIR2, "events.jsonl");
7599
7691
  var EventService = class {
7600
7692
  subscribers = /* @__PURE__ */ new Map();
7601
7693
  writer = new StreamWriter();
7602
7694
  async initialize() {
7603
- await mkdir7(ENGINE_DIR, { recursive: true });
7695
+ await mkdir7(ENGINE_DIR2, { recursive: true });
7604
7696
  this.writer.open(EVENTS_FILE);
7605
7697
  }
7606
7698
  subscribe(subscriber) {
@@ -8262,6 +8354,48 @@ async function removeTempImageFiles(paths) {
8262
8354
  await Promise.allSettled(paths.map((path6) => unlink2(path6)));
8263
8355
  }
8264
8356
 
8357
+ // src/managers/auth-fallback.ts
8358
+ var SWAPPABLE_AGENTS = {
8359
+ claude: claudeTokenManager,
8360
+ codex: codexTokenManager
8361
+ };
8362
+ var AuthFallbackCoordinator = class {
8363
+ constructor(provider, record) {
8364
+ this.provider = provider;
8365
+ this.record = record;
8366
+ }
8367
+ provider;
8368
+ record;
8369
+ async fallBack(reason, detail) {
8370
+ const tokenManager = SWAPPABLE_AGENTS[this.provider];
8371
+ if (!tokenManager) return "unavailable";
8372
+ const exhausted = ENGINE_ENV.REPLICAS_AGENT_CREDENTIALS[this.provider];
8373
+ if (!exhausted) return "unavailable";
8374
+ recordExhaustedCredential(this.provider, exhausted);
8375
+ const swap = await tokenManager.fetchFreshCredentials(
8376
+ `${this.provider} ${reason === "out_of_credits" ? "is out of credits" : "hit its usage limit"}: ${detail}`,
8377
+ "exhausted"
8378
+ );
8379
+ if (!swap.ok && swap.error.code === "no_credentials") return "unavailable";
8380
+ const applied = ENGINE_ENV.REPLICAS_AGENT_CREDENTIALS[this.provider] ?? exhausted;
8381
+ const moved = swap.ok && !(applied.method === exhausted.method && applied.scope === exhausted.scope);
8382
+ const payload = {
8383
+ provider: this.provider,
8384
+ reason,
8385
+ exhaustedMethod: exhausted.method,
8386
+ exhaustedScope: exhausted.scope,
8387
+ candidateMethod: applied.method,
8388
+ candidateScope: applied.scope,
8389
+ status: moved ? "switched" : "failed",
8390
+ detail: moved ? detail : "Replicas could not load an alternate credential."
8391
+ };
8392
+ const at = (/* @__PURE__ */ new Date()).toISOString();
8393
+ this.record({ timestamp: at, type: AUTH_FALLBACK_EVENT_TYPE, payload: { ...payload } });
8394
+ recordCredentialFallback({ ...payload, at });
8395
+ return moved ? "switched" : "failed";
8396
+ }
8397
+ };
8398
+
8265
8399
  // src/services/message-queue-service.ts
8266
8400
  var MessageQueueService = class {
8267
8401
  queue = [];
@@ -8445,6 +8579,7 @@ var CodingAgentManager = class {
8445
8579
  onEvent;
8446
8580
  hostOnTurnComplete;
8447
8581
  onProcessingChanged;
8582
+ authFallback;
8448
8583
  compacting = false;
8449
8584
  constructor(options) {
8450
8585
  this.workingDirectory = options.workingDirectory ?? ENGINE_ENV.WORKSPACE_ROOT;
@@ -8454,6 +8589,14 @@ var CodingAgentManager = class {
8454
8589
  this.hostOnTurnComplete = options.onTurnComplete;
8455
8590
  this.onProcessingChanged = options.onProcessingChanged ?? (() => {
8456
8591
  });
8592
+ this.authFallback = options.provider ? new AuthFallbackCoordinator(options.provider, (event) => {
8593
+ this.onEvent(event);
8594
+ this.getHistorySink()?.append(event);
8595
+ }) : null;
8596
+ }
8597
+ async fallBackCredential(reason, detail) {
8598
+ if (!this.authFallback) return "unavailable";
8599
+ return this.authFallback.fallBack(reason, detail);
8457
8600
  }
8458
8601
  onTurnComplete = async () => {
8459
8602
  if (this.compacting) {
@@ -9444,7 +9587,16 @@ var ClaudeTransientTurnError = class extends Error {
9444
9587
  }
9445
9588
  midTurn;
9446
9589
  };
9590
+ var ClaudeQuotaError = class extends Error {
9591
+ constructor(message, kind) {
9592
+ super(message);
9593
+ this.kind = kind;
9594
+ this.name = "ClaudeQuotaError";
9595
+ }
9596
+ kind;
9597
+ };
9447
9598
  var MAX_AUTH_RETRIES = 2;
9599
+ var MAX_QUOTA_FALLBACKS = 2;
9448
9600
  var MAX_TRANSIENT_RETRIES = 2;
9449
9601
  var MAX_MIDTURN_CONTINUE_RETRIES = 2;
9450
9602
  var TRANSIENT_RETRY_DELAYS_MS = [1e3, 2500];
@@ -9490,7 +9642,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
9490
9642
  slashCommandsDiscovery = null;
9491
9643
  authRetrying = false;
9492
9644
  constructor(options) {
9493
- super(options);
9645
+ super({ ...options, provider: AGENT.CLAUDE });
9494
9646
  this.historyFilePath = options.historyFilePath ?? join16(homedir12(), ".replicas", "claude", "history.jsonl");
9495
9647
  this.historyFile = new CodexHistoryFile(this.historyFilePath);
9496
9648
  this.systemPromptOverride = options.systemPromptOverride;
@@ -9722,6 +9874,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
9722
9874
  async processMessageInternal(request) {
9723
9875
  let lastError;
9724
9876
  let authRetryExhausted = false;
9877
+ let quotaFallbacks = 0;
9725
9878
  let attempt = 0;
9726
9879
  let authRetries = 0;
9727
9880
  let transientRetries = 0;
@@ -9734,6 +9887,19 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
9734
9887
  return;
9735
9888
  } catch (error) {
9736
9889
  lastError = error;
9890
+ const quotaKind = _ClaudeManager.quotaLimitKind(error);
9891
+ if (quotaKind) {
9892
+ const detail = error instanceof ClaudeQuotaError ? error.message : extractErrorText(error);
9893
+ const outcome = quotaFallbacks < MAX_QUOTA_FALLBACKS ? await this.fallBackCredential(quotaKind, detail) : "unavailable";
9894
+ if (outcome === "switched") {
9895
+ quotaFallbacks++;
9896
+ await this.tearDownSession();
9897
+ attempt++;
9898
+ continue;
9899
+ }
9900
+ await this.emitQuotaExhaustedEvent(quotaKind, outcome, detail);
9901
+ return;
9902
+ }
9737
9903
  if (_ClaudeManager.isAuthError(error)) {
9738
9904
  if (authRetries >= MAX_AUTH_RETRIES) {
9739
9905
  authRetryExhausted = true;
@@ -9748,7 +9914,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
9748
9914
  const refreshed = await claudeTokenManager.fetchFreshCredentials(
9749
9915
  error instanceof Error ? error.message : String(error)
9750
9916
  );
9751
- if (!refreshed) {
9917
+ if (!refreshed.ok) {
9752
9918
  authRetryExhausted = true;
9753
9919
  throw error;
9754
9920
  }
@@ -9840,6 +10006,11 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
9840
10006
  return "Claude authentication failed after multiple attempts. Check your Claude, Anthropic API key, or Bedrock credentials in Settings \u2192 Agents and try again.";
9841
10007
  }
9842
10008
  }
10009
+ async emitQuotaExhaustedEvent(kind, outcome, detail) {
10010
+ const limit = kind === "out_of_credits" ? "Claude ran out of credits on the credential this workspace is using." : "Claude hit the usage limit on the credential this workspace is using.";
10011
+ const next = outcome === "failed" ? "Switching to the alternate credential failed. Check the credentials in Settings \u2192 Agents and try again." : "No other credential is available for this workspace. Add one in Settings \u2192 Agents, or try again once the limit resets.";
10012
+ await this.emitTerminalErrorResult(detail, [`${limit} ${next}`]);
10013
+ }
9843
10014
  async emitMidTurnExhaustedEvent(error) {
9844
10015
  const detail = error instanceof Error ? error.message : String(error);
9845
10016
  await this.emitTerminalErrorResult(
@@ -10097,6 +10268,17 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
10097
10268
  }
10098
10269
  return;
10099
10270
  }
10271
+ const quotaError = _ClaudeManager.detectQuotaErrorInMessage(msg);
10272
+ if (quotaError) {
10273
+ suppressLinearResponseFlush = true;
10274
+ this.failPendingTurn(quotaError);
10275
+ try {
10276
+ response.close();
10277
+ } catch (err) {
10278
+ console.warn("[ClaudeManager] query.close() during quota error failed:", err);
10279
+ }
10280
+ return;
10281
+ }
10100
10282
  const transientErrorMessage = _ClaudeManager.detectTransientTurnErrorInMessage(msg);
10101
10283
  if (transientErrorMessage) {
10102
10284
  suppressLinearResponseFlush = true;
@@ -10293,6 +10475,22 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
10293
10475
  if (error instanceof ClaudeAuthError) return true;
10294
10476
  return isClaudeAuthErrorText(error instanceof Error ? error.message : String(error));
10295
10477
  }
10478
+ static quotaLimitKind(error) {
10479
+ if (error instanceof ClaudeQuotaError) return error.kind;
10480
+ return detectAgentQuotaLimit(extractErrorText(error));
10481
+ }
10482
+ static detectQuotaErrorInMessage(message) {
10483
+ if (message.type === "assistant" && "error" in message) {
10484
+ const content = Array.isArray(message.message?.content) ? message.message.content.map((block) => "text" in block ? block.text : "").join("\n") : "";
10485
+ const text2 = [typeof message.error === "string" ? message.error : "", content].filter(Boolean).join("\n");
10486
+ const kind2 = detectAgentQuotaLimit(text2);
10487
+ if (kind2) return new ClaudeQuotaError(text2, kind2);
10488
+ }
10489
+ if (message.type !== "result" || !message.is_error) return null;
10490
+ const text = message.subtype === "success" ? message.result : message.errors.join("\n");
10491
+ const kind = detectAgentQuotaLimit(text);
10492
+ return kind ? new ClaudeQuotaError(text, kind) : null;
10493
+ }
10296
10494
  static isTransientTurnError(error) {
10297
10495
  if (error instanceof ClaudeTransientTurnError) return true;
10298
10496
  return _ClaudeManager.isTransientTurnErrorText(extractErrorText(error));
@@ -10670,7 +10868,7 @@ var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
10670
10868
  var MIN_CODEX_CLI_VERSION = "0.144.6";
10671
10869
  var CODEX_UPGRADE_TIMEOUT_MS = 12e4;
10672
10870
  var codexCliVersionEnsured = null;
10673
- var ENGINE_PACKAGE_VERSION = "0.1.609";
10871
+ var ENGINE_PACKAGE_VERSION = "0.1.611";
10674
10872
  var INITIALIZE_METHOD = "initialize";
10675
10873
  var INITIALIZED_NOTIFICATION = "initialized";
10676
10874
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -11267,7 +11465,7 @@ function isQuotaTurnFailure(turn) {
11267
11465
  return false;
11268
11466
  }
11269
11467
  function isQuotaErrorMessage(error) {
11270
- return error instanceof Error && /\b429\b|rate.?limit|usage.?limit|quota|out.of.credits|credits?.?(?:depleted|exhausted|remaining)/i.test(error.message);
11468
+ return error instanceof Error && detectAgentQuotaLimit(error.message) !== null;
11271
11469
  }
11272
11470
  function formatTurnFailure(turn) {
11273
11471
  const turnError = turn.error;
@@ -11599,14 +11797,14 @@ var TranscriptUpdateCoalescer = class {
11599
11797
  // src/services/chat/history-paths.ts
11600
11798
  import { homedir as homedir13 } from "os";
11601
11799
  import { join as join17 } from "path";
11602
- var ENGINE_DIR2 = join17(homedir13(), ".replicas", "engine");
11603
- var CHATS_FILE = join17(ENGINE_DIR2, "chats.json");
11604
- var CLAUDE_HISTORY_DIR = join17(ENGINE_DIR2, "claude-histories");
11605
- var RELAY_HISTORY_DIR = join17(ENGINE_DIR2, "relay-histories");
11606
- var CODEX_HISTORY_DIR = join17(ENGINE_DIR2, "codex-histories");
11607
- var CURSOR_HISTORY_DIR = join17(ENGINE_DIR2, "cursor-histories");
11608
- var OPENCODE_HISTORY_DIR = join17(ENGINE_DIR2, "opencode-histories");
11609
- var PI_HISTORY_DIR = join17(ENGINE_DIR2, "pi-histories");
11800
+ var ENGINE_DIR3 = join17(homedir13(), ".replicas", "engine");
11801
+ var CHATS_FILE = join17(ENGINE_DIR3, "chats.json");
11802
+ var CLAUDE_HISTORY_DIR = join17(ENGINE_DIR3, "claude-histories");
11803
+ var RELAY_HISTORY_DIR = join17(ENGINE_DIR3, "relay-histories");
11804
+ var CODEX_HISTORY_DIR = join17(ENGINE_DIR3, "codex-histories");
11805
+ var CURSOR_HISTORY_DIR = join17(ENGINE_DIR3, "cursor-histories");
11806
+ var OPENCODE_HISTORY_DIR = join17(ENGINE_DIR3, "opencode-histories");
11807
+ var PI_HISTORY_DIR = join17(ENGINE_DIR3, "pi-histories");
11610
11808
  var HISTORY_DIR_BY_PROVIDER = {
11611
11809
  claude: CLAUDE_HISTORY_DIR,
11612
11810
  relay: RELAY_HISTORY_DIR,
@@ -11637,6 +11835,12 @@ var DuplicateDefaultChatError = class extends Error {
11637
11835
  };
11638
11836
 
11639
11837
  // src/managers/codex-asp/codex-asp-manager.ts
11838
+ var CodexQuotaError = class extends Error {
11839
+ constructor(message) {
11840
+ super(message);
11841
+ this.name = "CodexQuotaError";
11842
+ }
11843
+ };
11640
11844
  var GOAL_TURN_CONTINUATION_GRACE_MS = 5e3;
11641
11845
  var CODEX_SLASH_COMMANDS_CACHE_MS = 3e4;
11642
11846
  var POLICY_REASON_INJECTION_TIMEOUT_MS = 5e3;
@@ -11721,7 +11925,7 @@ var CodexAspManager = class extends CodingAgentManager {
11721
11925
  steeredTempImagePaths = [];
11722
11926
  steerLock = new AsyncLock();
11723
11927
  constructor(options) {
11724
- super(options);
11928
+ super({ ...options, provider: AGENT.CODEX });
11725
11929
  this.historyFile = options.historyFilePath ? new CodexHistoryFile(options.historyFilePath) : null;
11726
11930
  this.initializeManager(this.processMessageInternal.bind(this));
11727
11931
  }
@@ -11901,6 +12105,7 @@ var CodexAspManager = class extends CodingAgentManager {
11901
12105
  }
11902
12106
  async processMessageInternal(request) {
11903
12107
  let userMessageRecorded = false;
12108
+ let quotaFallbackAttempted = false;
11904
12109
  const recordUserMessage = (extraPayload = {}) => {
11905
12110
  if (userMessageRecorded) return;
11906
12111
  userMessageRecorded = true;
@@ -11946,17 +12151,26 @@ var CodexAspManager = class extends CodingAgentManager {
11946
12151
  await dispatch();
11947
12152
  } catch (error) {
11948
12153
  let terminalError = error;
12154
+ if (terminalError instanceof CodexQuotaError && !quotaFallbackAttempted) {
12155
+ quotaFallbackAttempted = true;
12156
+ const outcome = await this.fallBackCredential(
12157
+ detectAgentQuotaLimit(terminalError.message) ?? "rate_limit",
12158
+ terminalError.message
12159
+ );
12160
+ if (outcome === "switched") {
12161
+ const retryFailure = await this.restartAndRetry(dispatch);
12162
+ if (!retryFailure) return;
12163
+ terminalError = retryFailure.error;
12164
+ } else if (await this.confirmOutOfCredits(await getCodexAspHost())) {
12165
+ return;
12166
+ }
12167
+ }
11949
12168
  if (isCodexAuthError(terminalError)) {
11950
12169
  const refreshed = await codexTokenManager.fetchFreshCredentials(terminalError instanceof Error ? terminalError.message : String(terminalError));
11951
- if (refreshed) {
11952
- try {
11953
- await restartCodexAspHost();
11954
- this.threadAttached = false;
11955
- await dispatch();
11956
- return;
11957
- } catch (retryError) {
11958
- terminalError = retryError;
11959
- }
12170
+ if (refreshed.ok) {
12171
+ const retryFailure = await this.restartAndRetry(dispatch);
12172
+ if (!retryFailure) return;
12173
+ terminalError = retryFailure.error;
11960
12174
  }
11961
12175
  }
11962
12176
  const event = this.recordHistoryEvent("codex-asp-error", {
@@ -12008,16 +12222,14 @@ var CodexAspManager = class extends CodingAgentManager {
12008
12222
  try {
12009
12223
  completedTurn = await runTurn(host, threadId);
12010
12224
  } catch (error) {
12011
- if (isQuotaErrorMessage(error) && await this.confirmOutOfCredits(host)) {
12012
- return;
12225
+ if (isQuotaErrorMessage(error)) {
12226
+ throw new CodexQuotaError(error instanceof Error ? error.message : String(error));
12013
12227
  }
12014
12228
  throw error;
12015
12229
  }
12016
12230
  if (completedTurn.status === "failed") {
12017
- if (isQuotaTurnFailure(completedTurn) && await this.confirmOutOfCredits(host)) {
12018
- return;
12019
- }
12020
- throw new Error(formatTurnFailure(completedTurn));
12231
+ const failure = formatTurnFailure(completedTurn);
12232
+ throw isQuotaTurnFailure(completedTurn) ? new CodexQuotaError(failure) : new Error(failure);
12021
12233
  }
12022
12234
  this.emitQuotaEvent(this.quotaStatus.recordTurnSuccess());
12023
12235
  }
@@ -12887,6 +13099,16 @@ var CodexAspManager = class extends CodingAgentManager {
12887
13099
  } catch {
12888
13100
  }
12889
13101
  }
13102
+ async restartAndRetry(dispatch) {
13103
+ try {
13104
+ await restartCodexAspHost();
13105
+ this.threadAttached = false;
13106
+ await dispatch();
13107
+ return null;
13108
+ } catch (error) {
13109
+ return { error };
13110
+ }
13111
+ }
12890
13112
  async confirmOutOfCredits(host) {
12891
13113
  await this.refreshQuotaSnapshot(host);
12892
13114
  const event = this.quotaStatus.confirmOutOfCredits();
@@ -14966,34 +15188,93 @@ var RelayManager = class {
14966
15188
  }
14967
15189
  };
14968
15190
 
14969
- // src/analytics/analytics.service.ts
15191
+ // src/analytics/agent/activity/agent-chat-activity-tracker-service.ts
15192
+ import {
15193
+ appendFile as appendFile3,
15194
+ mkdir as mkdir14,
15195
+ readFile as readFile12,
15196
+ readdir as readdir6,
15197
+ rename as rename2,
15198
+ unlink as unlink3
15199
+ } from "fs/promises";
15200
+ import { join as join22 } from "path";
14970
15201
  import { randomUUID as randomUUID5 } from "crypto";
14971
15202
 
14972
- // src/analytics/analytics-buffer.service.ts
14973
- import { appendFile as appendFile3, mkdir as mkdir14, readFile as readFile12, readdir as readdir6, rename as rename2, unlink as unlink3 } from "fs/promises";
14974
- import { join as join22 } from "path";
15203
+ // src/analytics/agent/activity/skill-mcp-call-extractor.ts
15204
+ var NON_MCP_SERVERS = /* @__PURE__ */ new Set(["claude", "cursor", "opencode", "pi", "custom", "dynamic"]);
15205
+ function mcpNameFromToolCall(message) {
15206
+ const parsedName = parseMcpToolName(message.tool);
15207
+ if (parsedName) return parsedName.server;
15208
+ if (message.tool.startsWith("mcp.")) {
15209
+ const [server, ...toolParts] = message.tool.slice("mcp.".length).split(".");
15210
+ if (server && toolParts.length > 0) return server;
15211
+ }
15212
+ return NON_MCP_SERVERS.has(message.server) ? null : message.server;
15213
+ }
15214
+ function skillCallsFromCommand(message) {
15215
+ const skillNames = message.skillNames ?? [];
15216
+ return skillNames.map((skillName, index) => ({
15217
+ kind: "skill",
15218
+ id: skillNames.length === 1 ? message.id : `${message.id}-${index}`,
15219
+ skillName,
15220
+ occurredAt: message.timestamp
15221
+ }));
15222
+ }
15223
+ function extractSkillMcpCalls(messages, provider) {
15224
+ return messages.flatMap((message) => {
15225
+ if (message.type === "skill") {
15226
+ if (provider === "relay") return [];
15227
+ return [{ kind: "skill", id: message.id, skillName: message.skillName, occurredAt: message.timestamp }];
15228
+ }
15229
+ if (message.type === "command") {
15230
+ return skillCallsFromCommand(message);
15231
+ }
15232
+ if (message.type !== "tool_call") return [];
15233
+ const mcpName = mcpNameFromToolCall(message);
15234
+ return mcpName ? [{ kind: "mcp", id: message.id, mcpName, occurredAt: message.timestamp }] : [];
15235
+ });
15236
+ }
15237
+
15238
+ // src/analytics/agent/activity/agent-chat-activity-tracker-service.ts
14975
15239
  var MAX_FAILED_RECORDS = 500;
14976
15240
  var FLUSH_DEBOUNCE_MS = 15e3;
14977
15241
  var MAX_UPLOADED_SEGMENTS = 50;
14978
15242
  var UPLOADED_SUFFIX = ".uploaded";
14979
- var AnalyticsBufferService = class {
15243
+ var MAX_TRACKED_MESSAGES = 500;
15244
+ var AgentChatActivityBuffer = class {
14980
15245
  constructor(options) {
14981
15246
  this.options = options;
14982
- this.liveFile = join22(ENGINE_DIR2, `${options.name}.jsonl`);
14983
- this.segmentFilePattern = new RegExp(`^${options.name}\\.(\\d+)\\.jsonl$`);
15247
+ this.storageNames = [
15248
+ options.storageName,
15249
+ ...options.legacyStorageNames ?? []
15250
+ ];
15251
+ this.liveFile = join22(ENGINE_DIR, `${options.storageName}.jsonl`);
15252
+ this.segmentFilePatterns = this.storageNames.map(
15253
+ (storageName) => new RegExp(`^${storageName}\\.(\\d+)\\.jsonl$`)
15254
+ );
14984
15255
  }
14985
15256
  options;
14986
15257
  liveFile;
14987
- segmentFilePattern;
15258
+ storageNames;
15259
+ segmentFilePatterns;
14988
15260
  failedRecords = [];
14989
15261
  pendingAppends = /* @__PURE__ */ new Set();
14990
15262
  flushTimer = null;
14991
- activeFlush = Promise.resolve({ flushed: 0, failed: 0 });
15263
+ activeFlush = Promise.resolve({
15264
+ flushed: 0,
15265
+ failed: 0
15266
+ });
14992
15267
  append(record) {
14993
- const pending = mkdir14(ENGINE_DIR2, { recursive: true }).then(() => appendFile3(this.liveFile, `${JSON.stringify(record)}
14994
- `, "utf-8")).catch((error) => {
14995
- console.error(`[${this.options.name}] Append failed, will retry on flush:`, error);
14996
- if (this.failedRecords.length < MAX_FAILED_RECORDS) this.failedRecords.push(record);
15268
+ const pending = mkdir14(ENGINE_DIR, { recursive: true }).then(
15269
+ () => appendFile3(this.liveFile, `${JSON.stringify(record)}
15270
+ `, "utf-8")
15271
+ ).catch((error) => {
15272
+ console.error(
15273
+ `[${this.options.storageName}] Append failed, will retry on flush:`,
15274
+ error
15275
+ );
15276
+ if (this.failedRecords.length < MAX_FAILED_RECORDS)
15277
+ this.failedRecords.push(record);
14997
15278
  });
14998
15279
  this.pendingAppends.add(pending);
14999
15280
  void pending.finally(() => this.pendingAppends.delete(pending));
@@ -15003,7 +15284,10 @@ var AnalyticsBufferService = class {
15003
15284
  this.flushTimer = setTimeout(() => {
15004
15285
  this.flushTimer = null;
15005
15286
  void this.flush().catch((error) => {
15006
- console.error(`[${this.options.name}] Scheduled flush failed, retrying on the next record:`, error);
15287
+ console.error(
15288
+ `[${this.options.storageName}] Scheduled flush failed, retrying on the next record:`,
15289
+ error
15290
+ );
15007
15291
  });
15008
15292
  }, FLUSH_DEBOUNCE_MS);
15009
15293
  this.flushTimer.unref?.();
@@ -15019,20 +15303,30 @@ var AnalyticsBufferService = class {
15019
15303
  }
15020
15304
  async runFlush() {
15021
15305
  for (const record of this.failedRecords.splice(0)) this.append(record);
15022
- while (this.pendingAppends.size > 0) await Promise.allSettled([...this.pendingAppends]);
15023
- await rename2(this.liveFile, join22(ENGINE_DIR2, `${this.options.name}.${Date.now()}.jsonl`)).catch(() => {
15024
- });
15025
- const entries = await readdir6(ENGINE_DIR2).catch(() => []);
15306
+ while (this.pendingAppends.size > 0)
15307
+ await Promise.allSettled([...this.pendingAppends]);
15308
+ for (const storageName of this.storageNames) {
15309
+ await rename2(
15310
+ join22(ENGINE_DIR, `${storageName}.jsonl`),
15311
+ join22(ENGINE_DIR, `${storageName}.${Date.now()}.jsonl`)
15312
+ ).catch(() => {
15313
+ });
15314
+ }
15315
+ const entries = await readdir6(ENGINE_DIR).catch(() => []);
15026
15316
  let flushed = 0;
15027
15317
  let failed = 0;
15028
15318
  for (const entry of entries) {
15029
- if (!this.segmentFilePattern.test(entry)) continue;
15319
+ if (!this.segmentFilePatterns.some((pattern) => pattern.test(entry)))
15320
+ continue;
15030
15321
  try {
15031
- await this.uploadSegment(join22(ENGINE_DIR2, entry));
15322
+ await this.uploadSegment(join22(ENGINE_DIR, entry));
15032
15323
  flushed++;
15033
15324
  } catch (error) {
15034
15325
  failed++;
15035
- console.error(`[${this.options.name}] Segment upload failed, retained for retry:`, { entry, error });
15326
+ console.error(
15327
+ `[${this.options.storageName}] Segment upload failed, retained for retry:`,
15328
+ { entry, error }
15329
+ );
15036
15330
  }
15037
15331
  }
15038
15332
  const failedRecords = this.failedRecords.splice(0);
@@ -15042,13 +15336,22 @@ var AnalyticsBufferService = class {
15042
15336
  flushed++;
15043
15337
  } catch (error) {
15044
15338
  failed++;
15045
- this.failedRecords.unshift(...failedRecords.slice(0, MAX_FAILED_RECORDS));
15046
- console.error(`[${this.options.name}] Failed records upload failed, retained for retry:`, error);
15339
+ this.failedRecords.unshift(
15340
+ ...failedRecords.slice(0, MAX_FAILED_RECORDS)
15341
+ );
15342
+ console.error(
15343
+ `[${this.options.storageName}] Failed records upload failed, retained for retry:`,
15344
+ error
15345
+ );
15047
15346
  }
15048
15347
  }
15049
- const uploaded = entries.filter((entry) => entry.startsWith(`${this.options.name}.`) && entry.endsWith(UPLOADED_SUFFIX)).sort();
15348
+ const uploaded = entries.filter(
15349
+ (entry) => this.storageNames.some(
15350
+ (storageName) => entry.startsWith(`${storageName}.`)
15351
+ ) && entry.endsWith(UPLOADED_SUFFIX)
15352
+ ).sort();
15050
15353
  for (const entry of uploaded.slice(0, -MAX_UPLOADED_SEGMENTS)) {
15051
- await unlink3(join22(ENGINE_DIR2, entry)).catch(() => {
15354
+ await unlink3(join22(ENGINE_DIR, entry)).catch(() => {
15052
15355
  });
15053
15356
  }
15054
15357
  return { flushed, failed };
@@ -15067,66 +15370,40 @@ var AnalyticsBufferService = class {
15067
15370
  });
15068
15371
  }
15069
15372
  };
15070
-
15071
- // src/analytics/extract-skill-mcp-calls.ts
15072
- var NON_MCP_SERVERS = /* @__PURE__ */ new Set(["claude", "cursor", "opencode", "pi", "custom", "dynamic"]);
15073
- function mcpNameFromToolCall(message) {
15074
- const parsedName = parseMcpToolName(message.tool);
15075
- if (parsedName) return parsedName.server;
15076
- if (message.tool.startsWith("mcp.")) {
15077
- const [server, ...toolParts] = message.tool.slice("mcp.".length).split(".");
15078
- if (server && toolParts.length > 0) return server;
15373
+ var AgentChatActivityRecordTracker = class {
15374
+ pendingRecords = /* @__PURE__ */ new Map();
15375
+ buffer;
15376
+ constructor(options) {
15377
+ this.buffer = new AgentChatActivityBuffer(options);
15079
15378
  }
15080
- return NON_MCP_SERVERS.has(message.server) ? null : message.server;
15081
- }
15082
- function skillCallsFromCommand(message) {
15083
- const skillNames = message.skillNames ?? [];
15084
- return skillNames.map((skillName, index) => ({
15085
- kind: "skill",
15086
- id: skillNames.length === 1 ? message.id : `${message.id}-${index}`,
15087
- skillName,
15088
- occurredAt: message.timestamp
15089
- }));
15090
- }
15091
- function extractSkillMcpCalls(messages, provider) {
15092
- return messages.flatMap((message) => {
15093
- if (message.type === "skill") {
15094
- if (provider === "relay") return [];
15095
- return [{ kind: "skill", id: message.id, skillName: message.skillName, occurredAt: message.timestamp }];
15096
- }
15097
- if (message.type === "command") {
15098
- return skillCallsFromCommand(message);
15099
- }
15100
- if (message.type !== "tool_call") return [];
15101
- const mcpName = mcpNameFromToolCall(message);
15102
- return mcpName ? [{ kind: "mcp", id: message.id, mcpName, occurredAt: message.timestamp }] : [];
15103
- });
15104
- }
15105
-
15106
- // src/analytics/analytics.service.ts
15107
- var MAX_TRACKED_MESSAGES = 500;
15108
- async function uploadAnalyticsRecords(endpoint, body) {
15109
- const response = await monolithRequest(endpoint, { body });
15110
- if (!response.ok) throw new Error(`upload failed: ${response.status} ${await response.text()}`);
15111
- }
15112
- var AnalyticsService = class {
15379
+ track(chatId, record) {
15380
+ const records = this.pendingRecords.get(chatId) ?? [];
15381
+ records.push(record);
15382
+ this.pendingRecords.set(chatId, records);
15383
+ }
15384
+ noteTurnEnded(chatId) {
15385
+ const records = this.pendingRecords.get(chatId);
15386
+ if (!records) return;
15387
+ this.pendingRecords.delete(chatId);
15388
+ for (const record of records) this.buffer.append(record);
15389
+ this.buffer.scheduleFlush();
15390
+ }
15391
+ flush() {
15392
+ return this.buffer.flush();
15393
+ }
15394
+ };
15395
+ var AgentChatTurnActivityTracker = class {
15113
15396
  pendingMessages = /* @__PURE__ */ new Map();
15114
15397
  activeTurns = /* @__PURE__ */ new Map();
15115
- turnBuffer = new AnalyticsBufferService({
15116
- name: "turn-usage",
15117
- validate: isChatTurnUsageRecord,
15118
- upload: (turns) => uploadAnalyticsRecords("/v1/engine/chat-turn-usage", { turns })
15119
- });
15120
- skillBuffer = new AnalyticsBufferService({
15121
- name: "skill-usage",
15122
- validate: isSkillUsageRecord,
15123
- upload: (skills) => uploadAnalyticsRecords("/v1/engine/skill-usage", { skills })
15124
- });
15125
- mcpBuffer = new AnalyticsBufferService({
15126
- name: "mcp-usage",
15127
- validate: isMcpUsageRecord,
15128
- upload: (mcps) => uploadAnalyticsRecords("/v1/engine/mcp-usage", { mcps })
15129
- });
15398
+ turnBuffer;
15399
+ constructor(uploadTurns) {
15400
+ this.turnBuffer = new AgentChatActivityBuffer({
15401
+ storageName: "chat-turn-activity",
15402
+ legacyStorageNames: ["turn-usage"],
15403
+ validate: isAgentChatTurnActivityRecord,
15404
+ upload: uploadTurns
15405
+ });
15406
+ }
15130
15407
  noteMessageAccepted(messageId, request) {
15131
15408
  this.pendingMessages.set(messageId, {
15132
15409
  model: request.model,
@@ -15149,30 +15426,14 @@ var AnalyticsService = class {
15149
15426
  senderUserId: attributes.senderUserId,
15150
15427
  credentialMethod: credential?.method,
15151
15428
  credentialScope: credential?.scope,
15152
- skills: [],
15153
- mcps: [],
15154
15429
  seenCallIds: /* @__PURE__ */ new Set()
15155
15430
  });
15156
15431
  }
15157
- noteSkillMcpCalls(chatId, messages) {
15158
- const turn = this.activeTurns.get(chatId);
15159
- if (!turn) return;
15160
- for (const call of extractSkillMcpCalls(messages, turn.provider)) {
15161
- if (turn.seenCallIds.has(call.id)) continue;
15162
- turn.seenCallIds.add(call.id);
15163
- const common = {
15164
- callId: call.id,
15165
- chatId,
15166
- provider: turn.provider,
15167
- model: turn.model,
15168
- senderUserId: turn.senderUserId,
15169
- occurredAt: call.occurredAt,
15170
- credentialMethod: turn.credentialMethod,
15171
- credentialScope: turn.credentialScope
15172
- };
15173
- if (call.kind === "skill") turn.skills.push({ ...common, skillName: call.skillName });
15174
- else turn.mcps.push({ ...common, mcpName: call.mcpName });
15175
- }
15432
+ getActiveTurn(chatId) {
15433
+ return this.activeTurns.get(chatId);
15434
+ }
15435
+ getActiveChatIds() {
15436
+ return [...this.activeTurns.keys()];
15176
15437
  }
15177
15438
  noteTurnEnded(chatId) {
15178
15439
  const turn = this.activeTurns.get(chatId);
@@ -15189,22 +15450,91 @@ var AnalyticsService = class {
15189
15450
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
15190
15451
  seconds: Math.round((Date.now() - turn.startedAtMs) / 1e3)
15191
15452
  });
15192
- for (const skill of turn.skills) this.skillBuffer.append(skill);
15193
- for (const mcp of turn.mcps) this.mcpBuffer.append(mcp);
15194
15453
  this.turnBuffer.scheduleFlush();
15195
- if (turn.skills.length > 0) this.skillBuffer.scheduleFlush();
15196
- if (turn.mcps.length > 0) this.mcpBuffer.scheduleFlush();
15454
+ }
15455
+ flush() {
15456
+ return this.turnBuffer.flush();
15457
+ }
15458
+ };
15459
+ async function uploadAgentChatActivity(endpoint, body) {
15460
+ const response = await monolithRequest(endpoint, { body });
15461
+ if (!response.ok)
15462
+ throw new Error(
15463
+ `upload failed: ${response.status} ${await response.text()}`
15464
+ );
15465
+ }
15466
+ var AgentChatActivityTrackerService = class {
15467
+ turnActivityTracker;
15468
+ skillActivityTracker;
15469
+ mcpActivityTracker;
15470
+ constructor() {
15471
+ this.turnActivityTracker = new AgentChatTurnActivityTracker(
15472
+ (turns) => uploadAgentChatActivity("/v1/engine/chat-turn-activity", { turns })
15473
+ );
15474
+ this.skillActivityTracker = new AgentChatActivityRecordTracker({
15475
+ storageName: "skill-activity",
15476
+ legacyStorageNames: ["skill-usage"],
15477
+ validate: isAgentChatSkillActivityRecord,
15478
+ upload: (skills) => uploadAgentChatActivity("/v1/engine/skill-activity", { skills })
15479
+ });
15480
+ this.mcpActivityTracker = new AgentChatActivityRecordTracker({
15481
+ storageName: "mcp-activity",
15482
+ legacyStorageNames: ["mcp-usage"],
15483
+ validate: isAgentChatMcpActivityRecord,
15484
+ upload: (mcps) => uploadAgentChatActivity("/v1/engine/mcp-activity", { mcps })
15485
+ });
15486
+ }
15487
+ noteMessageAccepted(messageId, request) {
15488
+ this.turnActivityTracker.noteMessageAccepted(messageId, request);
15489
+ }
15490
+ noteTurnStarted(chatId, messageId, provider) {
15491
+ this.turnActivityTracker.noteTurnStarted(chatId, messageId, provider);
15492
+ }
15493
+ noteSkillMcpCalls(chatId, messages) {
15494
+ const turn = this.turnActivityTracker.getActiveTurn(chatId);
15495
+ if (!turn) return;
15496
+ for (const call of extractSkillMcpCalls(messages, turn.provider)) {
15497
+ if (turn.seenCallIds.has(call.id)) continue;
15498
+ turn.seenCallIds.add(call.id);
15499
+ const activityRecord = {
15500
+ callId: call.id,
15501
+ chatId,
15502
+ provider: turn.provider,
15503
+ model: turn.model,
15504
+ senderUserId: turn.senderUserId,
15505
+ occurredAt: call.occurredAt,
15506
+ credentialMethod: turn.credentialMethod,
15507
+ credentialScope: turn.credentialScope
15508
+ };
15509
+ if (call.kind === "skill") {
15510
+ this.skillActivityTracker.track(chatId, {
15511
+ ...activityRecord,
15512
+ skillName: call.skillName
15513
+ });
15514
+ } else {
15515
+ this.mcpActivityTracker.track(chatId, {
15516
+ ...activityRecord,
15517
+ mcpName: call.mcpName
15518
+ });
15519
+ }
15520
+ }
15521
+ }
15522
+ noteTurnEnded(chatId) {
15523
+ this.skillActivityTracker.noteTurnEnded(chatId);
15524
+ this.mcpActivityTracker.noteTurnEnded(chatId);
15525
+ this.turnActivityTracker.noteTurnEnded(chatId);
15197
15526
  }
15198
15527
  async flush(finalizeInFlight = true) {
15199
15528
  if (finalizeInFlight) {
15200
- for (const chatId of this.activeTurns.keys()) this.noteTurnEnded(chatId);
15529
+ for (const chatId of this.turnActivityTracker.getActiveChatIds())
15530
+ this.noteTurnEnded(chatId);
15201
15531
  }
15202
- const [turnUsage, skillUsage, mcpUsage] = await Promise.all([
15203
- this.turnBuffer.flush(),
15204
- this.skillBuffer.flush(),
15205
- this.mcpBuffer.flush()
15532
+ const [turnActivity, skillActivity, mcpActivity] = await Promise.all([
15533
+ this.turnActivityTracker.flush(),
15534
+ this.skillActivityTracker.flush(),
15535
+ this.mcpActivityTracker.flush()
15206
15536
  ]);
15207
- return { turnUsage, skillUsage, mcpUsage };
15537
+ return { turnActivity, skillActivity, mcpActivity };
15208
15538
  }
15209
15539
  };
15210
15540
 
@@ -15490,7 +15820,7 @@ import { basename as basename2, join as join25 } from "path";
15490
15820
 
15491
15821
  // src/services/chat/chat-senders.ts
15492
15822
  import { join as join24 } from "path";
15493
- var CHAT_SENDERS_DIR = join24(ENGINE_DIR2, "chat-senders");
15823
+ var CHAT_SENDERS_DIR = join24(ENGINE_DIR3, "chat-senders");
15494
15824
  function chatMessageSendersFilePath(chatId) {
15495
15825
  return join24(CHAT_SENDERS_DIR, `${chatId}.jsonl`);
15496
15826
  }
@@ -15508,9 +15838,9 @@ function parseChatMessageSendersJsonl(content) {
15508
15838
 
15509
15839
  // src/services/upload-chat-transcripts.ts
15510
15840
  var HISTORY_DIRS = [
15511
- join25(ENGINE_DIR2, "claude-histories"),
15512
- join25(ENGINE_DIR2, "relay-histories"),
15513
- join25(ENGINE_DIR2, "codex-histories")
15841
+ join25(ENGINE_DIR3, "claude-histories"),
15842
+ join25(ENGINE_DIR3, "relay-histories"),
15843
+ join25(ENGINE_DIR3, "codex-histories")
15514
15844
  ];
15515
15845
  async function putTranscript(uploadUrl, filePath, size) {
15516
15846
  await new Promise((resolve5, reject) => {
@@ -15881,20 +16211,20 @@ function corruptChatsFilePath() {
15881
16211
  return `${CHATS_FILE}.corrupt-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
15882
16212
  }
15883
16213
  var ChatService = class {
15884
- constructor(workingDirectory, analyticsService2) {
16214
+ constructor(workingDirectory, agentChatActivityTrackerService2) {
15885
16215
  this.workingDirectory = workingDirectory;
15886
- this.analyticsService = analyticsService2;
16216
+ this.agentChatActivityTrackerService = agentChatActivityTrackerService2;
15887
16217
  keepAliveService.setActivityCheck(
15888
16218
  () => [...this.chats.values()].some((chat) => !chat.persisted.deletedAt && hasActiveAgentWork(chat))
15889
16219
  );
15890
16220
  }
15891
16221
  workingDirectory;
15892
- analyticsService;
16222
+ agentChatActivityTrackerService;
15893
16223
  chats = /* @__PURE__ */ new Map();
15894
16224
  persistInFlight = false;
15895
16225
  persistQueued = false;
15896
16226
  async initialize() {
15897
- await mkdir15(ENGINE_DIR2, { recursive: true });
16227
+ await mkdir15(ENGINE_DIR3, { recursive: true });
15898
16228
  await mkdir15(CLAUDE_HISTORY_DIR, { recursive: true });
15899
16229
  await mkdir15(RELAY_HISTORY_DIR, { recursive: true });
15900
16230
  await mkdir15(CODEX_HISTORY_DIR, { recursive: true });
@@ -16011,7 +16341,7 @@ var ChatService = class {
16011
16341
  request.images
16012
16342
  );
16013
16343
  chat.pendingMessageIds.push(result.messageId);
16014
- this.analyticsService.noteMessageAccepted(result.messageId, request);
16344
+ this.agentChatActivityTrackerService.noteMessageAccepted(result.messageId, request);
16015
16345
  if (request.errorNotificationTarget) {
16016
16346
  chat.errorNotificationTargets.set(result.messageId, request.errorNotificationTarget);
16017
16347
  }
@@ -16067,7 +16397,7 @@ var ChatService = class {
16067
16397
  async interrupt(chatId) {
16068
16398
  const chat = this.requireChat(chatId);
16069
16399
  const result = await chat.provider.interrupt();
16070
- this.analyticsService.noteTurnEnded(chatId);
16400
+ this.agentChatActivityTrackerService.noteTurnEnded(chatId);
16071
16401
  chat.hasActiveTurn = false;
16072
16402
  chat.activeMessageId = null;
16073
16403
  chat.pendingMessageIds = [];
@@ -16089,7 +16419,7 @@ var ChatService = class {
16089
16419
  return { interrupted: false, queue: [], goal: null };
16090
16420
  }
16091
16421
  const interruptResult = await chat.provider.interrupt();
16092
- this.analyticsService.noteTurnEnded(chatId);
16422
+ this.agentChatActivityTrackerService.noteTurnEnded(chatId);
16093
16423
  chat.hasActiveTurn = false;
16094
16424
  chat.activeMessageId = null;
16095
16425
  chat.pendingMessageIds = [];
@@ -16176,7 +16506,7 @@ var ChatService = class {
16176
16506
  messageId,
16177
16507
  ...chat.pendingMessageIds.filter((pendingMessageId) => pendingMessageId !== messageId && pendingMessageId !== interruptedMessageId)
16178
16508
  ];
16179
- this.analyticsService.noteTurnEnded(chatId);
16509
+ this.agentChatActivityTrackerService.noteTurnEnded(chatId);
16180
16510
  chat.hasActiveTurn = false;
16181
16511
  chat.activeMessageId = null;
16182
16512
  if (interruptedMessageId) {
@@ -16310,14 +16640,22 @@ var ChatService = class {
16310
16640
  const chatsById = new Map(
16311
16641
  [...this.chats.entries()].map(([chatId, chat]) => [chatId, this.toSummary(chat)])
16312
16642
  );
16313
- const [chatTranscripts, canvas, repoState, engineLogs, analytics] = await Promise.all([
16643
+ const [chatTranscripts, canvas, repoState, engineLogs, chatActivity] = await Promise.all([
16314
16644
  flushAllChatTranscripts(chatsById),
16315
16645
  flushAllCanvasItems(),
16316
16646
  flushRepoState(),
16317
16647
  flushAllEngineLogs(),
16318
- this.analyticsService.flush()
16648
+ this.agentChatActivityTrackerService.flush()
16319
16649
  ]);
16320
- return { chatTranscripts, canvas, repoState, engineLogs, ...analytics };
16650
+ return {
16651
+ chatTranscripts,
16652
+ canvas,
16653
+ repoState,
16654
+ engineLogs,
16655
+ turnActivity: chatActivity.turnActivity,
16656
+ skillActivity: chatActivity.skillActivity,
16657
+ mcpActivity: chatActivity.mcpActivity
16658
+ };
16321
16659
  }
16322
16660
  createRuntimeChat(persisted) {
16323
16661
  const saveSession = async (sessionId) => {
@@ -16474,7 +16812,7 @@ var ChatService = class {
16474
16812
  }
16475
16813
  chat.hasActiveTurn = true;
16476
16814
  chat.activeMessageId = messageId;
16477
- this.analyticsService.noteTurnStarted(chat.persisted.id, messageId, chat.persisted.provider);
16815
+ this.agentChatActivityTrackerService.noteTurnStarted(chat.persisted.id, messageId, chat.persisted.provider);
16478
16816
  chat.activeErrorNotificationTarget = chat.errorNotificationTargets.get(messageId) ?? null;
16479
16817
  chat.errorNotificationTargets.delete(messageId);
16480
16818
  this.publish({
@@ -16492,7 +16830,7 @@ var ChatService = class {
16492
16830
  filter: false,
16493
16831
  parentToolUseId: typeof event.payload.parent_tool_use_id === "string" ? event.payload.parent_tool_use_id : null
16494
16832
  });
16495
- this.analyticsService.noteSkillMcpCalls(chatId, displayMessages);
16833
+ this.agentChatActivityTrackerService.noteSkillMcpCalls(chatId, displayMessages);
16496
16834
  }
16497
16835
  const terminalErrors = terminalErrorsFromEvent(event, chat.persisted.provider, codexTranscript);
16498
16836
  if (terminalErrors !== void 0) {
@@ -16537,7 +16875,7 @@ var ChatService = class {
16537
16875
  }
16538
16876
  }).catch(() => {
16539
16877
  });
16540
- if (event.type === "replicas-tool-input-request" || event.type === "replicas-tool-input-resolved" || event.type === "compaction-status" || event.type === AUTH_RETRY_STATUS_EVENT_TYPE || event.type === CHAT_GOAL_EVENT_TYPE || event.type === CLAUDE_ACTIVITY_STATUS_EVENT_TYPE || event.type === "claude-result" || event.type === "claude-system" && (event.payload.subtype === "init" || event.payload.subtype === "session_state_changed" || event.payload.subtype === "background_tasks_changed" || coerceBackgroundTaskPayload(event.payload) !== null)) {
16878
+ if (event.type === "replicas-tool-input-request" || event.type === "replicas-tool-input-resolved" || event.type === "compaction-status" || event.type === AUTH_RETRY_STATUS_EVENT_TYPE || event.type === AUTH_FALLBACK_EVENT_TYPE || event.type === CHAT_GOAL_EVENT_TYPE || event.type === CLAUDE_ACTIVITY_STATUS_EVENT_TYPE || event.type === "claude-result" || event.type === "claude-system" && (event.payload.subtype === "init" || event.payload.subtype === "session_state_changed" || event.payload.subtype === "background_tasks_changed" || coerceBackgroundTaskPayload(event.payload) !== null)) {
16541
16879
  this.publish({
16542
16880
  type: "chat.updated",
16543
16881
  payload: { chat: this.toSummary(chat) }
@@ -16556,7 +16894,7 @@ var ChatService = class {
16556
16894
  }
16557
16895
  chat.hasActiveTurn = false;
16558
16896
  chat.activeMessageId = null;
16559
- this.analyticsService.noteTurnEnded(chatId);
16897
+ this.agentChatActivityTrackerService.noteTurnEnded(chatId);
16560
16898
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
16561
16899
  this.publish({
16562
16900
  type: "chat.turn.completed",
@@ -18592,8 +18930,8 @@ var authMiddleware = async (c, next) => {
18592
18930
  }
18593
18931
  await next();
18594
18932
  };
18595
- var analyticsService = new AnalyticsService();
18596
- var chatService = new ChatService(gitService.getWorkspaceRoot(), analyticsService);
18933
+ var agentChatActivityTrackerService = new AgentChatActivityTrackerService();
18934
+ var chatService = new ChatService(gitService.getWorkspaceRoot(), agentChatActivityTrackerService);
18597
18935
  app.get("/health", async (c) => {
18598
18936
  const requestedWaitMs = Number(c.req.query(ENGINE_HEALTH_WAIT_QUERY_PARAM));
18599
18937
  const waitMs = Number.isFinite(requestedWaitMs) ? Math.min(Math.max(requestedWaitMs, 0), ENGINE_HEALTH_MAX_WAIT_MS) : 0;