@zhixuan92/multi-model-agent-core 3.10.7 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +3 -3
  2. package/dist/config/schema.d.ts +15 -0
  3. package/dist/config/schema.d.ts.map +1 -1
  4. package/dist/config/schema.js +17 -2
  5. package/dist/config/schema.js.map +1 -1
  6. package/dist/diagnostics/types.d.ts +11 -0
  7. package/dist/diagnostics/types.d.ts.map +1 -1
  8. package/dist/escalation/fallback.d.ts +14 -0
  9. package/dist/escalation/fallback.d.ts.map +1 -1
  10. package/dist/escalation/fallback.js +254 -19
  11. package/dist/escalation/fallback.js.map +1 -1
  12. package/dist/executors/audit.d.ts.map +1 -1
  13. package/dist/executors/audit.js +6 -4
  14. package/dist/executors/audit.js.map +1 -1
  15. package/dist/executors/debug.d.ts.map +1 -1
  16. package/dist/executors/debug.js +5 -3
  17. package/dist/executors/debug.js.map +1 -1
  18. package/dist/executors/delegate.d.ts +12 -0
  19. package/dist/executors/delegate.d.ts.map +1 -1
  20. package/dist/executors/delegate.js +45 -11
  21. package/dist/executors/delegate.js.map +1 -1
  22. package/dist/executors/execute-plan.d.ts.map +1 -1
  23. package/dist/executors/execute-plan.js +6 -4
  24. package/dist/executors/execute-plan.js.map +1 -1
  25. package/dist/executors/retry.js +1 -1
  26. package/dist/executors/retry.js.map +1 -1
  27. package/dist/executors/review.js +1 -1
  28. package/dist/executors/review.js.map +1 -1
  29. package/dist/executors/shared-compute.js +4 -4
  30. package/dist/executors/shared-compute.js.map +1 -1
  31. package/dist/executors/types.d.ts +1 -1
  32. package/dist/executors/types.d.ts.map +1 -1
  33. package/dist/executors/verify.js +2 -2
  34. package/dist/executors/verify.js.map +1 -1
  35. package/dist/heartbeat.d.ts +5 -5
  36. package/dist/heartbeat.d.ts.map +1 -1
  37. package/dist/heartbeat.js +21 -17
  38. package/dist/heartbeat.js.map +1 -1
  39. package/dist/index.d.ts +4 -3
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +5 -3
  42. package/dist/index.js.map +1 -1
  43. package/dist/intake/compilers/audit.d.ts.map +1 -1
  44. package/dist/intake/compilers/audit.js +5 -2
  45. package/dist/intake/compilers/audit.js.map +1 -1
  46. package/dist/intake/compilers/debug.d.ts.map +1 -1
  47. package/dist/intake/compilers/debug.js +4 -0
  48. package/dist/intake/compilers/debug.js.map +1 -1
  49. package/dist/intake/compilers/delegate.d.ts +3 -0
  50. package/dist/intake/compilers/delegate.d.ts.map +1 -1
  51. package/dist/intake/compilers/delegate.js +5 -1
  52. package/dist/intake/compilers/delegate.js.map +1 -1
  53. package/dist/intake/compilers/execute-plan.d.ts.map +1 -1
  54. package/dist/intake/compilers/execute-plan.js +5 -0
  55. package/dist/intake/compilers/execute-plan.js.map +1 -1
  56. package/dist/intake/compilers/review.d.ts.map +1 -1
  57. package/dist/intake/compilers/review.js +3 -0
  58. package/dist/intake/compilers/review.js.map +1 -1
  59. package/dist/intake/compilers/verify.d.ts.map +1 -1
  60. package/dist/intake/compilers/verify.js +7 -0
  61. package/dist/intake/compilers/verify.js.map +1 -1
  62. package/dist/intake/force-clarification.d.ts +5 -0
  63. package/dist/intake/force-clarification.d.ts.map +1 -0
  64. package/dist/intake/force-clarification.js +44 -0
  65. package/dist/intake/force-clarification.js.map +1 -0
  66. package/dist/intake/pipeline.d.ts +1 -1
  67. package/dist/intake/pipeline.d.ts.map +1 -1
  68. package/dist/intake/pipeline.js +32 -1
  69. package/dist/intake/pipeline.js.map +1 -1
  70. package/dist/intake/resolve.d.ts.map +1 -1
  71. package/dist/intake/resolve.js +0 -1
  72. package/dist/intake/resolve.js.map +1 -1
  73. package/dist/observability/bus.d.ts.map +1 -1
  74. package/dist/observability/bus.js +20 -0
  75. package/dist/observability/bus.js.map +1 -1
  76. package/dist/observability/events.d.ts +81 -4
  77. package/dist/observability/events.d.ts.map +1 -1
  78. package/dist/observability/events.js +77 -2
  79. package/dist/observability/events.js.map +1 -1
  80. package/dist/provider.d.ts +1 -0
  81. package/dist/provider.d.ts.map +1 -1
  82. package/dist/provider.js +8 -1
  83. package/dist/provider.js.map +1 -1
  84. package/dist/review/diff-review.d.ts +1 -0
  85. package/dist/review/diff-review.d.ts.map +1 -1
  86. package/dist/review/diff-review.js +1 -0
  87. package/dist/review/diff-review.js.map +1 -1
  88. package/dist/review/quality-reviewer.d.ts +1 -1
  89. package/dist/review/quality-reviewer.d.ts.map +1 -1
  90. package/dist/review/quality-reviewer.js +6 -6
  91. package/dist/review/quality-reviewer.js.map +1 -1
  92. package/dist/review/spec-reviewer.d.ts +1 -1
  93. package/dist/review/spec-reviewer.d.ts.map +1 -1
  94. package/dist/review/spec-reviewer.js +3 -1
  95. package/dist/review/spec-reviewer.js.map +1 -1
  96. package/dist/routing/canonical-model-identity.d.ts +9 -0
  97. package/dist/routing/canonical-model-identity.d.ts.map +1 -0
  98. package/dist/routing/canonical-model-identity.js +54 -0
  99. package/dist/routing/canonical-model-identity.js.map +1 -0
  100. package/dist/run-tasks/execute-task.js +1 -1
  101. package/dist/run-tasks/execute-task.js.map +1 -1
  102. package/dist/run-tasks/index.js +1 -1
  103. package/dist/run-tasks/index.js.map +1 -1
  104. package/dist/run-tasks/reviewed-lifecycle.d.ts.map +1 -1
  105. package/dist/run-tasks/reviewed-lifecycle.js +145 -31
  106. package/dist/run-tasks/reviewed-lifecycle.js.map +1 -1
  107. package/dist/runners/base/result-builders.d.ts +13 -2
  108. package/dist/runners/base/result-builders.d.ts.map +1 -1
  109. package/dist/runners/base/result-builders.js +25 -1
  110. package/dist/runners/base/result-builders.js.map +1 -1
  111. package/dist/runners/base/time-check.d.ts +9 -0
  112. package/dist/runners/base/time-check.d.ts.map +1 -0
  113. package/dist/runners/base/time-check.js +18 -0
  114. package/dist/runners/base/time-check.js.map +1 -0
  115. package/dist/runners/base/usage-accumulator.d.ts +9 -0
  116. package/dist/runners/base/usage-accumulator.d.ts.map +1 -0
  117. package/dist/runners/base/usage-accumulator.js +19 -0
  118. package/dist/runners/base/usage-accumulator.js.map +1 -0
  119. package/dist/runners/claude-runner.d.ts.map +1 -1
  120. package/dist/runners/claude-runner.js +129 -175
  121. package/dist/runners/claude-runner.js.map +1 -1
  122. package/dist/runners/codex-runner.d.ts.map +1 -1
  123. package/dist/runners/codex-runner.js +96 -128
  124. package/dist/runners/codex-runner.js.map +1 -1
  125. package/dist/runners/error-classification.d.ts +11 -0
  126. package/dist/runners/error-classification.d.ts.map +1 -1
  127. package/dist/runners/error-classification.js +51 -0
  128. package/dist/runners/error-classification.js.map +1 -1
  129. package/dist/runners/openai-runner.d.ts.map +1 -1
  130. package/dist/runners/openai-runner.js +80 -171
  131. package/dist/runners/openai-runner.js.map +1 -1
  132. package/dist/runners/supervision.d.ts +0 -49
  133. package/dist/runners/supervision.d.ts.map +1 -1
  134. package/dist/runners/supervision.js +0 -67
  135. package/dist/runners/supervision.js.map +1 -1
  136. package/dist/runners/types.d.ts +12 -5
  137. package/dist/runners/types.d.ts.map +1 -1
  138. package/dist/telemetry/concern-classifier.d.ts +1 -1
  139. package/dist/telemetry/concern-classifier.d.ts.map +1 -1
  140. package/dist/telemetry/concern-classifier.js +5 -0
  141. package/dist/telemetry/concern-classifier.js.map +1 -1
  142. package/dist/telemetry/event-builder.d.ts.map +1 -1
  143. package/dist/telemetry/event-builder.js +5 -5
  144. package/dist/telemetry/event-builder.js.map +1 -1
  145. package/dist/telemetry/field-coverage.js +2 -2
  146. package/dist/telemetry/field-coverage.js.map +1 -1
  147. package/dist/telemetry/types.d.ts +139 -91
  148. package/dist/telemetry/types.d.ts.map +1 -1
  149. package/dist/telemetry/types.js +23 -17
  150. package/dist/telemetry/types.js.map +1 -1
  151. package/dist/types.d.ts +2 -2
  152. package/dist/types.d.ts.map +1 -1
  153. package/dist/types.js +5 -2
  154. package/dist/types.js.map +1 -1
  155. package/package.json +1 -1
