@mcoda/core 0.1.26 → 0.1.28

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.
@@ -207,6 +207,90 @@ const normalizeSlugList = (input) => {
207
207
  }
208
208
  return Array.from(cleaned);
209
209
  };
210
+ const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
211
+ const normalizeFailoverEvents = (value) => {
212
+ if (!Array.isArray(value))
213
+ return [];
214
+ const events = [];
215
+ for (const entry of value) {
216
+ if (!isRecord(entry))
217
+ continue;
218
+ if (typeof entry.type !== "string" || entry.type.trim().length === 0)
219
+ continue;
220
+ events.push({ ...entry });
221
+ }
222
+ return events;
223
+ };
224
+ const mergeFailoverEvents = (left, right) => {
225
+ if (!left.length)
226
+ return right;
227
+ if (!right.length)
228
+ return left;
229
+ const seen = new Set();
230
+ const merged = [];
231
+ const signature = (event) => [
232
+ event.type ?? "",
233
+ event.fromAgentId ?? "",
234
+ event.toAgentId ?? "",
235
+ event.at ?? "",
236
+ event.until ?? "",
237
+ event.durationMs ?? "",
238
+ ].join("|");
239
+ for (const event of [...left, ...right]) {
240
+ const key = signature(event);
241
+ if (seen.has(key))
242
+ continue;
243
+ seen.add(key);
244
+ merged.push(event);
245
+ }
246
+ return merged;
247
+ };
248
+ const mergeInvocationMetadata = (current, incoming) => {
249
+ if (!current && !incoming)
250
+ return undefined;
251
+ if (!incoming)
252
+ return current;
253
+ if (!current)
254
+ return { ...incoming };
255
+ const merged = { ...current, ...incoming };
256
+ const currentEvents = normalizeFailoverEvents(current.failoverEvents);
257
+ const incomingEvents = normalizeFailoverEvents(incoming.failoverEvents);
258
+ if (currentEvents.length > 0 || incomingEvents.length > 0) {
259
+ merged.failoverEvents = mergeFailoverEvents(currentEvents, incomingEvents);
260
+ }
261
+ return merged;
262
+ };
263
+ const summarizeFailoverEvent = (event) => {
264
+ const type = String(event.type ?? "unknown");
265
+ if (type === "switch_agent") {
266
+ const from = typeof event.fromAgentId === "string" ? event.fromAgentId : "unknown";
267
+ const to = typeof event.toAgentId === "string" ? event.toAgentId : "unknown";
268
+ return `switch_agent ${from} -> ${to}`;
269
+ }
270
+ if (type === "sleep_until_reset") {
271
+ const duration = typeof event.durationMs === "number" && Number.isFinite(event.durationMs)
272
+ ? `${Math.round(event.durationMs / 1000)}s`
273
+ : "unknown duration";
274
+ const until = typeof event.until === "string" ? event.until : "unknown";
275
+ return `sleep_until_reset ${duration} (until ${until})`;
276
+ }
277
+ if (type === "stream_restart_after_limit") {
278
+ const from = typeof event.fromAgentId === "string" ? event.fromAgentId : "unknown";
279
+ return `stream_restart_after_limit from ${from}`;
280
+ }
281
+ return type;
282
+ };
283
+ const resolveFailoverAgentId = (events, fallbackAgentId) => {
284
+ for (let index = events.length - 1; index >= 0; index -= 1) {
285
+ const event = events[index];
286
+ if (event?.type !== "switch_agent")
287
+ continue;
288
+ if (typeof event.toAgentId === "string" && event.toAgentId.trim().length > 0) {
289
+ return event.toAgentId;
290
+ }
291
+ }
292
+ return fallbackAgentId;
293
+ };
210
294
  const normalizePath = (value) => value
211
295
  .replace(/\\/g, "/")
212
296
  .replace(/^\.\//, "")
@@ -1990,6 +2074,45 @@ export class CodeReviewService {
1990
2074
  metadata: { commandName: "code-review", phase, action: phase, attempt },
1991
2075
  });
1992
2076
  };