@@ -1,14 +1,15 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
- import { computeCostUSD, computeSavedCostUSD } from '../types.js';
3
+ import { computeCostUSD } from '../types.js';
4
4
  import { createProvider } from '../provider.js';
5
5
  import { delegateWithEscalation } from '../delegate-with-escalation.js';
6
6
  import { pickEscalation, pickReviewer, maxRowsFor, } from '../escalation/policy.js';
7
7
  import { runWithFallback, makeSyntheticRunResult, TRANSPORT_FAILURES, isReviewTransportFailure, } from '../escalation/fallback.js';
8
8
  import { findModelCapabilities, findModelProfile } from '../routing/model-profiles.js';
9
+ import { canonicalIdentity } from '../routing/canonical-model-identity.js';
9
10
  import { HeartbeatTimer } from '../heartbeat.js';
10
11
  import { newStageIdleTracker, snapshotIdle } from './stage-idle-tracker.js';
11
- import { DEFAULT_TASK_TIMEOUT_MS, DEFAULT_STALL_TIMEOUT_MS } from '../config/schema.js';
12
+ import { DEFAULT_TASK_TIMEOUT_MS, DEFAULT_STALL_TIMEOUT_MS, MAX_TIME_PRESTOP_RATIO } from '../config/schema.js';
12
13
  import { runSpecReview } from '../review/spec-reviewer.js';
13
14
  import { makeSkippedReviewResult } from '../review/skipped-result.js';
14
15
  import { runQualityReview } from '../review/quality-reviewer.js';
@@ -201,6 +202,17 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
201
202
  function providerFor(tier) {
202
203
  return providers[tier];
203
204
  }
205
+ // Compute the implementer's canonical identity for reviewer separation (R3).
206
+ // Used as forbiddenIdentities on reviewer fallback calls so the reviewer
207
+ // never lands on the same effective backend as the implementer.
208
+ const implementerIdentity = (() => {
209
+ try {
210
+ return canonicalIdentity(resolved.provider.config);
211
+ }
212
+ catch {
213
+ return undefined;
214
+ }
215
+ })();
204
216
  // Partition filePaths into output targets before the worker runs.
205
217
  // Output targets are paths that do not yet exist on disk.
206
218
  const { outputTargets } = partitionFilePaths(task.filePaths, task.cwd ?? process.cwd());
@@ -216,17 +228,54 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
216
228
  'task_done_summary',
217
229
  'fallback', 'fallback_unavailable',
218
230
  'escalation', 'escalation_unavailable',
219
- 'stall_abort', 'cost_check',
231
+ 'stall_abort', 'cost_check', 'time_check',
220
232
  ]);
221
233
  const shortBatchEarly = verboseBatchIdEarly ? verboseBatchIdEarly.slice(0, 8) : '????????';
222
234
  const emitTaskEvent = (event, fields) => {
223
235
  if (bus && verboseBatchIdEarly !== undefined) {
236
+ const schemaEvent = event === 'heartbeat_timer' ? 'task_started' : event;
224
237
  const cleaned = {};
225
238
  for (const [key, value] of Object.entries(fields)) {
226
239
  if (value !== undefined)
227
240
  cleaned[key] = value;
228
241
  }
229
- bus.emit({ event, ts: new Date().toISOString(), batchId: verboseBatchIdEarly, taskIndex, ...cleaned });
242
+ // Keep verbose-line field names stable while emitting schema-declared
243
+ // telemetry envelopes in their authoritative persisted shape. EventSchemas
244
+ // validate the full envelope at EventBus.emit in dev/test, so production
245
+ // emission paths must construct schema-shaped keys before persistence.
246
+ if (schemaEvent === 'task_started') {
247
+ cleaned.route = routeKey || 'delegate';
248
+ cleaned.cwd = task.cwd ?? process.cwd();
249
+ for (const key of ['state', 'stage_count', 'tick_ms', 'reason'])
250
+ delete cleaned[key];
251
+ }
252
+ if (event === 'verify_step') {
253
+ if ('exit_code' in cleaned) {
254
+ cleaned.exitCode = cleaned.exit_code;
255
+ delete cleaned.exit_code;
256
+ }
257
+ if ('duration_ms' in cleaned) {
258
+ cleaned.durationMs = cleaned.duration_ms;
259
+ delete cleaned.duration_ms;
260
+ }
261
+ if ('error_message' in cleaned) {
262
+ cleaned.errorMessage = cleaned.error_message;
263
+ delete cleaned.error_message;
264
+ }
265
+ }
266
+ if (event === 'task_completed') {
267
+ if ('stages_json' in cleaned) {
268
+ cleaned.stages = cleaned.stages_json;
269
+ delete cleaned.stages_json;
270
+ }
271
+ if (!('cachedTokens' in cleaned))
272
+ cleaned.cachedTokens = null;
273
+ if (!('reasoningTokens' in cleaned))
274
+ cleaned.reasoningTokens = null;
275
+ if (!('stages' in cleaned))
276
+ cleaned.stages = JSON.stringify(stats);
277
+ }
278
+ bus.emit({ event: schemaEvent, ts: new Date().toISOString(), batchId: verboseBatchIdEarly, taskIndex, ...cleaned });
230
279
  }
231
280
  if (verboseStreamRaw && (verbose || DEFAULT_MODE_EVENTS.has(event))) {
232
281
  verboseStreamRaw(composeVerboseLine({ event, ts: new Date().toISOString(), batch: shortBatchEarly, task: taskIndex, ...toVerboseFields(fields) }));
@@ -398,9 +447,11 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
398
447
  }
399
448
  if (event.kind === 'turn_complete') {
400
449
  heartbeat?.markEvent('llm');
401
- const costUSD = computeCostUSD(event.cumulativeInputTokens, event.cumulativeOutputTokens, resolved.provider.config);
402
- const savedCostUSD = computeSavedCostUSD(costUSD, event.cumulativeInputTokens, event.cumulativeOutputTokens, task.parentModel);
403
- heartbeat?.updateCost(costUSD, savedCostUSD);
450
+ const providerConfig = _activeRunnerProviderConfig ?? resolved.provider.config;
451
+ const costUSD = computeCostUSD(event.cumulativeInputTokens, event.cumulativeOutputTokens, providerConfig);
452
+ _currentRunnerCostUSD = costUSD ?? 0;
453
+ const cumulativeCostUSD = (_completedRunnerCostUSD ?? 0) + _currentRunnerCostUSD;
454
+ heartbeat?.updateCost(cumulativeCostUSD, null);
404
455
  const nowTurn = Date.now();
405
456
  const turnDurMs = nowTurn - prevEventAtMs;
406
457
  prevEventAtMs = nowTurn;
@@ -410,7 +461,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
410
461
  output_tokens: event.cumulativeOutputTokens,
411
462
  cost: costUSD,
412
463
  duration_ms: turnDurMs,
413
- provider: resolved.provider.config.model,
464
+ provider: providerConfig.model,
414
465
  });
415
466
  }
416
467
  }
@@ -422,7 +473,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
422
473
  // any in-flight call gets a per-call timeoutMs clamped to remaining
423
474
  // budget so it returns its salvage promptly. The user gets *something*
424
475
  // back instead of an open-ended retry storm.
425
- const taskTimeoutMs = task.timeoutMs ?? config.defaults.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
476
+ const taskTimeoutMs = task.timeoutMs ?? config.defaults?.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
426
477
  const taskDeadlineMs = taskStartMs + taskTimeoutMs;
427
478
  // Stall watchdog: when no LLM / tool / text event has fired for this
428
479
  // many ms, the in-flight runner is force-aborted via `stallController`.
@@ -484,7 +535,48 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
484
535
  const model = provider?.config.model ?? config.agents[tier]?.model ?? resolvedModel;
485
536
  return { tier, family: modelFamily(model), model };
486
537
  };