2077
+ const logFailoverEvents = async (events) => {
2078
+ if (!events.length)
2079
+ return;
2080
+ for (const event of events) {
2081
+ await this.deps.workspaceRepo.insertTaskLog({
2082
+ taskRunId: taskRun.id,
2083
+ sequence: this.sequenceForTask(taskRun.id),
2084
+ timestamp: new Date().toISOString(),
2085
+ source: "agent_failover",
2086
+ message: `Agent failover: ${summarizeFailoverEvent(event)}`,
2087
+ details: event,
2088
+ });
2089
+ }
2090
+ };
2091
+ const resolveUsageAgent = async (fallback, events) => {
2092
+ const usageAgentId = resolveFailoverAgentId(events, fallback.id);
2093
+ if (usageAgentId === fallback.id)
2094
+ return fallback;
2095
+ const resolver = this.deps.agentService?.resolveAgent;
2096
+ if (typeof resolver !== "function")
2097
+ return fallback;
2098
+ try {
2099
+ const resolved = await resolver.call(this.deps.agentService, usageAgentId);
2100
+ return {
2101
+ id: resolved.id,
2102
+ defaultModel: typeof resolved.defaultModel === "string" ? resolved.defaultModel : fallback.defaultModel,
2103
+ };
2104
+ }
2105
+ catch (error) {
2106
+ await this.deps.workspaceRepo.insertTaskLog({
2107
+ taskRunId: taskRun.id,
2108
+ sequence: this.sequenceForTask(taskRun.id),
2109
+ timestamp: new Date().toISOString(),
2110
+ source: "agent_failover",
2111
+ message: `Unable to resolve failover agent (${usageAgentId}) for usage accounting: ${error instanceof Error ? error.message : String(error)}`,
2112
+ });
2113
+ return fallback;
2114
+ }
2115
+ };
1993
2116
  agentOutput = "";
1994
2117
  let durationSeconds = 0;
1995
2118
  let lastStreamMeta;