487
- const runningCostUSD = () => taskCostUSD();
538
+ // §3.9: runningCostUSD must be cumulative and monotonic across explicit
539
+ // runner boundaries. Runner progress reports per-runner cumulative token
540
+ // counts, so lifecycle cost is completed runners + current runner partial.
541
+ // Boundaries are closed from actual RunResult.usage.costUSD values rather
542
+ // than inferred from drops; this handles reviewer costs greater than the
543
+ // implementer and preserves reviewer-provider pricing.
544
+ let _completedRunnerCostUSD = null;
545
+ let _currentRunnerCostUSD = 0;
546
+ let _activeRunnerProviderConfig = null;
547
+ let _prevRunningCost = null;
548
+ const runningCostUSD = () => {
549
+ const current = _completedRunnerCostUSD !== null || _currentRunnerCostUSD !== 0
550
+ ? (_completedRunnerCostUSD ?? 0) + _currentRunnerCostUSD
551
+ : null;
552
+ if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') {
553
+ if (_prevRunningCost !== null && current !== null && current < _prevRunningCost) {
554
+ throw new Error(`runningCostUSD non-monotonic: prev=${_prevRunningCost} now=${current}`);
555
+ }
556
+ _prevRunningCost = current;
557
+ }
558
+ return current;
559
+ };
560
+ const runAccounted = async (provider, call) => {
561
+ if (_activeRunnerProviderConfig !== null) {
562
+ throw new Error('lifecycle cost accounting runner overlap');
563
+ }
564
+ _activeRunnerProviderConfig = provider.config;
565
+ _currentRunnerCostUSD = 0;
566
+ try {
567
+ const result = await call();
568
+ const actualCost = result?.usage?.costUSD
569
+ ?? result?.metrics?.costUSD
570
+ ?? _currentRunnerCostUSD;
571
+ _completedRunnerCostUSD = (_completedRunnerCostUSD ?? 0) + actualCost;
572
+ _currentRunnerCostUSD = 0;
573
+ heartbeat?.updateCost(_completedRunnerCostUSD, null);
574
+ return result;
575
+ }
576
+ finally {
577
+ _activeRunnerProviderConfig = null;
578
+ }
579
+ };
488
580
  const policyEscalated = { spec: false, quality: false, diff: false };
489
581
  const emitFallback = (p) => {
490
582
  emitTaskEvent('fallback', p);
@@ -541,11 +633,21 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
541
633
  ...(fallbackOverrides.length > 0 ? { fallbackOverrides } : {}),
542
634
  };
543
635
  };
544
- const abortReviewLoop = (base, terminationReason, message, aborting) => ({
636
+ const abortReviewLoop = (base, terminationReason, message, aborting, wallClockMs) => ({
545
637
  ...base,
546
638
  status: 'incomplete',
547
639
  workerStatus: 'review_loop_aborted',
548
- terminationReason,
640
+ terminationReason: terminationReason === 'round_cap'
641
+ ? 'round_cap'
642
+ : {
643
+ cause: terminationReason === 'cost_ceiling' ? 'cost_exceeded' : 'time_ceiling',
644
+ turnsUsed: base.turns,
645
+ hasFileArtifacts: (base.filesWritten ?? []).length > 0,
646
+ usedShell: (base.toolCalls ?? []).some(c => c.startsWith('shell') || c.startsWith('runShell')),
647
+ workerSelfAssessment: 'review_loop_aborted',
648
+ wasPromoted: false,
649
+ ...(wallClockMs !== undefined ? { wallClockMs } : {}),
650
+ },
549
651
  reviewRounds: reviewRounds(),
550
652
  error: message,
551
653
  specReviewStatus: aborting === 'spec' ? 'changes_required' : (base.specReviewStatus ?? 'approved'),
@@ -562,7 +664,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
562
664
  const verification = await runVerifyStage({
563
665
  cwd,
564
666
  verifyCommand: task.verifyCommand,
565
- taskTimeoutMs: task.timeoutMs ?? config.defaults.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS,
667
+ taskTimeoutMs: task.timeoutMs ?? config.defaults?.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS,
566
668
  taskStartMs,
567
669
  });
568
670
  latestVerification = verification;
@@ -589,7 +691,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
589
691
  const cause = typeof result.terminationReason === 'object' ? result.terminationReason.cause : result.terminationReason;
590
692
  const capExhausted = result.capExhausted
591
693
  ?? (result.status === 'cost_exceeded' || cause === 'cost_exceeded' || cause === 'cost_ceiling' ? 'cost'
592
- : result.status === 'timeout' || cause === 'timeout' ? 'wall_clock'
694
+ : result.status === 'timeout' || cause === 'timeout' || cause === 'time_ceiling' ? 'wall_clock'
593
695
  : result.status === 'incomplete' && result.turns > 1 ? 'turn'
594
696
  : undefined);
595
697
  const lifecycleClarificationRequested = result.lifecycleClarificationRequested
@@ -605,7 +707,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
605
707
  return signalize({
606
708
  output: '',
607
709
  status: 'error',
608
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUSD: null },
710
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUSD: null, costDeltaVsParentUSD: null, cachedTokens: null, reasoningTokens: null },
609
711
  turns: 0,
610
712
  filesRead: [],
611
713
  filesWritten: [],
@@ -831,7 +933,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
831
933
  return withVerification({
832
934
  output: `Sub-agent error: task.cwd ${cwd} had pre-existing modifications`,
833
935
  status: 'error',
834
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUSD: null },
936
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUSD: null, costDeltaVsParentUSD: null, cachedTokens: null, reasoningTokens: null },
835
937
  turns: 0,
836
938
  filesRead: [],
837
939
  filesWritten: [],
@@ -858,7 +960,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
858
960
  isTransportFailure: (r) => TRANSPORT_FAILURES.has(r.status) && r.capExhausted === undefined,
859
961
  getStatus: (r) => r.status,
860
962
  makeSyntheticFailure: (assigned) => makeSyntheticRunResult(assigned, 'all_tiers_unavailable'),
861
- call: (provider) => delegateWithEscalation(withDoneCondition(task), [provider], { explicitlyPinned: false, onProgress: wrappedOnProgress, taskDeadlineMs, abortSignal: stallController.signal, assignedTier: initialDecision.impl }),
963
+ call: (provider) => runAccounted(provider, () => delegateWithEscalation(withDoneCondition(task), [provider], { explicitlyPinned: false, onProgress: wrappedOnProgress, taskDeadlineMs, abortSignal: stallController.signal, assignedTier: initialDecision.impl })),
862
964
  });
863
965
  if (initialImpl.fallbackFired || initialImpl.bothUnavailable) {
864
966
  fallbackOverrides.push({
@@ -1047,10 +1149,11 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1047
1149
  isTransportFailure: (r) => isReviewTransportFailure(r),
1048
1150
  getStatus: (r) => r.status,
1049
1151
  makeSyntheticFailure: () => makeSkippedReviewResult('all_tiers_unavailable'),
1050
- call: (provider) => runDiffReview({ cwd, diff: evidence.fullDiff, diffTruncated: evidence.diffTruncated, verification, worker: { call: (prompt, opts) => provider.run(prompt, { abortSignal: opts?.abortSignal, timeoutMs: opts?.timeoutMs }) }, taskDeadlineMs, abortSignal: stallController.signal }),
1152
+ forbiddenIdentities: implementerIdentity ? [implementerIdentity] : undefined,
1153
+ call: (provider) => runAccounted(provider, () => runDiffReview({ cwd, diff: evidence.fullDiff, diffTruncated: evidence.diffTruncated, verification, worker: { call: (prompt, opts) => provider.run(prompt, { cwd: opts?.cwd ?? cwd, abortSignal: opts?.abortSignal, timeoutMs: opts?.timeoutMs }) }, taskDeadlineMs, abortSignal: stallController.signal })),
1051
1154
  });
1052
1155
  if (diffCall.fallbackFired) {
1053
- emitFallback({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'diff', attempt: 0, role: 'diffReviewer', assignedTier: diffReviewerTier, usedTier: diffCall.usedTier, reason: diffCall.fallbackReason, triggeringStatus: diffCall.fallbackTriggeringStatus, violatesSeparation: diffCall.usedTier === implementerHistory[implementerHistory.length - 1] });
1156
+ emitFallback({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'diff', attempt: 0, role: 'diffReviewer', assignedTier: diffReviewerTier, usedTier: diffCall.usedTier, reason: diffCall.fallbackReason, triggeringStatus: diffCall.fallbackTriggeringStatus, violatesSeparation: diffCall.usedTier === implementerHistory[implementerHistory.length - 1], fallbackSeparationRespected: diffCall.fallbackSeparationRespected, assignedIdentity: diffCall.assignedIdentity ?? null, usedIdentity: diffCall.usedIdentity ?? null });
1054
1157
  fallbackOverrides.push({ role: 'diffReviewer', loop: 'diff', attempt: 0, assigned: diffReviewerTier, used: diffCall.usedTier, reason: diffCall.fallbackReason, triggeringStatus: diffCall.fallbackTriggeringStatus, bothUnavailable: diffCall.bothUnavailable });
1055
1158
  }
1056
1159
  if (diffCall.bothUnavailable) {
@@ -1118,7 +1221,8 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1118
1221
  isTransportFailure: (r) => isReviewTransportFailure(r),
1119
1222
  getStatus: (r) => r.status,
1120
1223
  makeSyntheticFailure: () => makeSkippedReviewResult('all_tiers_unavailable'),
1121
- call: (provider) => runSpecReview(provider, packet, effectiveImplReport, fileContents, implResult.toolCalls, task.planContext, evidence.block, taskDeadlineMs, stallController.signal, wrappedOnProgress),
1224
+ forbiddenIdentities: implementerIdentity ? [implementerIdentity] : undefined,
1225
+ call: (provider) => runAccounted(provider, () => runSpecReview(provider, packet, effectiveImplReport, fileContents, implResult.toolCalls, task.planContext, evidence.block, taskDeadlineMs, stallController.signal, wrappedOnProgress, cwd)),
1122
1226
  });
1123
1227
  specReviewDurationMs += Date.now() - initialSpecReviewIterStart;
1124
1228
  if (initialSpecReview.bothUnavailable) {
@@ -1129,7 +1233,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1129
1233
  else {
1130
1234
  specReviewerHistory.push(initialSpecReview.usedTier);
1131
1235
  if (initialSpecReview.fallbackFired) {
1132
- emitFallback({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'spec', attempt: 0, role: 'specReviewer', assignedTier: initialReviewerTier, usedTier: initialSpecReview.usedTier, reason: initialSpecReview.fallbackReason, triggeringStatus: initialSpecReview.fallbackTriggeringStatus, violatesSeparation: initialSpecReview.usedTier === implementerHistory[implementerHistory.length - 1] });
1236
+ emitFallback({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'spec', attempt: 0, role: 'specReviewer', assignedTier: initialReviewerTier, usedTier: initialSpecReview.usedTier, reason: initialSpecReview.fallbackReason, triggeringStatus: initialSpecReview.fallbackTriggeringStatus, violatesSeparation: initialSpecReview.usedTier === implementerHistory[implementerHistory.length - 1], fallbackSeparationRespected: initialSpecReview.fallbackSeparationRespected, assignedIdentity: initialSpecReview.assignedIdentity ?? null, usedIdentity: initialSpecReview.usedIdentity ?? null });
1133
1237
  fallbackOverrides.push({ role: 'specReviewer', loop: 'spec', attempt: 0, assigned: initialReviewerTier, used: initialSpecReview.usedTier, reason: initialSpecReview.fallbackReason, triggeringStatus: initialSpecReview.fallbackTriggeringStatus, bothUnavailable: false });
1134
1238
  }
1135
1239
  }
@@ -1149,6 +1253,11 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1149
1253
  emitTaskEvent('cost_check', { stage: 'spec_rework', tripped: true, cost_used_usd: currentCostUSD, cost_cap_usd: maxCostUSD, cost_available: true });
1150
1254
  return abortReviewLoop(finalImplResult, 'cost_ceiling', 'cost ceiling reached before spec rework', 'spec');
1151
1255
  }
1256
+ const wallClock = Date.now() - taskStartMs;
1257
+ if (wallClock >= MAX_TIME_PRESTOP_RATIO * taskTimeoutMs) {
1258
+ emitTaskEvent('time_check', { stage: 'spec_rework', tripped: true, wallClockMs: wallClock, timeoutMs: taskTimeoutMs });
1259
+ return abortReviewLoop(finalImplResult, 'time_ceiling', `time ceiling reached before spec rework (${wallClock}ms >= 0.8 × ${taskTimeoutMs}ms)`, 'spec', wallClock);
1260
+ }
1152
1261
  const decision = pickEscalation({ loop: 'spec', attemptIndex: specAttemptIndex, baseTier: resolved.slot });
1153
1262
  if (decision.isEscalated)
1154
1263
  emitEscalationEvent('spec', specAttemptIndex, decision);
@@ -1156,7 +1265,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1156
1265
  transitionStage('spec_review', 'spec_rework', { stage: 'spec_rework', stageIndex: 3, reviewRound: specAttemptIndex, attemptCap: maxSpecRows }, { attempt: specAttemptIndex, attemptCap: maxSpecRows, implTier: decision.impl, reviewerTier: decision.reviewer, escalated: decision.isEscalated });
1157
1266
  const feedback = specResult.findings.length > 0 ? `\n\n## Spec Review Feedback (round ${specAttemptIndex}):\n${specResult.findings.map(f => `- ${f}`).join('\n')}` : '';
1158
1267
  const reworkTask = withDoneCondition({ ...task, prompt: `${task.prompt}${feedback}` });
1159
- const reworkCall = await runWithFallback({ assigned: decision.impl, providerFor, unavailableTiers: specUnavailable, isTransportFailure: (r) => TRANSPORT_FAILURES.has(r.status) && r.capExhausted === undefined, getStatus: (r) => r.status, makeSyntheticFailure: (assigned) => makeSyntheticRunResult(assigned, 'all_tiers_unavailable'), call: (provider) => delegateWithEscalation(reworkTask, [provider], { explicitlyPinned: true, onProgress: wrappedOnProgress, taskDeadlineMs, abortSignal: stallController.signal, assignedTier: decision.impl }) });
1268
+ const reworkCall = await runWithFallback({ assigned: decision.impl, providerFor, unavailableTiers: specUnavailable, isTransportFailure: (r) => TRANSPORT_FAILURES.has(r.status) && r.capExhausted === undefined, getStatus: (r) => r.status, makeSyntheticFailure: (assigned) => makeSyntheticRunResult(assigned, 'all_tiers_unavailable'), call: (provider) => runAccounted(provider, () => delegateWithEscalation(reworkTask, [provider], { explicitlyPinned: true, onProgress: wrappedOnProgress, taskDeadlineMs, abortSignal: stallController.signal, assignedTier: decision.impl })) });
1160
1269
  if (reworkCall.fallbackFired || reworkCall.bothUnavailable)
1161
1270
  fallbackOverrides.push({ role: 'implementer', loop: 'spec', attempt: specAttemptIndex, assigned: decision.impl, used: reworkCall.usedTier, reason: (reworkCall.fallbackReason ?? reworkCall.unavailableReason), triggeringStatus: reworkCall.fallbackTriggeringStatus, bothUnavailable: reworkCall.bothUnavailable });
1162
1271
  if (reworkCall.fallbackFired) {
@@ -1180,7 +1289,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1180
1289
  commitReworkStage(stats, 'spec_rework', specReworkAcc, implementerAgentInfo);
1181
1290
  transitionStage('spec_rework', 'spec_review', { stage: 'spec_review', stageIndex: 2, reviewRound: specAttemptIndex + 1, attemptCap: maxSpecRows }, null);
1182
1291
  const reReviewIterStart = Date.now();
1183
- const reviewCall = await runWithFallback({ assigned: decision.reviewer, providerFor, unavailableTiers: specUnavailable, isTransportFailure: (r) => isReviewTransportFailure(r), getStatus: (r) => r.status, makeSyntheticFailure: () => makeSkippedReviewResult('all_tiers_unavailable'), call: (provider) => runSpecReview(provider, packet, finalImplReport, fileContents, finalImplResult.toolCalls, task.planContext, evidence.block, taskDeadlineMs, stallController.signal, wrappedOnProgress) });
1292
+ const reviewCall = await runWithFallback({ assigned: decision.reviewer, providerFor, unavailableTiers: specUnavailable, isTransportFailure: (r) => isReviewTransportFailure(r), getStatus: (r) => r.status, makeSyntheticFailure: () => makeSkippedReviewResult('all_tiers_unavailable'), forbiddenIdentities: implementerIdentity ? [implementerIdentity] : undefined, call: (provider) => runAccounted(provider, () => runSpecReview(provider, packet, finalImplReport, fileContents, finalImplResult.toolCalls, task.planContext, evidence.block, taskDeadlineMs, stallController.signal, wrappedOnProgress, cwd)) });
1184
1293
  specReviewDurationMs += Date.now() - reReviewIterStart;
1185
1294
  if (reviewCall.bothUnavailable) {
1186
1295
  emitFallbackUnavailable({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'spec', attempt: specAttemptIndex, role: 'specReviewer', assignedTier: decision.reviewer, reason: reviewCall.unavailableReason });
@@ -1190,7 +1299,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1190
1299
  else {
1191
1300
  specReviewerHistory.push(reviewCall.usedTier);
1192
1301
  if (reviewCall.fallbackFired) {
1193
- emitFallback({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'spec', attempt: specAttemptIndex, role: 'specReviewer', assignedTier: decision.reviewer, usedTier: reviewCall.usedTier, reason: reviewCall.fallbackReason, triggeringStatus: reviewCall.fallbackTriggeringStatus, violatesSeparation: reviewCall.usedTier === implementerHistory[implementerHistory.length - 1] });
1302
+ emitFallback({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'spec', attempt: specAttemptIndex, role: 'specReviewer', assignedTier: decision.reviewer, usedTier: reviewCall.usedTier, reason: reviewCall.fallbackReason, triggeringStatus: reviewCall.fallbackTriggeringStatus, violatesSeparation: reviewCall.usedTier === implementerHistory[implementerHistory.length - 1], fallbackSeparationRespected: reviewCall.fallbackSeparationRespected, assignedIdentity: reviewCall.assignedIdentity ?? null, usedIdentity: reviewCall.usedIdentity ?? null });
1194
1303
  fallbackOverrides.push({ role: 'specReviewer', loop: 'spec', attempt: specAttemptIndex, assigned: decision.reviewer, used: reviewCall.usedTier, reason: reviewCall.fallbackReason, triggeringStatus: reviewCall.fallbackTriggeringStatus, bothUnavailable: false });
1195
1304
  }
1196
1305
  }
@@ -1234,7 +1343,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1234
1343
  qualityReviewT0 = Date.now();
1235
1344
  qualityReviewC0 = runningCostUSD();
1236
1345
  const initialQualityIterStart = Date.now();
1237
- const initialQuality = await runWithFallback({ assigned: qualityReviewerTier, providerFor, unavailableTiers: qualityUnavailable, isTransportFailure: (r) => isReviewTransportFailure(r), getStatus: (r) => r.status, makeSyntheticFailure: () => makeSkippedReviewResult('all_tiers_unavailable'), call: (provider) => runQualityReview(provider, packet, specReport ?? finalImplReport, fileContents, finalImplResult.toolCalls, finalImplResult.filesWritten, evidence.block, qualityReviewPromptBuilder, finalImplResult.output, taskDeadlineMs, stallController.signal, wrappedOnProgress) });
1346
+ const initialQuality = await runWithFallback({ assigned: qualityReviewerTier, providerFor, unavailableTiers: qualityUnavailable, isTransportFailure: (r) => isReviewTransportFailure(r), getStatus: (r) => r.status, makeSyntheticFailure: () => makeSkippedReviewResult('all_tiers_unavailable'), forbiddenIdentities: implementerIdentity ? [implementerIdentity] : undefined, call: (provider) => runAccounted(provider, () => runQualityReview(provider, packet, specReport ?? finalImplReport, fileContents, finalImplResult.toolCalls, finalImplResult.filesWritten, evidence.block, qualityReviewPromptBuilder, finalImplResult.output, taskDeadlineMs, stallController.signal, wrappedOnProgress, cwd)) });
1238
1347
  qualityReviewDurationMs += Date.now() - initialQualityIterStart;
1239
1348
  if (initialQuality.bothUnavailable) {
1240
1349
  emitFallbackUnavailable({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'quality', attempt: 0, role: 'qualityReviewer', assignedTier: qualityReviewerTier, reason: initialQuality.unavailableReason });
@@ -1244,7 +1353,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1244
1353
  else {
1245
1354
  qualityReviewerHistory.push(initialQuality.usedTier);
1246
1355
  if (initialQuality.fallbackFired) {
1247
- emitFallback({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'quality', attempt: 0, role: 'qualityReviewer', assignedTier: qualityReviewerTier, usedTier: initialQuality.usedTier, reason: initialQuality.fallbackReason, triggeringStatus: initialQuality.fallbackTriggeringStatus, violatesSeparation: initialQuality.usedTier === implementerHistory[implementerHistory.length - 1] });
1356
+ emitFallback({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'quality', attempt: 0, role: 'qualityReviewer', assignedTier: qualityReviewerTier, usedTier: initialQuality.usedTier, reason: initialQuality.fallbackReason, triggeringStatus: initialQuality.fallbackTriggeringStatus, violatesSeparation: initialQuality.usedTier === implementerHistory[implementerHistory.length - 1], fallbackSeparationRespected: initialQuality.fallbackSeparationRespected, assignedIdentity: initialQuality.assignedIdentity ?? null, usedIdentity: initialQuality.usedIdentity ?? null });
1248
1357
  fallbackOverrides.push({ role: 'qualityReviewer', loop: 'quality', attempt: 0, assigned: qualityReviewerTier, used: initialQuality.usedTier, reason: initialQuality.fallbackReason, triggeringStatus: initialQuality.fallbackTriggeringStatus, bothUnavailable: false });
1249
1358
  }
1250
1359
  }
@@ -1288,8 +1397,6 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1288
1397
  : 'error',
1289
1398
  iterationIndex: 1,
1290
1399
  findingsReviewed: annotated.length,
1291
- findingsFlagged: 0, // legacy field — severity correction tracked elsewhere now
1292
- severityCorrections: 0, // reviewerSeverity field removed in 3.10.5
1293
1400
  meanConfidence,
1294
1401
  durationMs: Date.now() - qualityReviewT0,
1295
1402
  costUSD: runningCostUSD() !== null && qualityReviewC0 !== null ? runningCostUSD() - qualityReviewC0 : null,
@@ -1307,6 +1414,11 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1307
1414
  emitTaskEvent('cost_check', { stage: 'quality_rework', tripped: true, cost_used_usd: currentCostUSD, cost_cap_usd: maxCostUSD, cost_available: true });
1308
1415
  return abortReviewLoop(finalImplResult, 'cost_ceiling', 'cost ceiling reached before quality rework', 'quality');
1309
1416
  }
1417
+ const wallClock = Date.now() - taskStartMs;
1418
+ if (wallClock >= MAX_TIME_PRESTOP_RATIO * taskTimeoutMs) {
1419
+ emitTaskEvent('time_check', { stage: 'quality_rework', tripped: true, wallClockMs: wallClock, timeoutMs: taskTimeoutMs });
1420
+ return abortReviewLoop(finalImplResult, 'time_ceiling', `time ceiling reached before quality rework (${wallClock}ms >= 0.8 × ${taskTimeoutMs}ms)`, 'quality', wallClock);
1421
+ }
1310
1422
  const decision = pickEscalation({ loop: 'quality', attemptIndex: qualityAttemptIndex, baseTier: resolved.slot });
1311
1423
  if (decision.isEscalated)
1312
1424
  emitEscalationEvent('quality', qualityAttemptIndex, decision);
@@ -1314,7 +1426,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1314
1426
  transitionStage('quality_review', 'quality_rework', { stage: 'quality_rework', stageIndex: 5, reviewRound: qualityAttemptIndex, attemptCap: maxQualityRows }, { attempt: qualityAttemptIndex, attemptCap: maxQualityRows, implTier: decision.impl, reviewerTier: decision.reviewer, escalated: decision.isEscalated });
1315
1427
  const feedback = qualityResult.findings.length > 0 ? `\n\n## Quality Review Feedback (round ${qualityAttemptIndex}):\n${qualityResult.findings.map(f => `- ${f}`).join('\n')}` : '';
1316
1428
  const reworkTask = withDoneCondition({ ...task, prompt: `${task.prompt}${feedback}` });
1317
- const reworkCall = await runWithFallback({ assigned: decision.impl, providerFor, unavailableTiers: qualityUnavailable, isTransportFailure: (r) => TRANSPORT_FAILURES.has(r.status) && r.capExhausted === undefined, getStatus: (r) => r.status, makeSyntheticFailure: (assigned) => makeSyntheticRunResult(assigned, 'all_tiers_unavailable'), call: (provider) => delegateWithEscalation(reworkTask, [provider], { explicitlyPinned: true, onProgress: wrappedOnProgress, taskDeadlineMs, abortSignal: stallController.signal, assignedTier: decision.impl }) });
1429
+ const reworkCall = await runWithFallback({ assigned: decision.impl, providerFor, unavailableTiers: qualityUnavailable, isTransportFailure: (r) => TRANSPORT_FAILURES.has(r.status) && r.capExhausted === undefined, getStatus: (r) => r.status, makeSyntheticFailure: (assigned) => makeSyntheticRunResult(assigned, 'all_tiers_unavailable'), call: (provider) => runAccounted(provider, () => delegateWithEscalation(reworkTask, [provider], { explicitlyPinned: true, onProgress: wrappedOnProgress, taskDeadlineMs, abortSignal: stallController.signal, assignedTier: decision.impl })) });
1318
1430
  if (reworkCall.fallbackFired || reworkCall.bothUnavailable)
1319
1431
  fallbackOverrides.push({ role: 'implementer', loop: 'quality', attempt: qualityAttemptIndex, assigned: decision.impl, used: reworkCall.usedTier, reason: (reworkCall.fallbackReason ?? reworkCall.unavailableReason), triggeringStatus: reworkCall.fallbackTriggeringStatus, bothUnavailable: reworkCall.bothUnavailable });
1320
1432
  if (reworkCall.fallbackFired)
@@ -1335,7 +1447,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1335
1447
  commitReworkStage(stats, 'quality_rework', qualityReworkAcc, implementerAgentInfo);
1336
1448
  transitionStage('quality_rework', 'quality_review', { stage: 'quality_review', stageIndex: 4, reviewRound: qualityAttemptIndex + 1, attemptCap: maxQualityRows }, null);
1337
1449
  const qReReviewIterStart = Date.now();
1338
- const reviewCall = await runWithFallback({ assigned: decision.reviewer, providerFor, unavailableTiers: qualityUnavailable, isTransportFailure: (r) => isReviewTransportFailure(r), getStatus: (r) => r.status, makeSyntheticFailure: () => makeSkippedReviewResult('all_tiers_unavailable'), call: (provider) => runQualityReview(provider, packet, finalImplReport, fileContents, finalImplResult.toolCalls, finalImplResult.filesWritten, evidence.block, qualityReviewPromptBuilder, finalImplResult.output, taskDeadlineMs, stallController.signal, wrappedOnProgress) });
1450
+ const reviewCall = await runWithFallback({ assigned: decision.reviewer, providerFor, unavailableTiers: qualityUnavailable, isTransportFailure: (r) => isReviewTransportFailure(r), getStatus: (r) => r.status, makeSyntheticFailure: () => makeSkippedReviewResult('all_tiers_unavailable'), forbiddenIdentities: implementerIdentity ? [implementerIdentity] : undefined, call: (provider) => runAccounted(provider, () => runQualityReview(provider, packet, finalImplReport, fileContents, finalImplResult.toolCalls, finalImplResult.filesWritten, evidence.block, qualityReviewPromptBuilder, finalImplResult.output, taskDeadlineMs, stallController.signal, wrappedOnProgress, cwd)) });
1339
1451
  qualityReviewDurationMs += Date.now() - qReReviewIterStart;
1340
1452
  if (reviewCall.bothUnavailable) {
1341
1453
  emitFallbackUnavailable({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'quality', attempt: qualityAttemptIndex, role: 'qualityReviewer', assignedTier: decision.reviewer, reason: reviewCall.unavailableReason });
@@ -1345,7 +1457,7 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1345
1457
  else {
1346
1458
  qualityReviewerHistory.push(reviewCall.usedTier);
1347
1459
  if (reviewCall.fallbackFired) {
1348
- emitFallback({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'quality', attempt: qualityAttemptIndex, role: 'qualityReviewer', assignedTier: decision.reviewer, usedTier: reviewCall.usedTier, reason: reviewCall.fallbackReason, triggeringStatus: reviewCall.fallbackTriggeringStatus, violatesSeparation: reviewCall.usedTier === implementerHistory[implementerHistory.length - 1] });
1460
+ emitFallback({ batchId: heartbeatWiring?.batchId ?? '', taskIndex, loop: 'quality', attempt: qualityAttemptIndex, role: 'qualityReviewer', assignedTier: decision.reviewer, usedTier: reviewCall.usedTier, reason: reviewCall.fallbackReason, triggeringStatus: reviewCall.fallbackTriggeringStatus, violatesSeparation: reviewCall.usedTier === implementerHistory[implementerHistory.length - 1], fallbackSeparationRespected: reviewCall.fallbackSeparationRespected, assignedIdentity: reviewCall.assignedIdentity ?? null, usedIdentity: reviewCall.usedIdentity ?? null });
1349
1461
  fallbackOverrides.push({ role: 'qualityReviewer', loop: 'quality', attempt: qualityAttemptIndex, assigned: decision.reviewer, used: reviewCall.usedTier, reason: reviewCall.fallbackReason, triggeringStatus: reviewCall.fallbackTriggeringStatus, bothUnavailable: false });
1350
1462
  }
1351
1463
  }
@@ -1515,6 +1627,8 @@ export async function executeReviewedLifecycle(task, resolved, config, taskIndex
1515
1627
  toolCalls: r.toolCalls?.length ?? 0,
1516
1628
  inputTokens: r.usage.inputTokens,
1517
1629
  outputTokens: r.usage.outputTokens,
1630
+ cachedTokens: r.usage.cachedTokens ?? null,
1631
+ reasoningTokens: r.usage.reasoningTokens ?? null,
1518
1632
  costUSD: r.usage.costUSD,
1519
1633
  taskMaxIdleMs: r.taskMaxIdleMs ?? null,
1520
1634
  stallTriggered: r.stallTriggered ?? false,