@@ -2002,7 +2125,7 @@ export class CodeReviewService {
2002
2125
  if (useStream && this.deps.agentService.invokeStream) {
2003
2126
  const stream = await withAbort(this.deps.agentService.invokeStream(agentToUse.id, {
2004
2127
  input: prompt,
2005
- metadata: { taskKey: task.key, retry: logSource === "agent_retry" },
2128
+ metadata: { command: "code-review", taskKey: task.key, retry: logSource === "agent_retry" },
2006
2129
  }));
2007
2130
  while (true) {
2008
2131
  abortIfSignaled();
@@ -2011,7 +2134,7 @@ export class CodeReviewService {
2011
2134
  break;
2012
2135
  const chunk = value;
2013
2136
  output += chunk.output ?? "";
2014
- metadata = chunk.metadata ?? metadata;
2137
+ metadata = mergeInvocationMetadata(metadata, chunk.metadata);
2015
2138
  await this.deps.workspaceRepo.insertTaskLog({
2016
2139
  taskRunId: taskRun.id,
2017
2140
  sequence: this.sequenceForTask(taskRun.id),
@@ -2024,10 +2147,10 @@ export class CodeReviewService {
2024
2147
  else {
2025
2148
  const response = await withAbort(this.deps.agentService.invoke(agentToUse.id, {
2026
2149
  input: prompt,
2027
- metadata: { taskKey: task.key, retry: logSource === "agent_retry" },
2150
+ metadata: { command: "code-review", taskKey: task.key, retry: logSource === "agent_retry" },
2028
2151
  }));
2029
2152
  output = response.output ?? "";
2030
- metadata = response.metadata;
2153
+ metadata = mergeInvocationMetadata(metadata, response.metadata);
2031
2154
  await this.deps.workspaceRepo.insertTaskLog({
2032
2155
  taskRunId: taskRun.id,
2033
2156
  sequence: this.sequenceForTask(taskRun.id),
@@ -2074,7 +2197,15 @@ export class CodeReviewService {
2074
2197
  model: (lastStreamMeta.model ?? lastStreamMeta.model_name ?? null),
2075
2198
  }
2076
2199
  : undefined;
2077
- await recordUsage("review_main", prompt, agentOutput, durationSeconds, tokenMetaMain, agentUsedForOutput, outputAttempt);
2200
+ const mainFailoverEvents = normalizeFailoverEvents(lastStreamMeta?.failoverEvents);
2201
+ await logFailoverEvents(mainFailoverEvents);
2202
+ const usageAgentForOutput = await resolveUsageAgent({
2203
+ id: agentUsedForOutput.id,
2204
+ defaultModel: typeof agentUsedForOutput?.defaultModel === "string"
2205
+ ? agentUsedForOutput.defaultModel
2206
+ : undefined,
2207
+ }, mainFailoverEvents);
2208
+ await recordUsage("review_main", prompt, agentOutput, durationSeconds, tokenMetaMain, usageAgentForOutput, outputAttempt);
2078
2209
  const primaryOutput = agentOutput;
2079
2210
  let retryOutput;
2080
2211
  let retryAgentUsed;
@@ -2126,7 +2257,10 @@ export class CodeReviewService {
2126
2257
  message: `Retrying with JSON-only agent override: ${retryAgentUsed.slug ?? retryAgentUsed.id}`,
2127
2258
  });
2128
2259
  }
2129
- const retryResp = await withAbort(this.deps.agentService.invoke(retryAgentUsed.id, { input: retryPrompt, metadata: { taskKey: task.key, retry: true } }));
2260
+ const retryResp = await withAbort(this.deps.agentService.invoke(retryAgentUsed.id, {
2261
+ input: retryPrompt,
2262
+ metadata: { command: "code-review", taskKey: task.key, retry: true },
2263
+ }));
2130
2264
  retryOutput = retryResp.output ?? "";
2131
2265
  const retryDuration = Math.round(((Date.now() - retryStarted) / 1000) * 1000) / 1000;
2132
2266
  await this.deps.workspaceRepo.insertTaskLog({
@@ -2150,7 +2284,15 @@ export class CodeReviewService {
2150
2284
  model: (retryResp.metadata.model ?? retryResp.metadata.model_name ?? null),
2151
2285
  }
2152
2286
  : undefined;
2153
- await recordUsage("review_retry", retryPrompt, retryOutput, retryDuration, retryTokenMeta, retryAgentUsed, 2);
2287
+ const retryFailoverEvents = normalizeFailoverEvents(retryResp.metadata?.failoverEvents);
2288
+ await logFailoverEvents(retryFailoverEvents);
2289
+ const retryUsageAgent = await resolveUsageAgent({
2290
+ id: retryAgentUsed.id,
2291
+ defaultModel: typeof retryAgentUsed?.defaultModel === "string"
2292
+ ? retryAgentUsed.defaultModel
2293
+ : undefined,
2294
+ }, retryFailoverEvents);
2295
+ await recordUsage("review_retry", retryPrompt, retryOutput, retryDuration, retryTokenMeta, retryUsageAgent, 2);
2154
2296
  normalization = normalizeReviewOutput(retryOutput);
2155
2297
  parsed = normalization.result;
2156
2298
  validationError = validateReviewOutput(parsed, { requireCommentSlugs });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcoda/core",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Core services and APIs for the mcoda CLI.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,11 +32,11 @@
32
32
  "dependencies": {
33
33
  "@apidevtools/swagger-parser": "^10.1.0",
34
34
  "yaml": "^2.4.2",
35
- "@mcoda/shared": "0.1.26",
36
- "@mcoda/agents": "0.1.26",
37
- "@mcoda/generators": "0.1.26",
38
- "@mcoda/integrations": "0.1.26",
39
- "@mcoda/db": "0.1.26"
35
+ "@mcoda/shared": "0.1.28",
36
+ "@mcoda/generators": "0.1.28",
37
+ "@mcoda/db": "0.1.28",
38
+ "@mcoda/agents": "0.1.28",
39
+ "@mcoda/integrations": "0.1.28"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc -p tsconfig.json",